From 339c7adbcab8243248186bb44f5ab0100fe897ee Mon Sep 17 00:00:00 2001 From: L4XB Date: Fri, 11 Sep 2026 20:13:04 +0200 Subject: [PATCH] fix(mcp): gate every spelling of the OAuth endpoint requiresAuthentication compared the raw request path with "/mcp/oauth". Express matches routes case-insensitively and with an optional trailing slash, so "/mcp/oauth/" and "/MCP/OAUTH" reached the OAuth handler while the gate treated them like the anonymous /mcp route. Compare the canonical form instead, the same normalization mcpRouteFromUrl already uses for telemetry. --- .changeset/oauth-route-spelling.md | 5 +++++ packages/mcp/src/index.ts | 5 ++++- packages/mcp/test/integration.test.ts | 9 +++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 .changeset/oauth-route-spelling.md diff --git a/.changeset/oauth-route-spelling.md b/.changeset/oauth-route-spelling.md new file mode 100644 index 000000000..adb7e7528 --- /dev/null +++ b/.changeset/oauth-route-spelling.md @@ -0,0 +1,5 @@ +--- +"@upstash/context7-mcp": patch +--- + +Apply the OAuth endpoint's authentication requirement to every spelling Express routes to it (`/mcp/oauth/`, `/MCP/OAUTH`), not only the exact `/mcp/oauth`. diff --git a/packages/mcp/src/index.ts b/packages/mcp/src/index.ts index 6656dcbce..9e43d939c 100644 --- a/packages/mcp/src/index.ts +++ b/packages/mcp/src/index.ts @@ -54,7 +54,10 @@ function getPluginFromRequest(req: express.Request): typeof CLAUDE_CODE_PLUGIN | function requiresAuthentication(req: express.Request, plugin?: typeof CLAUDE_CODE_PLUGIN): boolean { // The MCP routes live on a router mounted at /mcp, so req.path is relative to it. - const isOAuthEndpoint = `${req.baseUrl}${req.path}` === "/mcp/oauth"; + // Express routes "/mcp/oauth/" and "/MCP/OAUTH" to the same handler but keeps the + // request's spelling here, so compare the canonical form (as mcpRouteFromUrl does). + const isOAuthEndpoint = + `${req.baseUrl}${req.path}`.replace(/\/+$/, "").toLowerCase() === "/mcp/oauth"; // The current official Claude plugin expands an unset API key to an empty header. const hasEmptyPluginAuthorization = plugin === CLAUDE_CODE_PLUGIN && req.headers.authorization === ""; diff --git a/packages/mcp/test/integration.test.ts b/packages/mcp/test/integration.test.ts index cff8bf91c..d4d8399bf 100644 --- a/packages/mcp/test/integration.test.ts +++ b/packages/mcp/test/integration.test.ts @@ -756,6 +756,15 @@ describe("plugin authentication", () => { expect(emptyHeaderRes.status).toBe(401); }); + test("protects every spelling Express routes to the OAuth endpoint", async () => { + // Express matches routes case-insensitively and with an optional trailing + // slash; the gate must not depend on the request's spelling. + for (const spelling of ["/mcp/oauth/", "/MCP/OAUTH", "/Mcp/OAuth/"]) { + const url = httpUrl.replace(/\/mcp$/, spelling); + expect((await postMcp(url)).status, spelling).toBe(401); + } + }); + test("tracks authenticated plugin requests separately", async () => { const client = new Client( { name: "claude-code", version: "1.0.0" },