Skip to content

v1.3.0

Choose a tag to compare

@github-actions github-actions released this 20 Jul 13:04
· 20 commits to main since this release
v1.3.0
47e589a

[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_servers entry may now set command (with optional args) instead of url, and the gateway launches that process and speaks MCP over its stdin/stdout for the gateway's lifetime. Exactly one of url or command must 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, and TMPDIR when set, plus exactly the keys under that server's env — so no gateway credential such as OPENAI_API_KEY or MASTER_KEY reaches an MCP server implicitly. Isolation is from implicit inheritance, not a prohibition: anything a server needs must be listed in its env, and a credential placed there deliberately is passed through as configured. The same applies to HTTPS_PROXY, NODE_PATH, or SSL_CERT_FILE.
  • ${VAR} references in a stdio server's env. Resolved when the MCP client is constructed, the same treatment headers already 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/config redacts env alongside headers.
  • Subprocess diagnostics. A stdio server's stderr is 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: true request 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 tools array — 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 receiving finish_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_depth bounded how many turns the agentic loop ran but nothing bounded the calls within one turn, so a response carrying thousands of tool_calls produced 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 npx no longer leak processes. npx and uvx exec 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 stderr no 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 tools array now pass through untouched, streaming included, and are unaffected by MCP entirely. Requests that send none behave exactly as before.
  • mcp_servers[].env documentation corrected. The published field documentation described env as 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 $VAR as a usable reference and attributed substitution to LoadConfig. 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