From ee7951f6ba181b6af516f04486f0018cf1757271 Mon Sep 17 00:00:00 2001 From: MaheshtheDev <38828053+MaheshtheDev@users.noreply.github.com> Date: Fri, 15 May 2026 23:56:25 +0000 Subject: [PATCH] fix(mcp): align oauth protected-resource metadata with MCP 2025-06-18 spec (#945) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes MCP OAuth discovery so the client-server handshake actually validates against the **MCP 2025-06-18 authorization spec** (which adopts RFC 9728 Protected Resource Metadata + RFC 8707 Resource Indicators). Previously, a client connecting to `https://mcp.dev.supermemory.ai/mcp` would receive `resource: "https://mcp.supermemory.ai"` (bare host, prod fallback) and reject the connection: > Protected resource https://mcp.supermemory.ai does not match expected https://mcp.dev.supermemory.ai/mcp (or origin) ## Changes - **`resource` now includes the `/mcp` endpoint path** — the spec wants the canonical MCP server URI, and the bundled `@modelcontextprotocol/sdk` reference implementation emits the same shape (`new URL(rsPath, base).href`). Bare-host worked with lenient clients that fell back to origin-matching; strict clients rejected it. - **Path-suffixed metadata route** added at `/.well-known/oauth-protected-resource/mcp` alongside the bare path. The SDK's `metadataHandler` mounts under the resource path, so this matches what spec-strict clients probe first. - **`WWW-Authenticate`'s `resource_metadata` URL** points to the canonical full URL (`https://host/.well-known/oauth-protected-resource/mcp`). - **Centralized base-URL derivation** in a new `mcpBaseUrl()` helper, with priority: 1. `MCP_URL` env var — set by portless dev script so dev requests resolve to the tunneled host, not whatever the local proxy sticks in `Host` 2. `x-forwarded-host` / `host` request headers 3. `https://mcp.supermemory.ai` last-resort fallback (only hit when the worker can't see the inbound host at all) ## Production impact `MCP_URL` is dev-only (not in `wrangler.jsonc` vars), so prod falls through to the `Host` header → `https://mcp.supermemory.ai/mcp`. The wire change in prod is that `resource` now ends with `/mcp` instead of being bare — spec-correct, what strict clients require, and tolerated by lenient ones. ## Contributor DX Added `apps/mcp/.dev.vars.example` documenting `API_URL`, `MCP_URL`, and `POSTHOG_API_KEY` for contributors running plain `wrangler dev` without portless. ## Test plan - [x] `curl https://mcp.dev.supermemory.ai/.well-known/oauth-protected-resource` returns `resource: https://mcp.dev.supermemory.ai/mcp` - [x] `curl https://mcp.dev.supermemory.ai/.well-known/oauth-protected-resource/mcp` returns the same payload - [x] 401 from `/mcp` carries `WWW-Authenticate: Bearer resource_metadata="…/oauth-protected-resource/mcp"` - [x] MCP client (vscode extension) connects successfully — previously failed with the resource-mismatch error - [ ] Verify in prod that bare-host clients continue to work after deploy --- apps/mcp/.dev.vars.example | 8 ++++++++ apps/mcp/src/index.ts | 28 +++++++++++++++------------- 2 files changed, 23 insertions(+), 13 deletions(-) create mode 100644 apps/mcp/.dev.vars.example diff --git a/apps/mcp/.dev.vars.example b/apps/mcp/.dev.vars.example new file mode 100644 index 00000000..dfc969e5 --- /dev/null +++ b/apps/mcp/.dev.vars.example @@ -0,0 +1,8 @@ +# Copy to .dev.vars for `wrangler dev`. Optional when running via `bun run dev` +# (portless) — that injects API_URL and MCP_URL automatically. + +API_URL= +MCP_URL= + +#(optional) +POSTHOG_API_KEY= \ No newline at end of file diff --git a/apps/mcp/src/index.ts b/apps/mcp/src/index.ts index 94a0bc74..846064e1 100644 --- a/apps/mcp/src/index.ts +++ b/apps/mcp/src/index.ts @@ -8,6 +8,7 @@ import type { ContentfulStatusCode } from "hono/utils/http-status" type Bindings = { MCP_SERVER: DurableObjectNamespace API_URL?: string + MCP_URL?: string POSTHOG_API_KEY?: string } @@ -22,6 +23,14 @@ type Props = { const app = new Hono<{ Bindings: Bindings }>() const DEFAULT_API_URL = "https://api.supermemory.ai" +const DEFAULT_MCP_URL = "https://mcp.supermemory.ai" + +const mcpBaseUrl = (c: Context<{ Bindings: Bindings }>) => { + if (c.env.MCP_URL) return c.env.MCP_URL.replace(/\/$/, "") + const host = c.req.header("x-forwarded-host") || c.req.header("host") + const proto = c.req.header("x-forwarded-proto") || "https" + return host ? `${proto}://${host}` : DEFAULT_MCP_URL +} // CORS app.use( @@ -57,21 +66,18 @@ app.get("/", (c) => { }) // MCP clients use this to discover the authorization server -app.get("/.well-known/oauth-protected-resource", (c) => { +const protectedResourceHandler = (c: Context<{ Bindings: Bindings }>) => { const apiUrl = c.env.API_URL || DEFAULT_API_URL - - const host = c.req.header("x-forwarded-host") || c.req.header("host") - const proto = c.req.header("x-forwarded-proto") || "https" - const resourceUrl = host ? `${proto}://${host}` : "https://mcp.supermemory.ai" - return c.json({ - resource: resourceUrl, + resource: `${mcpBaseUrl(c)}/mcp`, authorization_servers: [apiUrl], scopes_supported: ["openid", "profile", "email", "offline_access"], bearer_methods_supported: ["header"], resource_documentation: "https://docs.supermemory.ai/mcp", }) -}) +} +app.get("/.well-known/oauth-protected-resource", protectedResourceHandler) +app.get("/.well-known/oauth-protected-resource/mcp", protectedResourceHandler) // Proxy endpoint for MCP clients that don't follow the spec correctly // Some clients look for oauth-authorization-server on the MCP server domain @@ -116,11 +122,7 @@ const handleMcpRequest = async (c: Context<{ Bindings: Bindings }>) => { const containerTag = c.req.header("x-sm-project") const apiUrl = c.env.API_URL || DEFAULT_API_URL - const reqHost = c.req.header("x-forwarded-host") || c.req.header("host") || "" - const reqProto = c.req.header("x-forwarded-proto") || "https" - const resourceMetadataUrl = reqHost - ? `${reqProto}://${reqHost}/.well-known/oauth-protected-resource` - : "/.well-known/oauth-protected-resource" + const resourceMetadataUrl = `${mcpBaseUrl(c)}/.well-known/oauth-protected-resource/mcp` if (!token) { return new Response("Unauthorized", {