diff --git a/apps/mcp/src/auth.ts b/apps/mcp/src/auth.ts index 7bf6bc1b..6e794853 100644 --- a/apps/mcp/src/auth.ts +++ b/apps/mcp/src/auth.ts @@ -4,6 +4,8 @@ * This validates OAuth tokens and API keys by calling the main Supermemory API, */ +import { FETCH_TIMEOUT_MS } from "./constants" + export interface AuthUser { userId: string apiKey: string @@ -11,6 +13,18 @@ export interface AuthUser { name?: string } +export type AuthValidationResult = + | { status: "success"; user: AuthUser } + | { status: "invalid" } + | { status: "timeout" } + +function isFetchTimeout(error: unknown): boolean { + return ( + error instanceof Error && + (error.name === "AbortError" || error.name === "TimeoutError") + ) +} + /** * Check if a token is an API key (starts with "sm_") */ @@ -25,13 +39,14 @@ export function isApiKey(token: string): boolean { export async function validateApiKey( apiKey: string, apiUrl: string, -): Promise { +): Promise { try { const sessionResponse = await fetch(`${apiUrl}/v3/session`, { method: "GET", headers: { Authorization: `Bearer ${apiKey}`, }, + signal: AbortSignal.timeout(FETCH_TIMEOUT_MS), }) if (!sessionResponse.ok) { @@ -56,7 +71,7 @@ export async function validateApiKey( } else { console.error("API key validation failed:", status, responseText) } - return null + return { status: "invalid" } } const sessionData = (await sessionResponse.json()) as { @@ -72,20 +87,27 @@ export async function validateApiKey( if (!sessionData?.user?.id) { console.error("Missing user.id in session response:", sessionData) - return null + return { status: "invalid" } } console.log("API key validated for user:", sessionData.user.id) return { - userId: sessionData.user.id, - apiKey: apiKey, - email: sessionData.user.email, - name: sessionData.user.name, + status: "success", + user: { + userId: sessionData.user.id, + apiKey: apiKey, + email: sessionData.user.email, + name: sessionData.user.name, + }, } } catch (error) { + if (isFetchTimeout(error)) { + console.error("API key validation timed out") + return { status: "timeout" } + } console.error("API key validation error:", error) - return null + return { status: "invalid" } } } @@ -96,14 +118,14 @@ export async function validateApiKey( export async function validateOAuthToken( token: string, apiUrl: string, -): Promise { +): Promise { try { const sessionResponse = await fetch(`${apiUrl}/v3/mcp/session-with-key`, { method: "GET", headers: { Authorization: `Bearer ${token}`, }, - signal: AbortSignal.timeout(30_000), + signal: AbortSignal.timeout(FETCH_TIMEOUT_MS), }) if (!sessionResponse.ok) { @@ -128,7 +150,7 @@ export async function validateOAuthToken( } else { console.error("Token validation failed:", status, responseText) } - return null + return { status: "invalid" } } const sessionData = (await sessionResponse.json()) as { @@ -144,19 +166,26 @@ export async function validateOAuthToken( "Missing userId or apiKey in session response:", sessionData, ) - return null + return { status: "invalid" } } console.log("OAuth validated, got API key for user:", sessionData.userId) return { - userId: sessionData.userId, - apiKey: sessionData.apiKey, - email: sessionData.email, - name: sessionData.name, + status: "success", + user: { + userId: sessionData.userId, + apiKey: sessionData.apiKey, + email: sessionData.email, + name: sessionData.name, + }, } } catch (error) { + if (isFetchTimeout(error)) { + console.error("Token validation timed out") + return { status: "timeout" } + } console.error("Token validation error:", error) - return null + return { status: "invalid" } } } diff --git a/apps/mcp/src/client.ts b/apps/mcp/src/client.ts index 2ce8d41a..ccb08503 100644 --- a/apps/mcp/src/client.ts +++ b/apps/mcp/src/client.ts @@ -1,8 +1,9 @@ -import Supermemory from "supermemory" +import Supermemory, { APIConnectionTimeoutError } from "supermemory" + +import { FETCH_TIMEOUT_MS } from "./constants" const MAX_CHARS = 200000 // ~50k tokens (character-based limit) const DEFAULT_PROJECT_ID = "sm_project_default" -const FETCH_TIMEOUT_MS = 30_000 interface MemoryRichFields { metadata?: Record | null @@ -144,6 +145,7 @@ export class SupermemoryClient { apiKey: bearerToken, baseURL: apiUrl, timeout: FETCH_TIMEOUT_MS, + maxRetries: 0, }) this.containerTag = containerTag || DEFAULT_PROJECT_ID } @@ -394,10 +396,10 @@ export class SupermemoryClient { } private handleError(error: unknown): never { - // Handle request timeout (AbortSignal.timeout or explicit abort) if ( - error instanceof Error && - (error.name === "AbortError" || error.name === "TimeoutError") + error instanceof APIConnectionTimeoutError || + (error instanceof Error && + (error.name === "AbortError" || error.name === "TimeoutError")) ) { throw new Error("Request to Supermemory API timed out") } diff --git a/apps/mcp/src/constants.ts b/apps/mcp/src/constants.ts new file mode 100644 index 00000000..5bd658ef --- /dev/null +++ b/apps/mcp/src/constants.ts @@ -0,0 +1 @@ +export const FETCH_TIMEOUT_MS = 30_000 diff --git a/apps/mcp/src/index.ts b/apps/mcp/src/index.ts index 45731982..13c466f7 100644 --- a/apps/mcp/src/index.ts +++ b/apps/mcp/src/index.ts @@ -2,6 +2,7 @@ import { cors } from "hono/cors" import { Hono, type Context } from "hono" import { SupermemoryMCP } from "./server" import { isApiKey, validateApiKey, validateOAuthToken } from "./auth" +import { FETCH_TIMEOUT_MS } from "./constants" import { initPosthog } from "./posthog" import type { ContentfulStatusCode } from "hono/utils/http-status" @@ -89,7 +90,7 @@ app.get("/.well-known/oauth-authorization-server", async (c) => { // Fetch the authorization server metadata from the main API const response = await fetch( `${apiUrl}/.well-known/oauth-authorization-server`, - { signal: AbortSignal.timeout(30_000) }, + { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) }, ) if (!response.ok) { @@ -136,22 +137,32 @@ const handleMcpRequest = async (c: Context<{ Bindings: Bindings }>) => { }) } - let authUser: { - userId: string - apiKey: string - email?: string - name?: string - } | null = null + let authResult = isApiKey(token) + ? await validateApiKey(token, apiUrl) + : await validateOAuthToken(token, apiUrl) - if (isApiKey(token)) { - console.log("Authenticating with API key") - authUser = await validateApiKey(token, apiUrl) - } else { - console.log("Authenticating with OAuth token") - authUser = await validateOAuthToken(token, apiUrl) + if (authResult.status === "timeout") { + return new Response( + JSON.stringify({ + jsonrpc: "2.0", + error: { + code: -32000, + message: "Authentication service timed out. Please try again.", + }, + id: null, + }), + { + status: 504, + headers: { + "Content-Type": "application/json", + "Access-Control-Expose-Headers": "WWW-Authenticate", + "Access-Control-Allow-Origin": "*", + }, + }, + ) } - if (!authUser) { + if (authResult.status !== "success") { const errorMessage = isApiKey(token) ? "Unauthorized: Invalid or expired API key" : "Unauthorized: Invalid or expired token" @@ -177,6 +188,8 @@ const handleMcpRequest = async (c: Context<{ Bindings: Bindings }>) => { ) } + const authUser = authResult.user + // Create execution context with authenticated user props const ctx = { ...c.executionCtx,