fix(mcplocal): propagate caller's bearer to prompt-index and LLM-config calls
All checks were successful
CI/CD / typecheck (pull_request) Successful in 51s
CI/CD / test (pull_request) Successful in 1m3s
CI/CD / lint (pull_request) Successful in 2m27s
CI/CD / build (pull_request) Successful in 2m11s
CI/CD / smoke (pull_request) Successful in 4m56s
CI/CD / publish (pull_request) Has been skipped
All checks were successful
CI/CD / typecheck (pull_request) Successful in 51s
CI/CD / test (pull_request) Successful in 1m3s
CI/CD / lint (pull_request) Successful in 2m27s
CI/CD / build (pull_request) Successful in 2m11s
CI/CD / smoke (pull_request) Successful in 4m56s
CI/CD / publish (pull_request) Has been skipped
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:<sha> 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) <noreply@anthropic.com>
This commit is contained in:
@@ -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:<sha>).
|
||||
const saClient = requestClient.withHeaders({ 'X-Service-Account': `project:${projectName}` });
|
||||
router.setPromptConfig(saClient, projectName);
|
||||
|
||||
// System prompt fetcher for LLM consumers (uses router's cached fetcher)
|
||||
|
||||
@@ -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<typeof vi.fn>).mockReturnValue(client);
|
||||
(client.withToken as ReturnType<typeof vi.fn>).mockReturnValue(client);
|
||||
(client.withTimeout as ReturnType<typeof vi.fn>).mockReturnValue(client);
|
||||
return client;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user