Exosphere is a set of small, modular, self-hostable community tools built on the AT Protocol. app.exosphere.site
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: multi sphere mcp

Hugo ff8ee558 7b0ab6cd

+43 -16
+12 -1
packages/app/src/server.ts
··· 46 46 app.get("/api/health", (c) => c.json({ status: "ok" })); 47 47 48 48 // Mount MCP server (Streamable HTTP, stateless mode) 49 + // Each sphere gets its own endpoint: /mcp/:sphereHandle 49 50 app.route( 50 51 "/mcp", 51 - createMcpRoutes((path) => app.request(path)), 52 + createMcpRoutes((sphereHandle) => (path) => { 53 + // Rewrite /api/spheres/current → /api/spheres/:handle 54 + if (path === "/api/spheres/current") { 55 + return app.request(`/api/spheres/${sphereHandle}`); 56 + } 57 + // In multi-sphere mode, rewrite /api/<module>/… → /api/s/:handle/<module>/… 58 + if (isMultiSphere) { 59 + return app.request(path.replace(/^\/api\//, `/api/s/${sphereHandle}/`)); 60 + } 61 + return app.request(path); 62 + }), 52 63 ); 53 64 54 65 const isProd = process.env.NODE_ENV === "production";
+21 -10
packages/mcp/src/__tests__/routes.test.ts
··· 4 4 import { PROTOCOL_VERSION, SERVER_INFO } from "../protocol.ts"; 5 5 import { tools } from "../tools/index.ts"; 6 6 7 + const TEST_SPHERE_HANDLE = "test.bsky.social"; 8 + 7 9 /** Helper: build a mock API backend and mount MCP routes on it. */ 8 10 function createTestApp() { 9 11 const api = new Hono(); 10 12 11 - // Mock sphere endpoint 12 - api.get("/api/spheres/current", (c) => 13 - c.json({ 14 - sphere: { name: "Test Sphere", handle: "test.bsky.social", visibility: "public" }, 13 + // Mock sphere endpoint (by handle) 14 + api.get("/api/spheres/:handle", (c) => { 15 + if (c.req.param("handle") !== TEST_SPHERE_HANDLE) { 16 + return c.json({ error: "Sphere not found" }, 404); 17 + } 18 + return c.json({ 19 + sphere: { name: "Test Sphere", handle: TEST_SPHERE_HANDLE, visibility: "public" }, 15 20 modules: ["feature-requests"], 16 21 memberCount: 5, 17 22 role: null, 18 - }), 19 - ); 23 + }); 24 + }); 20 25 21 26 // Mock feature request list 22 27 api.get("/api/feature-requests", (c) => { ··· 76 81 77 82 api.route( 78 83 "/mcp", 79 - createMcpRoutes((path) => api.request(path)), 84 + createMcpRoutes((sphereHandle) => (path) => { 85 + // Rewrite sphere/current to sphere/:handle (mirrors server.ts logic) 86 + if (path === "/api/spheres/current") { 87 + return api.request(`/api/spheres/${sphereHandle}`); 88 + } 89 + return api.request(path); 90 + }), 80 91 ); 81 92 return api; 82 93 } 83 94 84 95 function mcpRequest(app: Hono, body: unknown) { 85 - return app.request("/mcp", { 96 + return app.request(`/mcp/${TEST_SPHERE_HANDLE}`, { 86 97 method: "POST", 87 98 headers: { "Content-Type": "application/json" }, 88 99 body: JSON.stringify(body), ··· 201 212 202 213 describe("GET and DELETE", () => { 203 214 it("returns 405 for GET", async () => { 204 - const res = await app.request("/mcp"); 215 + const res = await app.request(`/mcp/${TEST_SPHERE_HANDLE}`); 205 216 expect(res.status).toBe(405); 206 217 }); 207 218 208 219 it("returns 405 for DELETE", async () => { 209 - const res = await app.request("/mcp", { method: "DELETE" }); 220 + const res = await app.request(`/mcp/${TEST_SPHERE_HANDLE}`, { method: "DELETE" }); 210 221 expect(res.status).toBe(405); 211 222 }); 212 223 });
+10 -5
packages/mcp/src/routes.ts
··· 63 63 64 64 /** 65 65 * Create MCP Streamable HTTP routes (stateless mode). 66 - * @param apiFetch - function to make internal API calls (typically `(path) => app.request(path)`) 66 + * Each sphere gets its own MCP endpoint at `/mcp/:sphereHandle`. 67 + * @param apiFetchFactory - given a sphere handle, returns an `ApiFetch` that routes to the correct sphere API 67 68 */ 68 - export function createMcpRoutes(apiFetch: ApiFetch) { 69 + export function createMcpRoutes(apiFetchFactory: (sphereHandle: string) => ApiFetch) { 69 70 const mcp = new Hono(); 70 71 71 - mcp.post("/", async (c) => { 72 + mcp.post("/:sphereHandle", async (c) => { 73 + const sphereHandle = c.req.param("sphereHandle"); 74 + const apiFetch = apiFetchFactory(sphereHandle); 72 75 const body = (await c.req.json()) as JsonRpcRequest | JsonRpcRequest[]; 73 76 74 77 // Batch request ··· 89 92 }); 90 93 91 94 // SSE endpoint — not supported in stateless mode 92 - mcp.get("/", (c) => c.json({ error: "SSE not supported in stateless mode" }, 405)); 95 + mcp.get("/:sphereHandle", (c) => c.json({ error: "SSE not supported in stateless mode" }, 405)); 93 96 94 97 // Session termination — not applicable in stateless mode 95 - mcp.delete("/", (c) => c.json({ error: "Sessions not supported in stateless mode" }, 405)); 98 + mcp.delete("/:sphereHandle", (c) => 99 + c.json({ error: "Sessions not supported in stateless mode" }, 405), 100 + ); 96 101 97 102 return mcp; 98 103 }