Compare commits

..

4 Commits

Author SHA1 Message Date
Michal
c66502e590 fix(mcplocal): fetch project instructions with the caller's token (#113)
Some checks failed
CI/CD / typecheck (push) Successful in 1m23s
CI/CD / test (push) Successful in 1m26s
CI/CD / lint (push) Successful in 2m55s
CI/CD / smoke (push) Failing after 1m59s
CI/CD / build (push) Successful in 4m53s
CI/CD / publish (push) Has been skipped
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JaFvfHrQyUKCGv6o3N2Wir
2026-08-15 21:43:00 +01:00
Michal
740ce31469 fix(mcplocal): give proxied tools/call a 120s budget, not the 30s default
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JaFvfHrQyUKCGv6o3N2Wir
2026-08-15 21:43:00 +01:00
b6983f036d merge: stop the deploy script running smoke twice (#112)
Some checks failed
CI/CD / lint (push) Successful in 1m11s
CI/CD / test (push) Successful in 1m24s
CI/CD / typecheck (push) Successful in 3m7s
CI/CD / smoke (push) Failing after 1m57s
CI/CD / build (push) Successful in 2m18s
CI/CD / publish (push) Has been skipped
2026-08-14 22:45:30 +00:00
Michal
21aadf6d82 fix(deploy): stop running the smoke suite twice and crying wolf
Some checks failed
CI/CD / lint (pull_request) Successful in 1m14s
CI/CD / test (pull_request) Successful in 1m25s
CI/CD / typecheck (pull_request) Successful in 3m5s
CI/CD / smoke (pull_request) Failing after 1m57s
CI/CD / build (pull_request) Successful in 5m9s
CI/CD / publish (pull_request) Has been skipped
Step 7 called release.sh — which restarts mcplocal and runs the smoke suite
against the binary it just installed — and then restarted mcplocal and ran the
whole suite again. Two full suites inside a minute trips mcpd's rate limiter,
so the second run came back with six 429s and printed

    SMOKE TESTS FAILED — system may be unhealthy. Consider rollback

over a deploy that was fine. Seen for real on 35d506d: the first suite was
162/162 green, the second failed only on `mcpd returned 429: Rate limit
exceeded`, and a clean re-run afterwards was 162/162 again.

A deploy script that recommends rolling back a healthy release is worse than
one that says nothing. One run, one verdict — release.sh's exit status.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNXFvxanvM6uiFcb4Mp3xU
2026-08-14 23:45:16 +01:00
4 changed files with 31 additions and 10 deletions

View File

@@ -187,14 +187,16 @@ trap - ERR
# ── 7. RPM + smoke ──
say "7/7 Build/install CLI RPM + smoke tests"
bash scripts/release.sh
systemctl --user restart mcplocal && sleep 2
if pnpm test:smoke > /tmp/deploy-smoke.log 2>&1; then
grep -E "Tests |passed" /tmp/deploy-smoke.log | tail -2
# release.sh already restarts mcplocal and runs the smoke suite against the
# binary it just installed. This step used to restart and re-run it a second
# time, which put two full suites inside a minute and tripped mcpd's rate
# limiter: the second run failed with 429s and printed a false
# "SMOKE TESTS FAILED — consider rollback" over a perfectly healthy deploy.
# One run, one verdict.
if bash scripts/release.sh; then
say "Deploy complete — $TAG live. Rollback tag: $ROLLBACK_TAG"
else
tail -40 /tmp/deploy-smoke.log
warn "SMOKE TESTS FAILED — system may be unhealthy. Consider rollback:"
warn "RELEASE OR SMOKE TESTS FAILED — system may be unhealthy. Consider rollback:"
rollback_recipe
exit 1
fi

View File

@@ -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);

View File

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

View File

@@ -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[] = [];