diff --git a/packages/app/src/server.ts b/packages/app/src/server.ts index d058fb1..0af4d6a 100644 --- a/packages/app/src/server.ts +++ b/packages/app/src/server.ts @@ -46,9 +46,20 @@ if (!process.env.DISABLE_INDEXER) { app.get("/api/health", (c) => c.json({ status: "ok" })); // Mount MCP server (Streamable HTTP, stateless mode) +// Each sphere gets its own endpoint: /mcp/:sphereHandle app.route( "/mcp", - createMcpRoutes((path) => app.request(path)), + createMcpRoutes((sphereHandle) => (path) => { + // Rewrite /api/spheres/current → /api/spheres/:handle + if (path === "/api/spheres/current") { + return app.request(`/api/spheres/${sphereHandle}`); + } + // In multi-sphere mode, rewrite /api//… → /api/s/:handle//… + if (isMultiSphere) { + return app.request(path.replace(/^\/api\//, `/api/s/${sphereHandle}/`)); + } + return app.request(path); + }), ); const isProd = process.env.NODE_ENV === "production"; diff --git a/packages/mcp/src/__tests__/routes.test.ts b/packages/mcp/src/__tests__/routes.test.ts index a90127b..841f1e7 100644 --- a/packages/mcp/src/__tests__/routes.test.ts +++ b/packages/mcp/src/__tests__/routes.test.ts @@ -4,19 +4,24 @@ import { createMcpRoutes } from "../routes.ts"; import { PROTOCOL_VERSION, SERVER_INFO } from "../protocol.ts"; import { tools } from "../tools/index.ts"; +const TEST_SPHERE_HANDLE = "test.bsky.social"; + /** Helper: build a mock API backend and mount MCP routes on it. */ function createTestApp() { const api = new Hono(); - // Mock sphere endpoint - api.get("/api/spheres/current", (c) => - c.json({ - sphere: { name: "Test Sphere", handle: "test.bsky.social", visibility: "public" }, + // Mock sphere endpoint (by handle) + api.get("/api/spheres/:handle", (c) => { + if (c.req.param("handle") !== TEST_SPHERE_HANDLE) { + return c.json({ error: "Sphere not found" }, 404); + } + return c.json({ + sphere: { name: "Test Sphere", handle: TEST_SPHERE_HANDLE, visibility: "public" }, modules: ["feature-requests"], memberCount: 5, role: null, - }), - ); + }); + }); // Mock feature request list api.get("/api/feature-requests", (c) => { @@ -76,13 +81,19 @@ function createTestApp() { api.route( "/mcp", - createMcpRoutes((path) => api.request(path)), + createMcpRoutes((sphereHandle) => (path) => { + // Rewrite sphere/current to sphere/:handle (mirrors server.ts logic) + if (path === "/api/spheres/current") { + return api.request(`/api/spheres/${sphereHandle}`); + } + return api.request(path); + }), ); return api; } function mcpRequest(app: Hono, body: unknown) { - return app.request("/mcp", { + return app.request(`/mcp/${TEST_SPHERE_HANDLE}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), @@ -201,12 +212,12 @@ describe("MCP protocol", () => { describe("GET and DELETE", () => { it("returns 405 for GET", async () => { - const res = await app.request("/mcp"); + const res = await app.request(`/mcp/${TEST_SPHERE_HANDLE}`); expect(res.status).toBe(405); }); it("returns 405 for DELETE", async () => { - const res = await app.request("/mcp", { method: "DELETE" }); + const res = await app.request(`/mcp/${TEST_SPHERE_HANDLE}`, { method: "DELETE" }); expect(res.status).toBe(405); }); }); diff --git a/packages/mcp/src/routes.ts b/packages/mcp/src/routes.ts index a88f3b3..f633487 100644 --- a/packages/mcp/src/routes.ts +++ b/packages/mcp/src/routes.ts @@ -63,12 +63,15 @@ async function handleMessage( /** * Create MCP Streamable HTTP routes (stateless mode). - * @param apiFetch - function to make internal API calls (typically `(path) => app.request(path)`) + * Each sphere gets its own MCP endpoint at `/mcp/:sphereHandle`. + * @param apiFetchFactory - given a sphere handle, returns an `ApiFetch` that routes to the correct sphere API */ -export function createMcpRoutes(apiFetch: ApiFetch) { +export function createMcpRoutes(apiFetchFactory: (sphereHandle: string) => ApiFetch) { const mcp = new Hono(); - mcp.post("/", async (c) => { + mcp.post("/:sphereHandle", async (c) => { + const sphereHandle = c.req.param("sphereHandle"); + const apiFetch = apiFetchFactory(sphereHandle); const body = (await c.req.json()) as JsonRpcRequest | JsonRpcRequest[]; // Batch request @@ -89,10 +92,12 @@ export function createMcpRoutes(apiFetch: ApiFetch) { }); // SSE endpoint — not supported in stateless mode - mcp.get("/", (c) => c.json({ error: "SSE not supported in stateless mode" }, 405)); + mcp.get("/:sphereHandle", (c) => c.json({ error: "SSE not supported in stateless mode" }, 405)); // Session termination — not applicable in stateless mode - mcp.delete("/", (c) => c.json({ error: "Sessions not supported in stateless mode" }, 405)); + mcp.delete("/:sphereHandle", (c) => + c.json({ error: "Sessions not supported in stateless mode" }, 405), + ); return mcp; }