From 86ebeaa64a08594e8cbb965d793ea4b59c68ef32 Mon Sep 17 00:00:00 2001 From: Charlie Lamb Date: Thu, 28 May 2026 17:25:44 +0100 Subject: [PATCH 1/2] chore: proxy mcp from main server --- apps/mcp-server/Dockerfile | 20 ++++++++ server/src/initHono.ts | 3 ++ server/src/routers/mcpProxyRouter.ts | 69 ++++++++++++++++++++++++++++ server/src/utils/auth.ts | 16 +++---- 4 files changed, 100 insertions(+), 8 deletions(-) create mode 100644 apps/mcp-server/Dockerfile create mode 100644 server/src/routers/mcpProxyRouter.ts diff --git a/apps/mcp-server/Dockerfile b/apps/mcp-server/Dockerfile new file mode 100644 index 000000000..639e3abe9 --- /dev/null +++ b/apps/mcp-server/Dockerfile @@ -0,0 +1,20 @@ +FROM oven/bun:1.3.10 AS pruner +WORKDIR /app + +COPY . . +RUN bunx turbo@2.9.14 prune @autumn/mcp-server --docker + +FROM oven/bun:1.3.10 +WORKDIR /app + +COPY --from=pruner /app/out/json/ . +RUN mkdir -p scripts && touch scripts/preload-env.ts +RUN bun -e 'const fs = require("fs"); const pkg = JSON.parse(fs.readFileSync("package.json", "utf8")); pkg.workspaces.packages = ["shared", "apps/mcp-server", "packages/ksuid", "packages/mcp"]; delete pkg.dependencies; delete pkg.devDependencies; delete pkg.scripts; fs.writeFileSync("package.json", JSON.stringify(pkg, null, 2));' +RUN rm bun.lock && bun install --production --ignore-scripts + +COPY --from=pruner /app/out/full/ . + +ENV NODE_ENV=production +EXPOSE 8080 + +CMD ["bun", "-F", "@autumn/mcp-server", "start"] diff --git a/server/src/initHono.ts b/server/src/initHono.ts index b445f1591..20757e89d 100644 --- a/server/src/initHono.ts +++ b/server/src/initHono.ts @@ -22,6 +22,7 @@ import { cliRouter } from "./internal/dev/cli/cliRouter.js"; import { handleOAuthCallback } from "./internal/orgs/handlers/stripeHandlers/handleOAuthCallback.js"; import { apiRouter } from "./routers/apiRouter.js"; import { internalRouter } from "./routers/internalRouter.js"; +import { mcpProxyRouter } from "./routers/mcpProxyRouter.js"; import { publicRouter } from "./routers/publicRouter.js"; import { auth } from "./utils/auth.js"; import { isAllowedOrigin } from "./utils/corsOrigins.js"; @@ -88,6 +89,8 @@ export const createHonoApp = () => { app.get("/ready/:token", handleReadyCheck); app.get("/", handleHealthCheck); + app.route("", mcpProxyRouter); + // Step 1: OTel HTTP span + base middleware + span enrichment app.use( "*", diff --git a/server/src/routers/mcpProxyRouter.ts b/server/src/routers/mcpProxyRouter.ts new file mode 100644 index 000000000..c2bcd6959 --- /dev/null +++ b/server/src/routers/mcpProxyRouter.ts @@ -0,0 +1,69 @@ +import { Hono } from "hono"; +import type { Context } from "hono"; +import type { HonoEnv } from "../honoUtils/HonoEnv.js"; + +const hopByHopHeaders = [ + "connection", + "keep-alive", + "proxy-authenticate", + "proxy-authorization", + "te", + "trailer", + "transfer-encoding", + "upgrade", +]; + +const getMcpUpstream = () => { + const upstream = process.env.MCP_UPSTREAM_URL; + if (!upstream) return null; + + try { + return new URL(upstream); + } catch { + return null; + } +}; + +const proxyMcp = async (c: Context) => { + const upstream = getMcpUpstream(); + if (!upstream) { + return c.json({ error: "MCP upstream not configured" }, 503); + } + + const incomingUrl = new URL(c.req.url); + const targetUrl = new URL(incomingUrl.pathname + incomingUrl.search, upstream); + const headers = new Headers(c.req.raw.headers); + const forwardedHost = + headers.get("x-forwarded-host") ?? headers.get("host") ?? incomingUrl.host; + const forwardedProto = + headers.get("x-forwarded-proto") ?? incomingUrl.protocol.replace(":", ""); + + for (const header of hopByHopHeaders) headers.delete(header); + + headers.delete("host"); + headers.set("x-forwarded-host", forwardedHost); + headers.set("x-forwarded-proto", forwardedProto); + + const hasBody = c.req.method !== "GET" && c.req.method !== "HEAD"; + const response = await fetch(targetUrl, { + method: c.req.method, + headers, + body: hasBody ? c.req.raw.body : undefined, + duplex: hasBody ? "half" : undefined, + } as RequestInit & { duplex?: "half" }); + + return new Response(response.body, { + status: response.status, + statusText: response.statusText, + headers: response.headers, + }); +}; + +export const mcpProxyRouter = new Hono(); + +mcpProxyRouter.all("/mcp", proxyMcp); +mcpProxyRouter.all("/mcp/*", proxyMcp); +mcpProxyRouter.all("/internal/mcp", proxyMcp); +mcpProxyRouter.all("/internal/mcp/*", proxyMcp); +mcpProxyRouter.all("/.well-known/oauth-protected-resource/mcp", proxyMcp); +mcpProxyRouter.all("/.well-known/oauth-protected-resource/internal/mcp", proxyMcp); diff --git a/server/src/utils/auth.ts b/server/src/utils/auth.ts index 2227be700..b878e08fc 100644 --- a/server/src/utils/auth.ts +++ b/server/src/utils/auth.ts @@ -65,12 +65,13 @@ const emulateGoogleUrl = // OAuth flow leaves and returns via a third-party host (emulate.dev), so the // state cookie must be SameSite=None+Secure to survive the round trip. const isHttpsBaseUrl = process.env.BETTER_AUTH_URL?.startsWith("https://"); -const defaultMcpResourceUrl = process.env.BETTER_AUTH_URL - ? new URL("/mcp", process.env.BETTER_AUTH_URL).href - : null; -const defaultInternalMcpResourceUrl = process.env.BETTER_AUTH_URL - ? new URL("/internal/mcp", process.env.BETTER_AUTH_URL).href - : null; +const hostedMcpResourceUrls = + process.env.MCP_UPSTREAM_URL && process.env.BETTER_AUTH_URL + ? [ + new URL("/mcp", process.env.BETTER_AUTH_URL).href, + new URL("/internal/mcp", process.env.BETTER_AUTH_URL).href, + ] + : []; const parseMcpResourceUrl = (rawUrl: string) => { const resourceUrl = rawUrl.trim(); if (!resourceUrl) return null; @@ -256,8 +257,7 @@ const options = { scopes: [...ALL_SCOPES], validAudiences: [ process.env.BETTER_AUTH_URL, - defaultMcpResourceUrl, - defaultInternalMcpResourceUrl, + ...hostedMcpResourceUrls, ...mcpResourceUrls, ...internalMcpResourceUrls, ].filter(Boolean) as string[], From 999f9dcbe8e2ef0265aee6440353ff9223cac6e4 Mon Sep 17 00:00:00 2001 From: Charlie Lamb Date: Thu, 28 May 2026 17:31:12 +0100 Subject: [PATCH 2/2] chore: add proxy headers --- packages/mcp/src/mcp-server/oauth.ts | 10 ++++++++-- server/src/routers/mcpProxyRouter.ts | 2 ++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/mcp/src/mcp-server/oauth.ts b/packages/mcp/src/mcp-server/oauth.ts index 29ca4c144..245176804 100644 --- a/packages/mcp/src/mcp-server/oauth.ts +++ b/packages/mcp/src/mcp-server/oauth.ts @@ -69,12 +69,18 @@ export function getResourceUrl( _flags: MCPOAuthFlags, resourcePath = "/mcp", ): string { - const host = headers.get("x-forwarded-host") ?? headers.get("host"); + const host = + headers.get("x-autumn-forwarded-host") ?? + headers.get("x-forwarded-host") ?? + headers.get("host"); if (!host) { throw new OAuthHttpError(400, "Missing Host header", "invalid_request"); } - const proto = headers.get("x-forwarded-proto") ?? "http"; + const proto = + headers.get("x-autumn-forwarded-proto") ?? + headers.get("x-forwarded-proto") ?? + "http"; return new URL(resourcePath, `${proto}://${host}`).href; } diff --git a/server/src/routers/mcpProxyRouter.ts b/server/src/routers/mcpProxyRouter.ts index c2bcd6959..bffcecc49 100644 --- a/server/src/routers/mcpProxyRouter.ts +++ b/server/src/routers/mcpProxyRouter.ts @@ -41,6 +41,8 @@ const proxyMcp = async (c: Context) => { for (const header of hopByHopHeaders) headers.delete(header); headers.delete("host"); + headers.set("x-autumn-forwarded-host", forwardedHost); + headers.set("x-autumn-forwarded-proto", forwardedProto); headers.set("x-forwarded-host", forwardedHost); headers.set("x-forwarded-proto", forwardedProto);