fix(mcpd): per-route /api/v1/mcp/proxy auth missed McpToken dispatch

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) <noreply@anthropic.com>
This commit is contained in:
Michal
2026-04-18 00:23:44 +01:00
parent 1887d90821
commit dfc53cd15e

View File

@@ -315,10 +315,13 @@ async function main(): Promise<void> {
const backupService = new BackupService(serverRepo, projectRepo, secretRepo, userRepo, groupRepo, rbacDefinitionRepo, promptRepo, templateRepo); const backupService = new BackupService(serverRepo, projectRepo, secretRepo, userRepo, groupRepo, rbacDefinitionRepo, promptRepo, templateRepo);
const restoreService = new RestoreService(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 // Shared auth dependencies. Both the global auth hook and the per-route
const authMiddleware = createAuthMiddleware({ // preHandler on /api/v1/mcp/proxy must know how to resolve both session
findSession: (token) => authService.findSession(token), // bearers AND mcpctl_pat_ bearers, or mcplocal→mcpd proxy calls with a
findMcpToken: async (tokenHash) => { // 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); const row = await mcpTokenRepo.findByHash(tokenHash);
if (row === null) return null; if (row === null) return null;
return { return {
@@ -332,7 +335,8 @@ async function main(): Promise<void> {
revokedAt: row.revokedAt, revokedAt: row.revokedAt,
}; };
}, },
}); };
const authMiddleware = createAuthMiddleware(authDeps);
// Server // Server
const app = await createServer(config, { const app = await createServer(config, {
@@ -436,7 +440,7 @@ async function main(): Promise<void> {
registerMcpProxyRoutes(app, { registerMcpProxyRoutes(app, {
mcpProxyService, mcpProxyService,
auditLogService, auditLogService,
authDeps: { findSession: (token) => authService.findSession(token) }, authDeps,
}); });
registerRbacRoutes(app, rbacDefinitionService); registerRbacRoutes(app, rbacDefinitionService);
registerUserRoutes(app, userService); registerUserRoutes(app, userService);