v1.3.0
[1.3.0] - 2026-07-20
MCP servers can now be launched as local subprocesses, so any npx, uvx, or binary MCP server can be used without standing up an HTTP endpoint for it. Alongside the new transport, two long-standing defects in the existing MCP path are fixed — both of which affected the gateway whether or not you used MCP deliberately.
Thanks to @gr3enarr0w for contributing the stdio transport (#121).
Added
- MCP stdio transport. An
mcp_serversentry may now setcommand(with optionalargs) instead ofurl, and the gateway launches that process and speaks MCP over its stdin/stdout for the gateway's lifetime. Exactly one ofurlorcommandmust be set; both or neither is a config error naming the server. The existing Streamable HTTP transport is unchanged, and the two are interchangeable everywhere tools are consumed (#121). - Subprocess environment isolation. An MCP subprocess does not inherit the gateway's environment. It receives
PATH,HOME,LANG, andTMPDIRwhen set, plus exactly the keys under that server'senv— so no gateway credential such asOPENAI_API_KEYorMASTER_KEYreaches an MCP server implicitly. Isolation is from implicit inheritance, not a prohibition: anything a server needs must be listed in itsenv, and a credential placed there deliberately is passed through as configured. The same applies toHTTPS_PROXY,NODE_PATH, orSSL_CERT_FILE. ${VAR}references in a stdio server'senv. Resolved when the MCP client is constructed, the same treatmentheadersalready received, so the config keeps the reference and never stores the secret. Since the gateway environment is not inherited, this is the only channel by which a credential reaches an MCP subprocess.GET /admin/configredactsenvalongsideheaders.- Subprocess diagnostics. A stdio server's
stderris drained into the gateway log at debug level, one record per line. A server that dies on a missing API key or a permission error now says so in the log instead of surfacing as an opaque timeout.
Fixed
- A misconfigured MCP server no longer disables streaming. The agentic-loop redirect keyed off whether any MCP server was registered, which is true before the handshake runs and stays true after one fails. A single unreachable endpoint or typo'd command therefore turned every
stream: truerequest on the gateway into one buffered chunk — for every caller, including those making no use of MCP — with no error and no way to notice beyond the missing token-by-token delivery. Activation now keys off tools actually discovered, matching the non-streaming path. - Caller-supplied tool calls are no longer intercepted. With MCP active, every tool call in a model response was executed as if the gateway owned it. A client that sent its own
toolsarray — the ordinary OpenAI function-calling pattern, where the client executes the call and posts the result back — had its call swallowed and answered with a fabricated "tool not found in any registered MCP server", never receivingfinish_reason: "tool_calls". Tool calls the gateway does not own are now passed through untouched, so enabling MCP no longer breaks existing client-side tool integrations. - A single model response can no longer trigger unbounded tool execution.
max_call_depthbounded how many turns the agentic loop ran but nothing bounded the calls within one turn, so a response carrying thousands oftool_callsproduced that many executions and conversation messages, re-sent to the provider on every subsequent turn. Tool calls are capped per turn. - Stdio servers launched via
npxno longer leak processes.npxanduvxexec the real server as a separate process, and terminating the launcher does not terminate what it started, so every gateway shutdown and config reload left a live server behind holding its pipes. Subprocesses are now started in their own process group and the group is swept after the polite shutdown sequence completes. On Windows the previous single-process teardown is retained. - A stdio server writing heavily to
stderrno longer wedges. Its output went to a pipe nobody read, so once the operating system's pipe buffer filled, the server blocked mid-write and stopped answering requests entirely while still appearing to be running.
Changed
- MCP tools are advertised only when the request carries no tools of its own. Previously every MCP tool definition was injected into every chat completion, alongside whatever the caller sent. That let the model answer with one MCP call and one caller call in the same turn — which neither side can complete: the gateway has no implementation for the caller's tool, and the caller never declared the MCP one. Requests that send their own
toolsarray now pass through untouched, streaming included, and are unaffected by MCP entirely. Requests that send none behave exactly as before. mcp_servers[].envdocumentation corrected. The published field documentation describedenvas merged with the gateway's environment. It is not, and was not: the values listed are combined with a minimal base only. The godoc, README, and example config now describe the actual behavior.- Environment-reference documentation corrected across the config surface. Several field docs and example comments described
$VARas a usable reference and attributed substitution toLoadConfig. Only the braced${VAR}form is a reference, a bare$is literal data, an undefined variable is an error, and resolution happens when the component is constructed — never at config load, which is what keeps secrets out of the config-history store. The behavior is unchanged since v1.2.0; only the documentation was wrong.
Contributors
@gr3enarr0w — Clark Everson authored the MCP stdio transport in #121: the transport itself, its integration and unit test coverage, subprocess environment isolation via a custom CommandFunc, per-call timeouts, and transport-selection validation. That work is the substance of this release.
Those seven commits were cherry-picked onto the release branch with authorship preserved, but #352 was squash-merged, which collapsed them into a single commit and dropped his name from the history on main. This note is the durable record of that attribution.
@MitulShah1 — release hardening on top of that work (#352), plus #349 and #350.
Full changelog: https://github.com/ferro-labs/ai-gateway/blob/v1.3.0/CHANGELOG.md