From 75fe0533c1a9f795e8f33d65fb2c968da1cc5956 Mon Sep 17 00:00:00 2001 From: Michal Date: Sat, 18 Apr 2026 04:44:27 +0100 Subject: [PATCH] fix(mcplocal): propagate caller's bearer to prompt-index and LLM-config calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The proxy-path fix (5d10728) covered upstream tools/call routing via McpdUpstream, but getOrCreateRouter in project-mcp-endpoint.ts had TWO more mcpd-bound call sites that silently fell back to the pod's empty default token: 1. fetchProjectLlmConfig(mcpdClient, projectName) 2. router.setPromptConfig(mcpdClient.withHeaders({...})) → which is what gate.ts begin_session uses via ctx.fetchPromptIndex() to hit /api/v1/projects/:name/prompts/visible Symptom: in the k8s mcplocal pod, LiteLLM would initialize + tools/list fine (showing begin_session), but tools/call begin_session returned `{isError: true, content: "McpError: Authentication failed: invalid or expired token"}`. Reproduced against the live cluster by driving LiteLLM's /mcp/ endpoint with qwen3-thinking's exact payload. Fix: build `requestClient = mcpdClient.withToken(authToken)` once at the top of getOrCreateRouter and thread it through fetchProjectLlmConfig and setPromptConfig. withHeaders still adds X-Service-Account for mcpd-side audit tagging, but the bearer now carries the caller's McpToken identity (resolves as McpToken: on mcpd). Verified: unit tests pass (mock needed withToken/withTimeout stubs). Next step: rebuild image + roll pod + retest LiteLLM→mcp flow. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/mcplocal/src/http/project-mcp-endpoint.ts | 16 +++++++++++++--- src/mcplocal/tests/project-mcp-endpoint.test.ts | 6 +++++- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/mcplocal/src/http/project-mcp-endpoint.ts b/src/mcplocal/src/http/project-mcp-endpoint.ts index 48cd39e..42ec24a 100644 --- a/src/mcplocal/src/http/project-mcp-endpoint.ts +++ b/src/mcplocal/src/http/project-mcp-endpoint.ts @@ -62,21 +62,31 @@ export function registerProjectMcpEndpoint(app: FastifyInstance, mcpdClient: Mcp return existing.router; } + // HTTP-mode mcplocal has no pod-level credentials — the default + // `mcpdClient.token` is an empty string. Every downstream call from this + // request (upstream discovery, LLM config fetch, prompt index for + // begin_session) has to use the CALLER's McpToken as the bearer, or mcpd + // rejects with 401. Build one per-request client here and thread it + // everywhere instead of sprinkling `.withToken(authToken)` at each call site. + const requestClient = authToken ? mcpdClient.withToken(authToken) : mcpdClient; + // Create new router or refresh existing one const router = existing?.router ?? new McpRouter(); await refreshProjectUpstreams(router, mcpdClient, projectName, authToken); // Resolve project LLM model: local override → mcpd recommendation → global default const localOverride = loadProjectLlmOverride(projectName); - const mcpdConfig = await fetchProjectLlmConfig(mcpdClient, projectName); + const mcpdConfig = await fetchProjectLlmConfig(requestClient, projectName); const resolvedModel = localOverride?.model ?? mcpdConfig.llmModel ?? undefined; // If project llmProvider is "none", disable LLM for this project const llmDisabled = mcpdConfig.llmProvider === 'none' || localOverride?.provider === 'none'; const effectiveRegistry = llmDisabled ? null : (providerRegistry ?? null); - // Configure prompt resources with SA-scoped client for RBAC - const saClient = mcpdClient.withHeaders({ 'X-Service-Account': `project:${projectName}` }); + // Configure prompt resources with SA-scoped client for RBAC. + // Keep the X-Service-Account header for mcpd-side audit tagging, but carry + // the caller's bearer so auth passes (the principal resolves as McpToken:). + const saClient = requestClient.withHeaders({ 'X-Service-Account': `project:${projectName}` }); router.setPromptConfig(saClient, projectName); // System prompt fetcher for LLM consumers (uses router's cached fetcher) diff --git a/src/mcplocal/tests/project-mcp-endpoint.test.ts b/src/mcplocal/tests/project-mcp-endpoint.test.ts index 030a08b..04b5892 100644 --- a/src/mcplocal/tests/project-mcp-endpoint.test.ts +++ b/src/mcplocal/tests/project-mcp-endpoint.test.ts @@ -30,9 +30,13 @@ function mockMcpdClient() { delete: vi.fn(), forward: vi.fn(async () => ({ status: 200, body: [] })), withHeaders: vi.fn(), + withToken: vi.fn(), + withTimeout: vi.fn(), }; - // withHeaders returns a new client-like object (returns self for simplicity) + // Chainable withX returns the same client for simplicity (client.withHeaders as ReturnType).mockReturnValue(client); + (client.withToken as ReturnType).mockReturnValue(client); + (client.withTimeout as ReturnType).mockReturnValue(client); return client; }