From 740ce31469d8f536c3cdeff7832f1ab94e2cdb38 Mon Sep 17 00:00:00 2001 From: Michal Date: Sat, 15 Aug 2026 21:43:00 +0100 Subject: [PATCH 1/2] fix(mcplocal): give proxied tools/call a 120s budget, not the 30s default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit McpdUpstream.send routed every non-list method through the default-timeout client, so any tool that legitimately runs past 30s — web_url_read through a browser solver, a large PDF extraction, a slow retail site — died as "mcpd proxy error: mcpd did not respond within 30000ms" while mcpd was still working on it. LibreChat agents hit this constantly on real pages. New TOOLCALL_TIMEOUT_MS (default 120s, env MCPLOCAL_TOOLCALL_TIMEOUT_MS) sits between DISCOVERY_TIMEOUT_MS (list calls must stay short so a dead upstream can't stall session init) and LONG_RUNNING_TIMEOUT_MS (chat and inference, minutes). withTimeout preserves the caller token and headers. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01JaFvfHrQyUKCGv6o3N2Wir --- src/mcplocal/src/discovery.ts | 8 ++++++-- src/mcplocal/src/http/mcpd-client.ts | 11 +++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) 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; From c66502e5900cc3a665a31bbfed1decb57e0943f0 Mon Sep 17 00:00:00 2001 From: Michal Date: Sat, 15 Aug 2026 21:43:00 +0100 Subject: [PATCH 2/2] fix(mcplocal): fetch project instructions with the caller's token (#113) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The instructions fetch was the one downstream call in getOrCreateRouter still using mcpdClient — whose token is an empty string in HTTP mode — so mcpd answered 401, the catch swallowed it, and every session initialized with no instructions while nothing looked broken. requestClient exists precisely for this; use it. Closes #113 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01JaFvfHrQyUKCGv6o3N2Wir --- src/mcplocal/src/http/project-mcp-endpoint.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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[] = [];