Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 31 additions & 5 deletions apps/app/src/app/api/mcp-proxy/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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) {
Expand All @@ -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',
Expand All @@ -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',
},
})
Expand All @@ -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: {
Expand All @@ -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) {
Expand All @@ -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
Expand All @@ -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',
},
})
Expand All @@ -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',
},
})
Expand Down
69 changes: 48 additions & 21 deletions apps/mcp/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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);
Expand Down
Loading