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.
This commit is contained in:
Evandro Camargo 2026-02-12 13:38:27 -03:00
parent b439e7ea01
commit f22900d407
No known key found for this signature in database

View file

@ -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"
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",
})
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
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`,
)
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 },
)
}
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 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
// 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",
},
})
}
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
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 (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"
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",
},
},
)
}
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 }
// 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)
})
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 }