diff --git a/app/mcp/route.ts b/app/mcp/route.ts index e7d3719d..e7e95744 100644 --- a/app/mcp/route.ts +++ b/app/mcp/route.ts @@ -1,6 +1,6 @@ import { registerAllTools } from "@/lib/mcp/tools"; import { createMcpHandler, withMcpAuth } from "mcp-handler"; -import { verifyApiKey } from "@/lib/mcp/verifyApiKey"; +import { verifyBearerToken } from "@/lib/mcp/verifyApiKey"; const baseHandler = createMcpHandler( server => { @@ -14,8 +14,8 @@ const baseHandler = createMcpHandler( }, ); -// Wrap with auth - API key is required for all MCP requests -const handler = withMcpAuth(baseHandler, verifyApiKey, { +// Wrap with auth - Privy JWT or API key required for all MCP requests +const handler = withMcpAuth(baseHandler, verifyBearerToken, { required: true, }); diff --git a/lib/mcp/getAccountIdByApiKey.ts b/lib/mcp/getAccountIdByApiKey.ts new file mode 100644 index 00000000..17e1578f --- /dev/null +++ b/lib/mcp/getAccountIdByApiKey.ts @@ -0,0 +1,22 @@ +import { hashApiKey } from "@/lib/keys/hashApiKey"; +import { PRIVY_PROJECT_SECRET } from "@/lib/const"; +import { selectAccountApiKeys } from "@/lib/supabase/account_api_keys/selectAccountApiKeys"; + +/** + * Validates an API key and returns the associated account ID. + * + * @param apiKey - The raw API key to validate. + * @returns The account ID if valid, or null if invalid. + */ +export async function getAccountIdByApiKey( + apiKey: string, +): Promise { + const keyHash = hashApiKey(apiKey, PRIVY_PROJECT_SECRET); + const apiKeys = await selectAccountApiKeys({ keyHash }); + + if (!apiKeys || apiKeys.length === 0) { + return null; + } + + return apiKeys[0]?.account ?? null; +} diff --git a/lib/mcp/verifyApiKey.ts b/lib/mcp/verifyApiKey.ts index 4bcc1b65..8cf3d91e 100644 --- a/lib/mcp/verifyApiKey.ts +++ b/lib/mcp/verifyApiKey.ts @@ -1,5 +1,6 @@ import type { AuthInfo } from "@modelcontextprotocol/sdk/server/auth/types.js"; -import { getApiKeyDetails } from "@/lib/keys/getApiKeyDetails"; +import { getAccountIdByAuthToken } from "@/lib/privy/getAccountIdByAuthToken"; +import { getAccountIdByApiKey } from "@/lib/mcp/getAccountIdByApiKey"; export interface McpAuthInfoExtra extends Record { accountId: string; @@ -11,13 +12,15 @@ export interface McpAuthInfo extends AuthInfo { } /** - * Verifies an API key and returns auth info with account details. + * Verifies a bearer token (Privy JWT or API key) and returns auth info. + * + * Tries Privy JWT validation first, then falls back to API key validation. * * @param _req - The request object (unused). - * @param bearerToken - The API key from the Authorization: Bearer header. - * @returns AuthInfo with accountId and orgId, or undefined if invalid. + * @param bearerToken - The token from Authorization: Bearer header (Privy JWT or API key). + * @returns AuthInfo with accountId, or undefined if invalid. */ -export async function verifyApiKey( +export async function verifyBearerToken( _req: Request, bearerToken?: string, ): Promise { @@ -25,21 +28,41 @@ export async function verifyApiKey( return undefined; } - const apiKey = bearerToken; + // Try Privy JWT first + try { + const accountId = await getAccountIdByAuthToken(bearerToken); + + return { + token: bearerToken, + scopes: ["mcp:tools"], + clientId: accountId, + extra: { + accountId, + orgId: null, + }, + }; + } catch { + // Privy validation failed, try API key + } - const keyDetails = await getApiKeyDetails(apiKey); + // Try API key validation + try { + const accountId = await getAccountIdByApiKey(bearerToken); - if (!keyDetails) { + if (!accountId) { + return undefined; + } + + return { + token: bearerToken, + scopes: ["mcp:tools"], + clientId: accountId, + extra: { + accountId, + orgId: null, + }, + }; + } catch { return undefined; } - - return { - token: apiKey, - scopes: ["mcp:tools"], - clientId: keyDetails.accountId, - extra: { - accountId: keyDetails.accountId, - orgId: keyDetails.orgId, - }, - }; }