From 8ad4814a0d8d12cc278047ddc719aad2f65fb0f9 Mon Sep 17 00:00:00 2001 From: matcha Date: Sun, 4 Oct 2026 03:25:09 +0000 Subject: [PATCH] Define Phast v0 management MCP vocabulary --- server.py | 49 +++++++++++++++++++++++++++++++++++++------------ 1 file changed, 37 insertions(+), 12 deletions(-) diff --git a/server.py b/server.py index 25f7127..3c29977 100644 --- a/server.py +++ b/server.py @@ -28,7 +28,7 @@ except RegistryUnavailable as error: mcp = FastMCP( "My Phast", - instructions="Owner control plane for Phast app manifests. Mutations fail closed until owner auth is installed.", + instructions="Management MCP for portable Phast tool belts and Prefab homes. Mutations fail closed until owner auth and durable provisioning are installed.", ) def _require_registry() -> AppRegistry: @@ -53,35 +53,60 @@ def phast_status() -> dict: "auth_blocker": AUTH_BLOCKER, } -@mcp.tool(name="apps_list", annotations={"readOnlyHint": True, "destructiveHint": False, "idempotentHint": True}) -def apps_list() -> dict: +@mcp.tool(name="phasts_list", annotations={"readOnlyHint": True, "destructiveHint": False, "idempotentHint": True}) +def phasts_list() -> dict: return {"apps": _require_registry().list()} -@mcp.tool(name="apps_get", annotations={"readOnlyHint": True, "destructiveHint": False, "idempotentHint": True}) -def apps_get(slug: str) -> dict: +@mcp.tool(name="phasts_get", annotations={"readOnlyHint": True, "destructiveHint": False, "idempotentHint": True}) +def phasts_get(slug: str) -> dict: result = _require_registry().get(slug) if result is None: raise KeyError(f"unknown app: {slug}") return result -@mcp.tool(name="apps_create", annotations={"readOnlyHint": False, "destructiveHint": True, "idempotentHint": False}) -def apps_create(slug: str, name: str, base_url: str, human_route: str = "/", mcp_route: str = "/mcp") -> dict: +@mcp.tool(name="phasts_create", annotations={"readOnlyHint": False, "destructiveHint": True, "idempotentHint": False}) +def phasts_create(slug: str, name: str, base_url: str, human_route: str = "/", mcp_route: str = "/mcp") -> dict: _require_owner_auth() return _require_registry().create(slug=slug, name=name, base_url=base_url, human_route=human_route, mcp_route=mcp_route) -@mcp.tool(name="apps_update", annotations={"readOnlyHint": False, "destructiveHint": True, "idempotentHint": True}) -def apps_update(slug: str, name: str | None = None, base_url: str | None = None, +@mcp.tool(name="phasts_update", annotations={"readOnlyHint": False, "destructiveHint": True, "idempotentHint": True}) +def phasts_update(slug: str, name: str | None = None, base_url: str | None = None, human_route: str | None = None, mcp_route: str | None = None) -> dict: _require_owner_auth() return _require_registry().update(slug, name=name, base_url=base_url, human_route=human_route, mcp_route=mcp_route) +@mcp.tool(name="providers_search", annotations={"readOnlyHint": True, "destructiveHint": False, "idempotentHint": True}) +def providers_search(query: str) -> dict: + """V0 seam: provider discovery belongs to Oracle, not a hard-coded Phast catalog.""" + return {"query": query, "source": "oracle", "status": "discovery-route-pending"} + +@mcp.tool(name="widgets_list", annotations={"readOnlyHint": True, "destructiveHint": False, "idempotentHint": True}) +def widgets_list(slug: str) -> dict: + item = phasts_get(slug) + return {"phast": slug, "widgets": item.get("prefab", {}).get("home", {}).get("widgets", [])} + +@mcp.tool(name="widgets_add", annotations={"readOnlyHint": False, "destructiveHint": True, "idempotentHint": False}) +def widgets_add(slug: str, widget: dict) -> dict: + _require_owner_auth() + raise NotImplementedError("durable phast.json widget persistence is not installed yet") + +@mcp.tool(name="widgets_update", annotations={"readOnlyHint": False, "destructiveHint": True, "idempotentHint": True}) +def widgets_update(slug: str, widget_id: str, widget: dict) -> dict: + _require_owner_auth() + raise NotImplementedError("durable phast.json widget persistence is not installed yet") + +@mcp.tool(name="widgets_remove", annotations={"readOnlyHint": False, "destructiveHint": True, "idempotentHint": True}) +def widgets_remove(slug: str, widget_id: str) -> dict: + _require_owner_auth() + raise NotImplementedError("durable phast.json widget persistence is not installed yet") + def control_app() -> PrefabApp: apps = registry.list() if registry is not None else [] with PrefabApp(title="My Phast", css_class="max-w-4xl mx-auto p-6", mode="light") as result: with Column(gap=5): with Row(gap=3, justify="between", align="center"): with Column(gap=1): - Text("PHAST · OWNER CONTROL PLANE", code=True) + Text("PHAST · OWNER CONTROL PLANE", code=[REDACTED] Heading("My Phast", level=1) Badge("read-only" if registry is not None else "setup required", variant="warning") Text("Your apps share one boring convention: a human surface at / and an agent surface at /mcp.") @@ -96,7 +121,7 @@ def control_app() -> PrefabApp: with Row(gap=3, justify="between", align="center"): with Column(gap=1): Heading(item["name"], level=3) - Text(item["base_url"], code=True) + Text(item["base_url"], code=[REDACTED] Badge(item["runtime"], variant="outline") Text(f"human {item['human']['route']} · mcp {item['mcp']['route']}") else: @@ -145,4 +170,4 @@ app = mcp.http_app( ) if __name__ == "__main__": - uvicorn.run(app, host="0.0.0.0", port=int(os.environ.get("PORT", "8080"))) + uvicorn.run(app, host="0.0.0.0", port=int(os.environ.get("PORT", "8080"))) \ No newline at end of file