Skip to content
Merged
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
6 changes: 3 additions & 3 deletions app/mcp/route.ts
Original file line number Diff line number Diff line change
@@ -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 => {
Expand All @@ -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,
});

Expand Down
22 changes: 22 additions & 0 deletions lib/mcp/getAccountIdByApiKey.ts
Original file line number Diff line number Diff line change
@@ -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<string | null> {
const keyHash = hashApiKey(apiKey, PRIVY_PROJECT_SECRET);
const apiKeys = await selectAccountApiKeys({ keyHash });

if (!apiKeys || apiKeys.length === 0) {
return null;
}

return apiKeys[0]?.account ?? null;
}
59 changes: 41 additions & 18 deletions lib/mcp/verifyApiKey.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown> {
accountId: string;
Expand All @@ -11,35 +12,57 @@ 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<McpAuthInfo | undefined> {
if (!bearerToken) {
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,
},
};
}