From aa9a7e7e4034426a10ac7d3c499e29eeed7c89a0 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Tue, 22 Sep 2026 13:41:02 +0100 Subject: [PATCH 1/2] fix: make hosted MCP connector compatible with Claude and Grok --- apps/web/__tests__/unit/mcp-auth.test.ts | 3 +- apps/web/__tests__/unit/mcp-server.test.ts | 50 +++++++++++----------- apps/web/app/api/mcp/route.ts | 1 + apps/web/lib/mcp-auth.ts | 4 +- apps/web/lib/mcp-server.ts | 1 + 5 files changed, 33 insertions(+), 26 deletions(-) diff --git a/apps/web/__tests__/unit/mcp-auth.test.ts b/apps/web/__tests__/unit/mcp-auth.test.ts index 550e14d8f3..ba89ac047d 100644 --- a/apps/web/__tests__/unit/mcp-auth.test.ts +++ b/apps/web/__tests__/unit/mcp-auth.test.ts @@ -97,9 +97,10 @@ describe("MCP OAuth authorization", () => { true, ); expect(isMcpRedirectUri("http://127.0.0.1:49152/callback")).toBe(true); + expect(isMcpRedirectUri("http://localhost:49152/callback")).toBe(true); for (const uri of [ "http://chatgpt.com/connector/callback", - "http://localhost:49152/callback", + "http://localhost.evil.example:49152/callback", "http://127.0.0.1/callback", "https://chatgpt.com/connector/callback#fragment", "https://user:password@chatgpt.com/callback", diff --git a/apps/web/__tests__/unit/mcp-server.test.ts b/apps/web/__tests__/unit/mcp-server.test.ts index 1646bd6e3a..ad91c88428 100644 --- a/apps/web/__tests__/unit/mcp-server.test.ts +++ b/apps/web/__tests__/unit/mcp-server.test.ts @@ -83,30 +83,31 @@ describe("hosted MCP transport", () => { }); it("preserves browser preflight and OAuth challenge headers", async () => { - const origin = "https://chatgpt.com"; - const preflight = await OPTIONS( - new Request("https://cap.so/api/mcp", { - method: "OPTIONS", - headers: { Origin: origin }, - }), - ); - expect(preflight.status).toBe(204); - expect(preflight.headers.get("access-control-allow-origin")).toBe(origin); - expect(preflight.headers.get("cache-control")).toBe("no-store"); - const unauthorized = await POST( - new Request("https://cap.so/api/mcp", { - method: "POST", - headers: { Origin: origin, "Content-Type": "application/json" }, - body: "{}", - }), - ); - expect(unauthorized.status).toBe(401); - expect(unauthorized.headers.get("access-control-allow-origin")).toBe( - origin, - ); - expect(unauthorized.headers.get("www-authenticate")).toContain( - "resource_metadata", - ); + for (const origin of ["https://chatgpt.com", "https://grok.com"]) { + const preflight = await OPTIONS( + new Request("https://cap.so/api/mcp", { + method: "OPTIONS", + headers: { Origin: origin }, + }), + ); + expect(preflight.status).toBe(204); + expect(preflight.headers.get("access-control-allow-origin")).toBe(origin); + expect(preflight.headers.get("cache-control")).toBe("no-store"); + const unauthorized = await POST( + new Request("https://cap.so/api/mcp", { + method: "POST", + headers: { Origin: origin, "Content-Type": "application/json" }, + body: "{}", + }), + ); + expect(unauthorized.status).toBe(401); + expect(unauthorized.headers.get("access-control-allow-origin")).toBe( + origin, + ); + expect(unauthorized.headers.get("www-authenticate")).toContain( + "resource_metadata", + ); + } }); it("keeps host, origin, media type, and body limits ahead of MCP dispatch", async () => { @@ -150,6 +151,7 @@ describe("hosted MCP transport", () => { expect(listBody).toContain("caps_list"); expect(listBody).toContain("caps_get"); expect(listBody).toContain("caps_context"); + expect(listBody).toContain('"openWorldHint":false'); expect(listBody).not.toContain("caps_delete"); const called = await POST( request({ diff --git a/apps/web/app/api/mcp/route.ts b/apps/web/app/api/mcp/route.ts index 32704c367b..e59700e3e9 100644 --- a/apps/web/app/api/mcp/route.ts +++ b/apps/web/app/api/mcp/route.ts @@ -24,6 +24,7 @@ const allowedOrigins = () => "https://chatgpt.com", "https://claude.ai", "https://muse.ai", + "https://grok.com", ]); const corsHeaders = (request: Request) => { diff --git a/apps/web/lib/mcp-auth.ts b/apps/web/lib/mcp-auth.ts index c6c14c680c..d61ea1fdaa 100644 --- a/apps/web/lib/mcp-auth.ts +++ b/apps/web/lib/mcp-auth.ts @@ -37,7 +37,9 @@ export const isMcpRedirectUri = (value: string) => { const url = new URL(value); const loopback = url.protocol === "http:" && - (url.hostname === "127.0.0.1" || url.hostname === "[::1]") && + (url.hostname === "localhost" || + url.hostname === "127.0.0.1" || + url.hostname === "[::1]") && url.port.length > 0; return ( (url.protocol === "https:" || loopback) && diff --git a/apps/web/lib/mcp-server.ts b/apps/web/lib/mcp-server.ts index 9725b3d55e..16548cf1a2 100644 --- a/apps/web/lib/mcp-server.ts +++ b/apps/web/lib/mcp-server.ts @@ -17,6 +17,7 @@ const readOnly = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, + openWorldHint: false, }; const result = (value: Record) => ({ From 6457f0535b8a326d5fde6f96e4f37580c1655310 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Tue, 22 Sep 2026 13:47:55 +0100 Subject: [PATCH 2/2] fix: include directory tool annotation titles --- apps/web/__tests__/unit/mcp-server.test.ts | 25 ++++++++++++++++++++++ apps/web/lib/mcp-server.ts | 6 +++--- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/apps/web/__tests__/unit/mcp-server.test.ts b/apps/web/__tests__/unit/mcp-server.test.ts index ad91c88428..d396c2c4dc 100644 --- a/apps/web/__tests__/unit/mcp-server.test.ts +++ b/apps/web/__tests__/unit/mcp-server.test.ts @@ -152,6 +152,31 @@ describe("hosted MCP transport", () => { expect(listBody).toContain("caps_get"); expect(listBody).toContain("caps_context"); expect(listBody).toContain('"openWorldHint":false'); + const data = listBody.split("\n").find((line) => line.startsWith("data: ")); + expect(data).toBeDefined(); + const tools = JSON.parse(data?.slice(6) ?? "").result.tools; + expect(tools).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + name: "caps_list", + annotations: expect.objectContaining({ + title: "List Cap recordings", + }), + }), + expect.objectContaining({ + name: "caps_get", + annotations: expect.objectContaining({ + title: "Get a Cap recording", + }), + }), + expect.objectContaining({ + name: "caps_context", + annotations: expect.objectContaining({ + title: "Read a Cap recording", + }), + }), + ]), + ); expect(listBody).not.toContain("caps_delete"); const called = await POST( request({ diff --git a/apps/web/lib/mcp-server.ts b/apps/web/lib/mcp-server.ts index 16548cf1a2..aa96c9fba6 100644 --- a/apps/web/lib/mcp-server.ts +++ b/apps/web/lib/mcp-server.ts @@ -55,7 +55,7 @@ export const createCapMcpServer = (userId: User.UserId) => { }, additionalProperties: false, }), - annotations: readOnly, + annotations: { ...readOnly, title: "List Cap recordings" }, }, async (input) => { try { @@ -78,7 +78,7 @@ export const createCapMcpServer = (userId: User.UserId) => { required: ["id"], additionalProperties: false, }), - annotations: readOnly, + annotations: { ...readOnly, title: "Get a Cap recording" }, }, async ({ id }) => { try { @@ -106,7 +106,7 @@ export const createCapMcpServer = (userId: User.UserId) => { required: ["id"], additionalProperties: false, }), - annotations: readOnly, + annotations: { ...readOnly, title: "Read a Cap recording" }, _meta: { ui: { resourceUri: cardUri }, "openai/outputTemplate": cardUri }, }, async ({ id, query }) => {