diff --git a/apps/app/src/app/api/mcp-proxy/route.ts b/apps/app/src/app/api/mcp-proxy/route.ts index 65b0b2a4..2b6975ce 100644 --- a/apps/app/src/app/api/mcp-proxy/route.ts +++ b/apps/app/src/app/api/mcp-proxy/route.ts @@ -44,6 +44,10 @@ export async function POST(request: Request) { const h = await headers() const url = new URL(request.url) const targetUrl = url.searchParams.get('target-url') + // Optional header hints from client/CLI + const authMode = (h.get('x-mcp-auth-mode') || '').toLowerCase() // 'api-key' | 'mcp-auth' | 'none' + const autoPay = (h.get('x-mcp-auto-pay') || '').toLowerCase() + const errorMode = (h.get('x-mcp-error-mode') || '').toLowerCase() const session = await serverAuth.getSession({ fetchOptions: { @@ -55,7 +59,13 @@ export async function POST(request: Request) { }) if (!session.data) { - return new Response("Unauthorized", { status: 401 }) + // If the caller requested x420 behavior (browser wallets), surface 420 + const wantsX420 = errorMode === 'x420' + const payload = wantsX420 ? { error: 'UNAUTHORIZED' } : 'Unauthorized' + return new Response(typeof payload === 'string' ? payload : JSON.stringify(payload), { + status: wantsX420 ? 420 : 401, + headers: { 'Content-Type': 'application/json' } + }) } if (!targetUrl) { @@ -82,6 +92,10 @@ export async function POST(request: Request) { 'X-Wallet-Type': h.get('x-wallet-type') || '', 'X-Wallet-Address': h.get('x-wallet-address') || '', 'X-Wallet-Provider': h.get('x-wallet-provider') || '', + // Forward client routing/autopay preferences to MCP server + 'X-MCP-Auth-Mode': authMode, + 'X-MCP-Auto-Pay': autoPay, + 'X-MCP-Error-Mode': errorMode, }, body: request.body, credentials: 'include', @@ -98,7 +112,7 @@ export async function POST(request: Request) { 'Content-Type': response.headers.get('Content-Type') || 'application/json', 'Access-Control-Allow-Origin': validOrigin || '*', 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', - 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Wallet-Type, X-Wallet-Address, X-Wallet-Provider', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Wallet-Type, X-Wallet-Address, X-Wallet-Provider, X-MCP-Auth-Mode, X-MCP-Auto-Pay, X-MCP-Error-Mode', 'Access-Control-Allow-Credentials': validOrigin ? 'true' : 'false', }, }) @@ -108,6 +122,10 @@ export async function GET(request: Request) { const h = await headers() const url = new URL(request.url) const targetUrl = url.searchParams.get('target-url') + // Optional header hints from client/CLI + const authMode = (h.get('x-mcp-auth-mode') || '').toLowerCase() + const autoPay = (h.get('x-mcp-auto-pay') || '').toLowerCase() + const errorMode = (h.get('x-mcp-error-mode') || '').toLowerCase() const session = await serverAuth.getSession({ fetchOptions: { @@ -119,7 +137,12 @@ export async function GET(request: Request) { }) if (!session.data) { - return new Response("Unauthorized", { status: 401 }) + const wantsX420 = errorMode === 'x420' + const payload = wantsX420 ? { error: 'UNAUTHORIZED' } : 'Unauthorized' + return new Response(typeof payload === 'string' ? payload : JSON.stringify(payload), { + status: wantsX420 ? 420 : 401, + headers: { 'Content-Type': 'application/json' } + }) } if (!targetUrl) { @@ -139,6 +162,9 @@ export async function GET(request: Request) { 'X-Wallet-Type': h.get('x-wallet-type') || '', 'X-Wallet-Address': h.get('x-wallet-address') || '', 'X-Wallet-Provider': h.get('x-wallet-provider') || '', + 'X-MCP-Auth-Mode': authMode, + 'X-MCP-Auto-Pay': autoPay, + 'X-MCP-Error-Mode': errorMode, }, credentials: 'include', // @ts-expect-error this is valid and needed @@ -154,7 +180,7 @@ export async function GET(request: Request) { 'Content-Type': response.headers.get('Content-Type') || 'application/json', 'Access-Control-Allow-Origin': validOrigin || '*', 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', - 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Wallet-Type, X-Wallet-Address, X-Wallet-Provider', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Wallet-Type, X-Wallet-Address, X-Wallet-Provider, X-MCP-Auth-Mode, X-MCP-Auto-Pay, X-MCP-Error-Mode', 'Access-Control-Allow-Credentials': validOrigin ? 'true' : 'false', }, }) @@ -170,7 +196,7 @@ export async function OPTIONS(request: Request) { 'Content-Type': 'application/json, text/event-stream', 'Access-Control-Allow-Origin': validOrigin || '*', 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', - 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Wallet-Type, X-Wallet-Address, X-Wallet-Provider', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Wallet-Type, X-Wallet-Address, X-Wallet-Provider, X-MCP-Auth-Mode, X-MCP-Auto-Pay, X-MCP-Error-Mode', 'Access-Control-Allow-Credentials': validOrigin ? 'true' : 'false', }, }) diff --git a/apps/mcp/src/index.ts b/apps/mcp/src/index.ts index 1345d17f..80ca3781 100644 --- a/apps/mcp/src/index.ts +++ b/apps/mcp/src/index.ts @@ -48,6 +48,10 @@ app.use("*", cors({ "Authorization", "WWW-Authenticate", "x-api-key", + // Client-controlled routing of auth and autopay behavior + "X-MCP-Auth-Mode", // "api-key" | "mcp-auth" | "none" + "X-MCP-Auto-Pay", // "on" | "off" + "X-MCP-Error-Mode", // e.g. "x420" (used by app proxy) "X-Wallet-Type", "X-Wallet-Address", "X-Wallet-Provider" @@ -474,34 +478,57 @@ app.all("/mcp", async (c) => { return new Response("target-url missing", { status: 400 }); } - const withMcpProxy = (session: any) => withProxy(targetUrl, [ - new AnalyticsHook(analyticsSink, targetUrl), - new LoggingHook(), - new X402WalletHook(session), - new SecurityHook(), - ]); + // Determine routing mode and autopay behavior from headers + const inbound = new Headers(original.headers); + const authMode = (inbound.get("x-mcp-auth-mode") || "").toLowerCase(); + const autoPay = (inbound.get("x-mcp-auto-pay") || "on").toLowerCase(); // default on for CLI + const errorMode = (inbound.get("x-mcp-error-mode") || "").toLowerCase(); + + // Build proxy hooks conditionally (X402WalletHook toggled by autoPay) + const buildHooks = (sessionLike: any) => { + const hooks = [ + new AnalyticsHook(analyticsSink, targetUrl), + new LoggingHook(), + new SecurityHook(), + ] as any[]; + if (autoPay !== "off") { + hooks.splice(2, 0, new X402WalletHook(sessionLike)); + } + return hooks; + }; + const withMcpProxy = (session: any) => withProxy(targetUrl, buildHooks(session)); - // Extract API key from various sources + // Extract API key from sources const apiKeyFromQuery = currentUrl.searchParams.get("apiKey") || currentUrl.searchParams.get("api_key"); - // const authHeader = original.headers.get("authorization"); - // const apiKeyFromHeader = authHeader?.startsWith("Bearer ") ? authHeader.substring(7) : null; const apiKeyFromXHeader = original.headers.get("x-api-key"); - - const apiKey = apiKeyFromQuery || apiKeyFromXHeader; //|| apiKeyFromHeader - - let session = null; - if (apiKey) { - session = await auth.api.getSession({ - headers: new Headers({ - 'x-api-key': apiKey, - }), - }); + const apiKey = apiKeyFromQuery || apiKeyFromXHeader; + + // Header-based mode selection + // 1) api-key: prefer header key, ignore cookie/session + if (authMode === "api-key") { + if (!apiKey) { + // Explicit api-key mode but no key provided + const wantsX420 = errorMode === "x420"; + const body = wantsX420 ? { error: "API_KEY_REQUIRED" } : { error: "Unauthorized" }; + return new Response(JSON.stringify(body), { status: wantsX420 ? 420 : 401, headers: { 'content-type': 'application/json' } }); + } + const session = await auth.api.getSession({ headers: new Headers({ 'x-api-key': apiKey }) }); + if (!session) { + const wantsX420 = errorMode === "x420"; + const body = wantsX420 ? { error: "INVALID_API_KEY" } : { error: "Unauthorized" }; + return new Response(JSON.stringify(body), { status: wantsX420 ? 420 : 401, headers: { 'content-type': 'application/json' } }); + } + return withMcpProxy(session.session)(original); } - if (!session) { - session = await auth.api.getSession({ headers: original.headers }); + // 2) none: bypass Better Auth; run proxy with anonymous session + if (authMode === "none") { + const anon = { userId: undefined } as any; + return withMcpProxy(anon)(original); } + // 3) default or "mcp-auth": attempt cookie/session; if missing, fall back to OIDC flow via withMcpAuth + let session = await auth.api.getSession({ headers: original.headers }); if (session) { console.log("[MCP] Authenticated session found, proxying with session:", session.session?.userId || session.session); return withMcpProxy(session.session)(original);