Repository navigation
Frodo CLI 4.10.0
Added
-
Stabilized the
frodo mcp server startHTTP transport (--transport http) so an MCP gateway running in a Docker container on the same host can reach it without changing the gateway's image or network model; every gate is frodo-side configuration:SIGHUP(terminal closed) andSIGQUIT(Ctrl+) now joinSIGTERM/SIGINTin the graceful-shutdown wiring, and shutdown force-closes idle keep-alive sockets before releasing the port -- closing the SSH session that launched a long-lived HTTP server no longer orphans the listener holding the port.EADDRINUSEprints one actionable message (find the incumbent withlsof -iTCP:<port> -sTCP:LISTEN, use--port <other>or stop it) and exits with code 1, instead of doubling the error through the global unhandled-rejection printer.--allowed-hosts <host...>extends the defaultHostheader allow-list (localhost,127.0.0.1,[::1]) with extra client hostnames;host.docker.internalis accepted automatically whenever the bind host is non-loopback (the standard bridge-network container alias for the host machine).--mcp-auth-token <secret>(with theFRODO_MCP_AUTH_TOKENenvironment fallback, preferred so the secret stays out of process listings) requires a matchingAuthorization: Bearerheader on every MCP request, timing-safely compared;GET /healthstays unauthenticated for liveness probes. Binding a non-loopback host without a token refuses to start unless--allow-unauthenticatedis passed explicitly; loopback binds without a token behave exactly as before.- Request-metadata headers (
MCP-Protocol-Version,Mcp-Method,Mcp-Name) are now validated era-conditionally: current-protocol clients get the full header/body cross-checks as before, while earlier-era gateways (LiteLLM defaults to protocol revision 2025-11-25; Kong shipped 2025-06-18) are no longer 400'd at frodo's HTTP layer for headers their protocol era never sends -- the incident behind the original containerized-gateway connection failure. - Two SDK-parity rejections close silent-downgrade holes in the era gate: a present-but-malformed
_metaenvelope claim (a claim key whose value is not a protocol-version string) is answered400 -32602with the SDK's ownenvelope-invalidwording instead of being quietly served as legacy traffic with every modern cross-check skipped, and an empty JSON-RPC batch ([]) is answered400 -32600instead of an empty202. Each received shutdown signal (SIGTERM/SIGINT/SIGHUP/SIGQUIT) is now logged, so a remotely triggered shutdown leaves a record of why the server went down. - Observability for the long-running server: the listening line now carries the server PID, a liveness heartbeat (
heartbeat: ... still listening ..., every 15 minutes) tells a healthy listener apart from a hung one in remote logs, anEADDRINUSEmessage names a live MCP incumbent when its/healthendpoint answers the probe (plain squatters still get the generic message), and an uncaught exception in server mode logs one timestamped crash line (eventcrash), closes the port best-effort, and exits 1 so a supervisor restarts cleanly; unhandled promise rejections stay owned byfrodo's global CLI handler (logged, exit code 1, server keeps serving).frodo'slaunch.cjswrapper now forwardsSIGHUP/SIGTERM/SIGINT/SIGQUITto the CLI child (and kills the child if the wrapper exits first), so signals that reach only the wrapper — a closing SSH session,kill <wrapper-pid>— actually stop the server and release the port. - SDK-parity completion of the 2026-07-28 envelope gate: every request carrying an envelope claim must now also carry the required
io.modelcontextprotocol/clientCapabilitieskey (a present object), exactly as the SDK'svalidateEnvelopeMetarequires — a claim-only request (modern or legacy-dated revision named, capabilities missing) is answered400 -32602envelope-invalidwith the SDK's exact wording, missing keys reported before schema violations inside present keys. Aninitializewith a modern claim but no capabilities key still classifies as the legacy handshake (SDKcarriesValidModernEnvelopeClaimparity), and a JSON-RPC batch containing ANY claimed element is rejected400 -32600(batch-with-modern-elementparity — the SDK rejects on claim presence, whatever its validity or era). Notifications are validated exactly as narrowly as the SDK validates them: a claimed notification needs only a string claim (no capabilities key, whatever revision) and the only notification envelope rejection is a non-string claim value.POST /mcpnow also accepts a query string (/mcp?x=y— RFC 9110, the query is not part of the path; debug arrival lines log the route path only, never the query), and the -32020 presence messages restored origin/main's header capitalization (MCP-Protocol-Version,Mcp-Method,Mcp-Name). - Per-request gate observability at
--mcp-log-level debug(httpevent): every request's arrival (method, URL, remote address), its fate at each gate (route miss, Host/Origin, 405, 401 -- the Authorization header's PRESENCE is logged, never its contents --, 400, 406, metadata/protocol-version/batch rejections) and one acceptance line before the SDK transport answers. At the defaultinfolevel these lines stay quiet; the point is telling "requests arrive and are rejected at a gate" apart from "requests never arrive". - Operational hygiene for long-running deployments:
frodo mcp server stopstops a running HTTP server via its PID lockfile (~/.frodo/mcp-http-<port>.pid, honoringFRODO_CONFIG_PATH; written after a successful listen, removed on every shutdown signal and on the crash path) -- SIGTERM first with a 10-second wait,--forcefor SIGKILL, stale lockfiles cleaned as a success, a best-effort PID-reuse guard refusing to signal a process that demonstrably is not frodo, and a clear exit 1 when no lockfile exists for the port. A start over a dead-PID lockfile logs a one-line stale note (overwriting stale lockfile for port N (recorded pid P is not running)) before overwriting it.--port autobinds an OS-assigned ephemeral port and the listening line/heartbeat/lockfile all report the RESOLVED port (also fixing--port 0printing the requested0; a literal--port 0now falls back to the default 6277 like other invalid values instead of binding ephemeral -- use--port autofor that). The dry-run summary reports the literal option value ("port": "auto"rather than the internal0) since no port is bound on a dry run.--max-body-size <bytes>(default 1 MiB;FRODO_MCP_MAX_BODY_SIZE) bounds request bodies with aContent-Lengthpre-check plus a mid-stream accumulation cap, answering413with a JSON-RPC-32000error naming the limit and closing the socket (the mid-stream path'sreceivedBytesreports the bytes actually observed up to the cap, not the limit itself);--max-concurrent-requests <n>(default 64;FRODO_MCP_MAX_CONCURRENT_REQUESTS) rejects over-cap handler executions with429+Retry-After: 1(queue-less; the cap counts handler executions, so a slow SSE stream holds its slot until its handler resolves);FRODO_MCP_HEARTBEAT_INTERVAL_MSoverrides the liveness heartbeat (clamped >= 1000 ms, invalid keeps the 15-minute default). All defaults are generous (1 MiB is ~2.5x the largest observed QA payload; 64 far above observed load) and every knob is opt-in, so existing deployments are unaffected. - Container packaging: a multi-stage
Dockerfile(node:24-slim build runningnpm run build:only, then a slim runtime stage with only the self-containeddist/bundle -- zero runtimenode_modules-- running as the non-rootnodeuser withlaunch.cjsas ENTRYPOINT sodocker stopperforms the graceful shutdown) and an exampledocker/docker-compose.yml(bridge networkmcpnet, read-onlyConnections.jsonmount, node-one-liner/healthhealthcheck,restart: unless-stopped,--allowed-hosts frodo-mcpso the co-located gateway's service-DNS-nameHostheader passes the Host gate, and a commented-out co-located-gateway service dialinghttp://frodo-mcp:6277/mcpby service DNS name). Documented indocs/MCP_CLIENT_SETUP.md.
-
Graceful shutdown now includes
SIGHUPandSIGQUITsignals, force-closes idle sockets, and logs shutdown signals (SIGTERM/SIGINT/SIGHUP/SIGQUIT) for traceability. (commit 6d6f291)- Improved error handling for
EADDRINUSE, providing actionable messages and exit codes. (commit 6d6f291) - Extended
--allowed-hoststo include extra client hostnames, automatically acceptinghost.docker.internalfor non-loopback binds. (commit 6d6f291) - Introduced
--mcp-auth-tokenfor secure requests, with an environment variable fallback. Unauthenticated binds require explicit allowance. (commit 6d6f291) - Enhanced request validation to accommodate different protocol eras, preventing silent downgrades and ensuring SDK parity. (commit 6d6f291)
- Improved observability with server PID logging, liveness heartbeats, and detailed request processing logs at
--mcp-log-level debug. (commit 6d6f291) - Operational hygiene improvements include a
frodo mcp server stopcommand, lockfile management, and support for ephemeral ports with--port auto. (commit 6d6f291) - Added container packaging with a multi-stage
Dockerfileand exampledocker-compose.yml, supporting non-root execution and graceful shutdown. (commit 6d6f291)
- Improved error handling for
Fixed
- Corrected the dry-run summary to report the literal 'auto' port instead of the internal
0. (commit c9906c5) - Log a note when starting over a stale lockfile to indicate overwriting a dead record. (commit 06d344b)
- Allow the compose gateway's service DNS name in the Host gate to facilitate containerized deployments. (commit 8e62327)