Merge pull request #1741 from useautumn/charlie/mcp
chore: proxy mcp from main server
This commit is contained in:
20
apps/mcp-server/Dockerfile
Normal file
20
apps/mcp-server/Dockerfile
Normal 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"]
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
"*",
|
||||
|
||||
71
server/src/routers/mcpProxyRouter.ts
Normal file
71
server/src/routers/mcpProxyRouter.ts
Normal 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);
|
||||
@@ -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[],
|
||||
|
||||
Reference in New Issue
Block a user