From dfc53cd15e72224eefd1d40674add7f1999debfb Mon Sep 17 00:00:00 2001 From: Michal Date: Sat, 18 Apr 2026 00:23:44 +0100 Subject: [PATCH] fix(mcpd): per-route /api/v1/mcp/proxy auth missed McpToken dispatch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Symptom: LiteLLM → mcplocal → mcpd proxy calls for project-scoped MCP tool discovery all 401'd with "Authentication failed: invalid or expired token", even though the same mcpctl_pat_ bearer works against /api/v1/mcptokens/introspect and /api/v1/projects/:name/servers. Result: the new k8s mcplocal pod could accept the bearer and respond to /projects/:name/mcp (initialize was 200), but every downstream upstream discovery call through /api/v1/mcp/proxy failed. Root cause: registerMcpProxyRoutes installs its own route-scoped createAuthMiddleware with the `authDeps` parameter it receives. In main.ts that was being constructed with only `findSession` — missing the `findMcpToken` that the GLOBAL auth hook already had. So a mcpctl_pat_ bearer got all the way to the proxy route and then was handed to an old-shape middleware that knew nothing about the prefix. Fix: extract authDeps (findSession + findMcpToken) to a named const and reuse it for both the global hook and the proxy route. Comment at the declaration site warns future additions to keep the two paths in sync — they have to agree or McpToken bearers silently break on whichever one drifts. Verified against the live cluster: LiteLLM's discoverTools path no longer 401s; mcplocal logs now show successful upstream proxy calls. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/mcpd/src/main.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/mcpd/src/main.ts b/src/mcpd/src/main.ts index 4f7ce38..71545fa 100644 --- a/src/mcpd/src/main.ts +++ b/src/mcpd/src/main.ts @@ -315,10 +315,13 @@ async function main(): Promise { const backupService = new BackupService(serverRepo, projectRepo, secretRepo, userRepo, groupRepo, rbacDefinitionRepo, promptRepo, templateRepo); const restoreService = new RestoreService(serverRepo, projectRepo, secretRepo, userRepo, groupRepo, rbacDefinitionRepo, promptRepo, templateRepo); - // Auth middleware for global hooks - const authMiddleware = createAuthMiddleware({ - findSession: (token) => authService.findSession(token), - findMcpToken: async (tokenHash) => { + // Shared auth dependencies. Both the global auth hook and the per-route + // preHandler on /api/v1/mcp/proxy must know how to resolve both session + // bearers AND mcpctl_pat_ bearers, or mcplocal→mcpd proxy calls with a + // McpToken will 401 at the route layer even though the global hook accepts them. + const authDeps = { + findSession: (token: string) => authService.findSession(token), + findMcpToken: async (tokenHash: string) => { const row = await mcpTokenRepo.findByHash(tokenHash); if (row === null) return null; return { @@ -332,7 +335,8 @@ async function main(): Promise { revokedAt: row.revokedAt, }; }, - }); + }; + const authMiddleware = createAuthMiddleware(authDeps); // Server const app = await createServer(config, { @@ -436,7 +440,7 @@ async function main(): Promise { registerMcpProxyRoutes(app, { mcpProxyService, auditLogService, - authDeps: { findSession: (token) => authService.findSession(token) }, + authDeps, }); registerRbacRoutes(app, rbacDefinitionService); registerUserRoutes(app, userService);