diff --git a/src/mcplocal/src/discovery.ts b/src/mcplocal/src/discovery.ts index 2cf8970..50279b3 100644 --- a/src/mcplocal/src/discovery.ts +++ b/src/mcplocal/src/discovery.ts @@ -1,5 +1,5 @@ import type { McpdClient } from './http/mcpd-client.js'; -import { DISCOVERY_TIMEOUT_MS } from './http/mcpd-client.js'; +import { DISCOVERY_TIMEOUT_MS, TOOLCALL_TIMEOUT_MS } from './http/mcpd-client.js'; import type { McpRouter } from './router.js'; import { McpdUpstream } from './upstream/mcpd.js'; @@ -152,6 +152,10 @@ function syncUpstreams(router: McpRouter, mcpdClient: McpdClient, servers: McpdS // unreachable upstream cannot stall session init for the full tool-call window. const discoveryClient = mcpdClient.withTimeout(DISCOVERY_TIMEOUT_MS); + // Everything else an upstream receives is a proxied tools/call, which can + // legitimately run past the 30s default (browser solvers, PDF extraction). + const toolClient = mcpdClient.withTimeout(TOOLCALL_TIMEOUT_MS); + // Remove stale upstreams const currentNames = new Set(router.getUpstreamNames()); const serverNames = new Set(servers.map((s) => s.name)); @@ -164,7 +168,7 @@ function syncUpstreams(router: McpRouter, mcpdClient: McpdClient, servers: McpdS // Add/update upstreams for each server for (const server of servers) { if (!currentNames.has(server.name)) { - const upstream = new McpdUpstream(server.id, server.name, mcpdClient, server.description, discoveryClient); + const upstream = new McpdUpstream(server.id, server.name, toolClient, server.description, discoveryClient); router.addUpstream(upstream); } registered.push(server.name); diff --git a/src/mcplocal/src/http/mcpd-client.ts b/src/mcplocal/src/http/mcpd-client.ts index 8e755e4..1734076 100644 --- a/src/mcplocal/src/http/mcpd-client.ts +++ b/src/mcplocal/src/http/mcpd-client.ts @@ -62,6 +62,17 @@ export const LONG_RUNNING_TIMEOUT_MS = Number(process.env['MCPLOCAL_LONG_TIMEOUT */ export const DISCOVERY_TIMEOUT_MS = Number(process.env['MCPLOCAL_DISCOVERY_TIMEOUT_MS']) || 8_000; +/** + * Budget for proxied tools/call. The 30s DEFAULT_TIMEOUT_MS is a guaranteed + * failure for tools that legitimately run long — web_url_read through a + * browser solver, a large PDF extraction, a slow retail site — all of which + * died as `mcpd proxy error: mcpd did not respond within 30000ms` while mcpd + * was still working. Distinct from LONG_RUNNING_TIMEOUT_MS (chat/inference, + * minutes) and DISCOVERY_TIMEOUT_MS (list calls, must stay short so a dead + * upstream cannot stall session init). Override via `MCPLOCAL_TOOLCALL_TIMEOUT_MS`. + */ +export const TOOLCALL_TIMEOUT_MS = Number(process.env['MCPLOCAL_TOOLCALL_TIMEOUT_MS']) || 120_000; + export class McpdClient { private readonly baseUrl: string; private readonly token: string; diff --git a/src/mcplocal/src/http/project-mcp-endpoint.ts b/src/mcplocal/src/http/project-mcp-endpoint.ts index 274b6b7..a66645f 100644 --- a/src/mcplocal/src/http/project-mcp-endpoint.ts +++ b/src/mcplocal/src/http/project-mcp-endpoint.ts @@ -176,9 +176,13 @@ export function registerProjectMcpEndpoint(app: FastifyInstance, mcpdClient: Mcp chain.push(createAgentsPlugin()); router.setPlugin(composePlugins(chain)); - // Fetch project instructions and set on router + // Fetch project instructions and set on router. Must ride the CALLER's + // token (requestClient) like every other downstream call here: in HTTP + // mode mcpdClient's own token is empty, mcpd answers 401, and the catch + // below swallowed it — every session initialized with no instructions + // while nothing looked broken (#113). try { - const instructions = await mcpdClient.get<{ prompt: string; servers: Array<{ name: string; description: string }> }>( + const instructions = await requestClient.get<{ prompt: string; servers: Array<{ name: string; description: string }> }>( `/api/v1/projects/${encodeURIComponent(projectName)}/instructions`, ); const parts: string[] = [];