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; }