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:
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user