From f22900d4078866997d2e074959c62f12c66fb21f Mon Sep 17 00:00:00 2001 From: Evandro Camargo Date: Thu, 12 Feb 2026 13:38:27 -0300 Subject: [PATCH 1/3] fix(mcp): register handler on bare /mcp path to match MCP transport spec The Hono route pattern /mcp/* requires at least one path segment after /mcp/, causing 404 responses when MCP clients POST to the bare /mcp endpoint. This is the standard behavior for Streamable HTTP transport clients per the MCP specification. Extract the inline handler to a named function and register it on both /mcp (bare path) and /mcp/* (sub-paths) to ensure compatibility with all MCP clients including mcp-cli. --- apps/mcp/src/index.ts | 272 +++++++++++++++++++++--------------------- 1 file changed, 139 insertions(+), 133 deletions(-) diff --git a/apps/mcp/src/index.ts b/apps/mcp/src/index.ts index 0586492f8..7c143ae98 100644 --- a/apps/mcp/src/index.ts +++ b/apps/mcp/src/index.ts @@ -1,22 +1,22 @@ import { cors } from "hono/cors" -import { Hono } from "hono" +import { Hono, type Context } from "hono" import { SupermemoryMCP } from "./server" import { isApiKey, validateApiKey, validateOAuthToken } from "./auth" import { initPosthog } from "./posthog" import type { ContentfulStatusCode } from "hono/utils/http-status" type Bindings = { - MCP_SERVER: DurableObjectNamespace - API_URL?: string - POSTHOG_API_KEY?: string + MCP_SERVER: DurableObjectNamespace + API_URL?: string + POSTHOG_API_KEY?: string } type Props = { - userId: string - apiKey: string - containerTag?: string - email?: string - name?: string + userId: string + apiKey: string + containerTag?: string + email?: string + name?: string } const app = new Hono<{ Bindings: Bindings }>() @@ -25,151 +25,157 @@ const DEFAULT_API_URL = "https://api.supermemory.ai" // CORS app.use( - "*", - cors({ - origin: "*", - allowMethods: ["GET", "POST", "OPTIONS"], - allowHeaders: ["Content-Type", "Authorization", "x-sm-project"], - }), + "*", + cors({ + origin: "*", + allowMethods: ["GET", "POST", "OPTIONS"], + allowHeaders: ["Content-Type", "Authorization", "x-sm-project"], + }), ) app.use("*", async (c, next) => { - initPosthog(c.env.POSTHOG_API_KEY) - await next() + initPosthog(c.env.POSTHOG_API_KEY) + await next() }) app.get("/", (c) => { - return c.json({ - name: "supermemory-mcp", - version: "4.0.0", - description: "Give your AI a memory", - docs: "https://docs.supermemory.ai/mcp", - }) + return c.json({ + name: "supermemory-mcp", + version: "4.0.0", + description: "Give your AI a memory", + docs: "https://docs.supermemory.ai/mcp", + }) }) // MCP clients use this to discover the authorization server app.get("/.well-known/oauth-protected-resource", (c) => { - const apiUrl = c.env.API_URL || DEFAULT_API_URL - const resourceUrl = - c.env.API_URL === "http://localhost:8787" - ? "http://localhost:8788" - : "https://mcp.supermemory.ai" - - return c.json({ - resource: resourceUrl, - authorization_servers: [apiUrl], - scopes_supported: ["openid", "profile", "email", "offline_access"], - bearer_methods_supported: ["header"], - resource_documentation: "https://docs.supermemory.ai/mcp", - }) + const apiUrl = c.env.API_URL || DEFAULT_API_URL + const resourceUrl = + c.env.API_URL === "http://localhost:8787" + ? "http://localhost:8788" + : "https://mcp.supermemory.ai" + + return c.json({ + resource: resourceUrl, + authorization_servers: [apiUrl], + scopes_supported: ["openid", "profile", "email", "offline_access"], + bearer_methods_supported: ["header"], + resource_documentation: "https://docs.supermemory.ai/mcp", + }) }) // Proxy endpoint for MCP clients that don't follow the spec correctly // Some clients look for oauth-authorization-server on the MCP server domain // instead of following the authorization_servers array app.get("/.well-known/oauth-authorization-server", async (c) => { - const apiUrl = c.env.API_URL || DEFAULT_API_URL - - try { - // Fetch the authorization server metadata from the main API - const response = await fetch( - `${apiUrl}/.well-known/oauth-authorization-server`, - ) - - if (!response.ok) { - return c.json( - { error: "Failed to fetch authorization server metadata" }, - { status: response.status as ContentfulStatusCode }, - ) - } - - const metadata = await response.json() - return c.json(metadata) - } catch (error) { - console.error("Error fetching OAuth authorization server metadata:", error) - return c.json({ error: "Internal server error" }, 500) - } + const apiUrl = c.env.API_URL || DEFAULT_API_URL + + try { + // Fetch the authorization server metadata from the main API + const response = await fetch( + `${apiUrl}/.well-known/oauth-authorization-server`, + ) + + if (!response.ok) { + return c.json( + { error: "Failed to fetch authorization server metadata" }, + { status: response.status as ContentfulStatusCode }, + ) + } + + const metadata = await response.json() + return c.json(metadata) + } catch (error) { + console.error("Error fetching OAuth authorization server metadata:", error) + return c.json({ error: "Internal server error" }, 500) + } }) const mcpHandler = SupermemoryMCP.mount("/mcp", { - binding: "MCP_SERVER", - corsOptions: { - origin: "*", - methods: "GET, POST, OPTIONS", - headers: "Content-Type, Authorization, x-sm-project", - }, + binding: "MCP_SERVER", + corsOptions: { + origin: "*", + methods: "GET, POST, OPTIONS", + headers: "Content-Type, Authorization, x-sm-project", + }, }) -app.all("/mcp/*", async (c) => { - const authHeader = c.req.header("Authorization") - const token = authHeader?.replace(/^Bearer\s+/i, "") - const containerTag = c.req.header("x-sm-project") - const apiUrl = c.env.API_URL || DEFAULT_API_URL - - if (!token) { - return new Response("Unauthorized", { - status: 401, - headers: { - "WWW-Authenticate": `Bearer resource_metadata="/.well-known/oauth-protected-resource"`, - "Access-Control-Expose-Headers": "WWW-Authenticate", - }, - }) - } - - let authUser: { - userId: string - apiKey: string - email?: string - name?: string - } | null = null - - if (isApiKey(token)) { - console.log("Authenticating with API key") - authUser = await validateApiKey(token, apiUrl) - } else { - console.log("Authenticating with OAuth token") - authUser = await validateOAuthToken(token, apiUrl) - } - - if (!authUser) { - const errorMessage = isApiKey(token) - ? "Unauthorized: Invalid or expired API key" - : "Unauthorized: Invalid or expired token" - - return new Response( - JSON.stringify({ - jsonrpc: "2.0", - error: { - code: -32000, - message: errorMessage, - }, - id: null, - }), - { - status: 401, - headers: { - "Content-Type": "application/json", - "WWW-Authenticate": `Bearer error="invalid_token", resource_metadata="/.well-known/oauth-protected-resource"`, - "Access-Control-Expose-Headers": "WWW-Authenticate", - }, - }, - ) - } - - // Create execution context with authenticated user props - const ctx = { - ...c.executionCtx, - props: { - userId: authUser.userId, - apiKey: authUser.apiKey, - containerTag, - email: authUser.email, - name: authUser.name, - } satisfies Props, - } as ExecutionContext & { props: Props } - - return mcpHandler.fetch(c.req.raw, c.env, ctx) -}) +// MCP request handler — registered on both bare and wildcard paths +// to comply with MCP Streamable HTTP transport spec, which requires +// a single endpoint that accepts POST, GET, and DELETE. +const handleMcpRequest = async (c: Context<{ Bindings: Bindings }>) => { + const authHeader = c.req.header("Authorization") + const token = authHeader?.replace(/^Bearer\s+/i, "") + const containerTag = c.req.header("x-sm-project") + const apiUrl = c.env.API_URL || DEFAULT_API_URL + + if (!token) { + return new Response("Unauthorized", { + status: 401, + headers: { + "WWW-Authenticate": `Bearer resource_metadata="/.well-known/oauth-protected-resource"`, + "Access-Control-Expose-Headers": "WWW-Authenticate", + }, + }) + } + + let authUser: { + userId: string + apiKey: string + email?: string + name?: string + } | null = null + + if (isApiKey(token)) { + console.log("Authenticating with API key") + authUser = await validateApiKey(token, apiUrl) + } else { + console.log("Authenticating with OAuth token") + authUser = await validateOAuthToken(token, apiUrl) + } + + if (!authUser) { + const errorMessage = isApiKey(token) + ? "Unauthorized: Invalid or expired API key" + : "Unauthorized: Invalid or expired token" + + return new Response( + JSON.stringify({ + jsonrpc: "2.0", + error: { + code: -32000, + message: errorMessage, + }, + id: null, + }), + { + status: 401, + headers: { + "Content-Type": "application/json", + "WWW-Authenticate": `Bearer error="invalid_token", resource_metadata="/.well-known/oauth-protected-resource"`, + "Access-Control-Expose-Headers": "WWW-Authenticate", + }, + }, + ) + } + + // Create execution context with authenticated user props + const ctx = { + ...c.executionCtx, + props: { + userId: authUser.userId, + apiKey: authUser.apiKey, + containerTag, + email: authUser.email, + name: authUser.name, + } satisfies Props, + } as ExecutionContext & { props: Props } + + return mcpHandler.fetch(c.req.raw, c.env, ctx) +} + +app.all("/mcp", handleMcpRequest) +app.all("/mcp/*", handleMcpRequest) // Export the Durable Object class for Cloudflare Workers export { SupermemoryMCP } From 1b6d5bc17aab7240c151e4edf66f1178e36ba966 Mon Sep 17 00:00:00 2001 From: Evandro Camargo Date: Thu, 12 Feb 2026 18:46:30 -0300 Subject: [PATCH 2/3] biome format --write --- apps/mcp/src/index.ts | 260 +++++++++++++++++++++--------------------- 1 file changed, 130 insertions(+), 130 deletions(-) diff --git a/apps/mcp/src/index.ts b/apps/mcp/src/index.ts index 7c143ae98..f0bfa60d0 100644 --- a/apps/mcp/src/index.ts +++ b/apps/mcp/src/index.ts @@ -6,17 +6,17 @@ import { initPosthog } from "./posthog" import type { ContentfulStatusCode } from "hono/utils/http-status" type Bindings = { - MCP_SERVER: DurableObjectNamespace - API_URL?: string - POSTHOG_API_KEY?: string + MCP_SERVER: DurableObjectNamespace + API_URL?: string + POSTHOG_API_KEY?: string } type Props = { - userId: string - apiKey: string - containerTag?: string - email?: string - name?: string + userId: string + apiKey: string + containerTag?: string + email?: string + name?: string } const app = new Hono<{ Bindings: Bindings }>() @@ -25,153 +25,153 @@ const DEFAULT_API_URL = "https://api.supermemory.ai" // CORS app.use( - "*", - cors({ - origin: "*", - allowMethods: ["GET", "POST", "OPTIONS"], - allowHeaders: ["Content-Type", "Authorization", "x-sm-project"], - }), + "*", + cors({ + origin: "*", + allowMethods: ["GET", "POST", "OPTIONS"], + allowHeaders: ["Content-Type", "Authorization", "x-sm-project"], + }), ) app.use("*", async (c, next) => { - initPosthog(c.env.POSTHOG_API_KEY) - await next() + initPosthog(c.env.POSTHOG_API_KEY) + await next() }) app.get("/", (c) => { - return c.json({ - name: "supermemory-mcp", - version: "4.0.0", - description: "Give your AI a memory", - docs: "https://docs.supermemory.ai/mcp", - }) + return c.json({ + name: "supermemory-mcp", + version: "4.0.0", + description: "Give your AI a memory", + docs: "https://docs.supermemory.ai/mcp", + }) }) // MCP clients use this to discover the authorization server app.get("/.well-known/oauth-protected-resource", (c) => { - const apiUrl = c.env.API_URL || DEFAULT_API_URL - const resourceUrl = - c.env.API_URL === "http://localhost:8787" - ? "http://localhost:8788" - : "https://mcp.supermemory.ai" - - return c.json({ - resource: resourceUrl, - authorization_servers: [apiUrl], - scopes_supported: ["openid", "profile", "email", "offline_access"], - bearer_methods_supported: ["header"], - resource_documentation: "https://docs.supermemory.ai/mcp", - }) + const apiUrl = c.env.API_URL || DEFAULT_API_URL + const resourceUrl = + c.env.API_URL === "http://localhost:8787" + ? "http://localhost:8788" + : "https://mcp.supermemory.ai" + + return c.json({ + resource: resourceUrl, + authorization_servers: [apiUrl], + scopes_supported: ["openid", "profile", "email", "offline_access"], + bearer_methods_supported: ["header"], + resource_documentation: "https://docs.supermemory.ai/mcp", + }) }) // Proxy endpoint for MCP clients that don't follow the spec correctly // Some clients look for oauth-authorization-server on the MCP server domain // instead of following the authorization_servers array app.get("/.well-known/oauth-authorization-server", async (c) => { - const apiUrl = c.env.API_URL || DEFAULT_API_URL - - try { - // Fetch the authorization server metadata from the main API - const response = await fetch( - `${apiUrl}/.well-known/oauth-authorization-server`, - ) - - if (!response.ok) { - return c.json( - { error: "Failed to fetch authorization server metadata" }, - { status: response.status as ContentfulStatusCode }, - ) - } - - const metadata = await response.json() - return c.json(metadata) - } catch (error) { - console.error("Error fetching OAuth authorization server metadata:", error) - return c.json({ error: "Internal server error" }, 500) - } + const apiUrl = c.env.API_URL || DEFAULT_API_URL + + try { + // Fetch the authorization server metadata from the main API + const response = await fetch( + `${apiUrl}/.well-known/oauth-authorization-server`, + ) + + if (!response.ok) { + return c.json( + { error: "Failed to fetch authorization server metadata" }, + { status: response.status as ContentfulStatusCode }, + ) + } + + const metadata = await response.json() + return c.json(metadata) + } catch (error) { + console.error("Error fetching OAuth authorization server metadata:", error) + return c.json({ error: "Internal server error" }, 500) + } }) const mcpHandler = SupermemoryMCP.mount("/mcp", { - binding: "MCP_SERVER", - corsOptions: { - origin: "*", - methods: "GET, POST, OPTIONS", - headers: "Content-Type, Authorization, x-sm-project", - }, + binding: "MCP_SERVER", + corsOptions: { + origin: "*", + methods: "GET, POST, OPTIONS", + headers: "Content-Type, Authorization, x-sm-project", + }, }) // MCP request handler — registered on both bare and wildcard paths // to comply with MCP Streamable HTTP transport spec, which requires // a single endpoint that accepts POST, GET, and DELETE. const handleMcpRequest = async (c: Context<{ Bindings: Bindings }>) => { - const authHeader = c.req.header("Authorization") - const token = authHeader?.replace(/^Bearer\s+/i, "") - const containerTag = c.req.header("x-sm-project") - const apiUrl = c.env.API_URL || DEFAULT_API_URL - - if (!token) { - return new Response("Unauthorized", { - status: 401, - headers: { - "WWW-Authenticate": `Bearer resource_metadata="/.well-known/oauth-protected-resource"`, - "Access-Control-Expose-Headers": "WWW-Authenticate", - }, - }) - } - - let authUser: { - userId: string - apiKey: string - email?: string - name?: string - } | null = null - - if (isApiKey(token)) { - console.log("Authenticating with API key") - authUser = await validateApiKey(token, apiUrl) - } else { - console.log("Authenticating with OAuth token") - authUser = await validateOAuthToken(token, apiUrl) - } - - if (!authUser) { - const errorMessage = isApiKey(token) - ? "Unauthorized: Invalid or expired API key" - : "Unauthorized: Invalid or expired token" - - return new Response( - JSON.stringify({ - jsonrpc: "2.0", - error: { - code: -32000, - message: errorMessage, - }, - id: null, - }), - { - status: 401, - headers: { - "Content-Type": "application/json", - "WWW-Authenticate": `Bearer error="invalid_token", resource_metadata="/.well-known/oauth-protected-resource"`, - "Access-Control-Expose-Headers": "WWW-Authenticate", - }, - }, - ) - } - - // Create execution context with authenticated user props - const ctx = { - ...c.executionCtx, - props: { - userId: authUser.userId, - apiKey: authUser.apiKey, - containerTag, - email: authUser.email, - name: authUser.name, - } satisfies Props, - } as ExecutionContext & { props: Props } - - return mcpHandler.fetch(c.req.raw, c.env, ctx) + const authHeader = c.req.header("Authorization") + const token = authHeader?.replace(/^Bearer\s+/i, "") + const containerTag = c.req.header("x-sm-project") + const apiUrl = c.env.API_URL || DEFAULT_API_URL + + if (!token) { + return new Response("Unauthorized", { + status: 401, + headers: { + "WWW-Authenticate": `Bearer resource_metadata="/.well-known/oauth-protected-resource"`, + "Access-Control-Expose-Headers": "WWW-Authenticate", + }, + }) + } + + let authUser: { + userId: string + apiKey: string + email?: string + name?: string + } | null = null + + if (isApiKey(token)) { + console.log("Authenticating with API key") + authUser = await validateApiKey(token, apiUrl) + } else { + console.log("Authenticating with OAuth token") + authUser = await validateOAuthToken(token, apiUrl) + } + + if (!authUser) { + const errorMessage = isApiKey(token) + ? "Unauthorized: Invalid or expired API key" + : "Unauthorized: Invalid or expired token" + + return new Response( + JSON.stringify({ + jsonrpc: "2.0", + error: { + code: -32000, + message: errorMessage, + }, + id: null, + }), + { + status: 401, + headers: { + "Content-Type": "application/json", + "WWW-Authenticate": `Bearer error="invalid_token", resource_metadata="/.well-known/oauth-protected-resource"`, + "Access-Control-Expose-Headers": "WWW-Authenticate", + }, + }, + ) + } + + // Create execution context with authenticated user props + const ctx = { + ...c.executionCtx, + props: { + userId: authUser.userId, + apiKey: authUser.apiKey, + containerTag, + email: authUser.email, + name: authUser.name, + } satisfies Props, + } as ExecutionContext & { props: Props } + + return mcpHandler.fetch(c.req.raw, c.env, ctx) } app.all("/mcp", handleMcpRequest) From 93911d683850b18ac44f5abe61654ab656bf5fc3 Mon Sep 17 00:00:00 2001 From: Evandro Camargo Date: Mon, 16 Feb 2026 22:17:17 -0300 Subject: [PATCH 3/3] remove extra comments --- apps/mcp/src/index.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/apps/mcp/src/index.ts b/apps/mcp/src/index.ts index f0bfa60d0..e717b31f6 100644 --- a/apps/mcp/src/index.ts +++ b/apps/mcp/src/index.ts @@ -100,9 +100,6 @@ const mcpHandler = SupermemoryMCP.mount("/mcp", { }, }) -// MCP request handler — registered on both bare and wildcard paths -// to comply with MCP Streamable HTTP transport spec, which requires -// a single endpoint that accepts POST, GET, and DELETE. const handleMcpRequest = async (c: Context<{ Bindings: Bindings }>) => { const authHeader = c.req.header("Authorization") const token = authHeader?.replace(/^Bearer\s+/i, "")