diff --git a/apps/mcp/src/server/auth/index.test.ts b/apps/mcp/src/server/auth/index.test.ts index 55b5a197..7cb7691e 100644 --- a/apps/mcp/src/server/auth/index.test.ts +++ b/apps/mcp/src/server/auth/index.test.ts @@ -214,6 +214,34 @@ describe("MCP authentication", () => { expect(error).not.toHaveBeenCalled() }) + it("rejects an API key whose session has no organization", async () => { + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}) + const error = vi.spyOn(console, "error").mockImplementation(() => {}) + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue( + Response.json({ + user: { id: "user_test", email: "test@example.com", name: "Test" }, + org: null, + session: null, + role: null, + accessType: "full", + containerTags: null, + scope: { type: "full", permission: "write" }, + }), + ), + ) + + await expect( + validateApiKey("sm_noorg_key_0123456789abcdef", API_URL), + ).resolves.toBeNull() + expect(warn).toHaveBeenCalledTimes(1) + expect(warn).toHaveBeenCalledWith( + "API key rejected: session has no organization", + ) + expect(error).not.toHaveBeenCalled() + }) + it("rejects malformed API keys without an API request", async () => { const fetchSpy = vi.fn() vi.stubGlobal("fetch", fetchSpy) diff --git a/apps/mcp/src/server/auth/index.ts b/apps/mcp/src/server/auth/index.ts index 006cf66d..cb0ab193 100644 --- a/apps/mcp/src/server/auth/index.ts +++ b/apps/mcp/src/server/auth/index.ts @@ -117,7 +117,10 @@ export async function validateApiKey( try { const session = await fetchSession(token, apiUrl) const organizationId = session.org?.id - if (!organizationId) return null + if (!organizationId) { + console.warn("API key rejected: session has no organization") + return null + } const user: AuthUser = { userId: session.user.id, diff --git a/apps/mcp/src/shared/types.ts b/apps/mcp/src/shared/types.ts index a1834f72..8c8a0ae3 100644 --- a/apps/mcp/src/shared/types.ts +++ b/apps/mcp/src/shared/types.ts @@ -37,8 +37,8 @@ export const sessionInfoSchema = z email: z.string().optional(), name: z.string().optional(), }), - org: z.looseObject({ id: z.string().min(1) }).optional(), - role: z.string().optional(), + org: z.looseObject({ id: z.string().min(1) }).nullish(), + role: z.string().nullish(), accessType: z.enum(["full", "restricted"]).optional(), containerTags: z.array(sessionNamespaceAccessSchema).nullable().optional(), scope: sessionScopeSchema.optional(),