From f355dc80396ff8e5fccc36b774ddd22b0e19cd05 Mon Sep 17 00:00:00 2001 From: sreedharsreeram <141047751+sreedharsreeram@users.noreply.github.com> Date: Sat, 24 Oct 2026 23:59:06 +0000 Subject: [PATCH] feat(mcp): validate API keys via /session (#1839) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What The Supermemory API now serves the session endpoint unversioned at `/session` (supermemoryai/mono#3626). `sm_` API-key validation in the MCP worker calls `/session` instead of `/v3/session`. One-line code change. - `apps/mcp/src/server/auth/index.ts`: `fetchSession` requests `/session`. - `apps/mcp/src/server/auth/index.test.ts`: asserts the new URL and that a refused key makes exactly one request. - `apps/mcp/src/shared/types.ts`: comment updated. ## Deploy order This worker depends on the API serving `/session`. **Deploy mono#3626 first.** Until then, production answers `/session` with 404 for full keys and 403 for scoped keys (the scope allowlist is checked before routing), and every `sm_` key would be rejected. No fallback to `/v3/session` on purpose: the only "old server" in scope is that deploy window, and a failed-request fallback would blur 403/404 semantics for a case merge ordering already covers. ## Verification - `bun run check-types`, `vitest run src` (29/29). - Against the API with mono#3626 running: full key → `/session` 200 valid; scoped key → `/session` 200 valid; bad key → `/session` 401 rejected, single call. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- apps/mcp/src/server/auth/index.test.ts | 12 +++++++----- apps/mcp/src/server/auth/index.ts | 2 +- apps/mcp/src/shared/types.ts | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/apps/mcp/src/server/auth/index.test.ts b/apps/mcp/src/server/auth/index.test.ts index 7adb52c4..55b5a197 100644 --- a/apps/mcp/src/server/auth/index.test.ts +++ b/apps/mcp/src/server/auth/index.test.ts @@ -177,7 +177,7 @@ describe("MCP authentication", () => { scopes: [], }) expect(fetchSpy).toHaveBeenCalledWith( - `${API_URL}/v3/session`, + `${API_URL}/session`, expect.objectContaining({ headers: { Authorization: `Bearer ${key}` }, }), @@ -197,14 +197,16 @@ describe("MCP authentication", () => { it("rejects an API key the session endpoint refuses", async () => { const warn = vi.spyOn(console, "warn").mockImplementation(() => {}) const error = vi.spyOn(console, "error").mockImplementation(() => {}) - vi.stubGlobal( - "fetch", - vi.fn().mockResolvedValue(new Response(null, { status: 401 })), - ) + const fetchSpy = vi + .fn() + .mockResolvedValue(new Response(null, { status: 401 })) + vi.stubGlobal("fetch", fetchSpy) await expect( validateApiKey("sm_revoked_key_0123456789abcdef", API_URL), ).resolves.toBeNull() + expect(fetchSpy).toHaveBeenCalledTimes(1) + expect(fetchSpy.mock.calls[0]?.[0]).toBe(`${API_URL}/session`) expect(warn).toHaveBeenCalledTimes(1) expect(warn).toHaveBeenCalledWith( "API key rejected: Session request failed with status 401", diff --git a/apps/mcp/src/server/auth/index.ts b/apps/mcp/src/server/auth/index.ts index b1e56bf9..006cf66d 100644 --- a/apps/mcp/src/server/auth/index.ts +++ b/apps/mcp/src/server/auth/index.ts @@ -31,7 +31,7 @@ export async function fetchSession( bearerToken: string, apiUrl: string, ): Promise { - const response = await fetch(`${apiUrl.replace(/\/+$/, "")}/v3/session`, { + const response = await fetch(`${apiUrl.replace(/\/+$/, "")}/session`, { method: "GET", headers: { Authorization: `Bearer ${bearerToken}` }, signal: AbortSignal.timeout(FETCH_TIMEOUT_MS), diff --git a/apps/mcp/src/shared/types.ts b/apps/mcp/src/shared/types.ts index ad59a02b..a1834f72 100644 --- a/apps/mcp/src/shared/types.ts +++ b/apps/mcp/src/shared/types.ts @@ -11,7 +11,7 @@ export const namespaceAccessSchema = z.object({ export type NamespaceAccess = z.infer -// /v3/session has no v5 replacement and still sends container-tag field names. +// /session still sends container-tag field names. const sessionNamespaceAccessSchema = z .object({ containerTag: z.string(), permission: z.enum(["read", "write"]) }) .transform(({ containerTag, permission }) => ({