Merge pull request #1741 from useautumn/charlie/mcp

chore: proxy mcp from main server
This commit is contained in:
Charlie Lamb
2026-05-28 17:32:00 +01:00
committed by GitHub
5 changed files with 110 additions and 10 deletions

View File

@@ -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"]

View File

@@ -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;
}

View File

@@ -23,6 +23,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";
@@ -89,6 +90,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(
"*",

View File

@@ -0,0 +1,71 @@
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<HonoEnv>) => {
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-autumn-forwarded-host", forwardedHost);
headers.set("x-autumn-forwarded-proto", forwardedProto);
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<HonoEnv>();
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);

View File

@@ -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[],