Skip to content

fix(mcp): give stdio servers the environment, and let builtins reconnect - #5770

Open
MummIndia wants to merge 1 commit into
odysseus-dev:devfrom
MummIndia:mcp-stdio-env
Open

fix(mcp): give stdio servers the environment, and let builtins reconnect#5770
MummIndia wants to merge 1 commit into
odysseus-dev:devfrom
MummIndia:mcp-stdio-env

Conversation

@MummIndia

Copy link
Copy Markdown

Summary

Two defects that combine to make the built-in browser MCP server unusable, each of
which hides the other.

_connect_stdio built env={**os.environ, **env} if env else None. None does not
mean "inherit the parent environment" — the MCP SDK substitutes a minimal default one.
Callers that pass an env inherit everything, which is why the Python builtins work:
they pass builtin_python_env(base_dir). The NPX browser server passes nothing, so it
loses the entire container environment including PLAYWRIGHT_BROWSERS_PATH. It then
looks for browsers in the default cache and reports Browser "firefox" is not installed — with the browser sitting one directory away, which is what makes this
hard to place.

Second, a stdio session can disappear without the process dying: the teardown races
across asyncio tasks. call_tool returned early on a missing session, and the existing
recovery only ran when a call raised — which presupposes a session. A missing one was
therefore terminal, even though reconnecting would have fixed it. Reconnection is now
attempted in that case too.

_reconnect_builtin also excluded the browser outright: it tested membership against
_BUILTIN_SERVERS, the Python-server dict, while is_builtin() counts the NPX servers
as builtins as well. It now handles both kinds.

Target branch

  • This PR targets dev, not main.

Linked Issue

Fixes #5769

Type of Change

  • Bug fix (non-breaking — fixes a confirmed issue)

Checklist

  • I searched open issues and open PRs — this is not a duplicate.
  • This PR targets dev
  • My changes are limited to the scope described above — no unrelated refactors or whitespace changes mixed in.
  • I actually ran the app (docker compose up) and verified the change works end-to-end.

How to Test

  1. Run in a deployment where PLAYWRIGHT_BROWSERS_PATH points somewhere other than the
    default cache, with the built-in browser server enabled.
  2. Before — the server starts but reports Browser "firefox" is not installed.
    Confirm the env is the cause by printing os.environ inside the spawned server.
  3. After — the server connects and reports its 30 tools, and
    browser_navigate against a real URL returns exit_code=0.
  4. For the reconnect path: drop the session (kill the npx child), then call a browser
    tool. Before, every subsequent call returns
    MCP server not connected: builtin_browser for the process lifetime. After, the
    log shows No session for builtin builtin_browser; attempting reconnect followed by
    Reconnected builtin MCP server: Built-in: Browser, and the call succeeds.

Visual / UI changes

None. src/mcp_manager.py only — transport and lifecycle, no rendering path.

Two defects that combine to make the built-in browser MCP server
unusable, each of which hides the other.

`_connect_stdio` built `env={**os.environ, **env} if env else None`.
`None` does not mean "inherit the parent environment" — the MCP SDK
substitutes a minimal default one. Callers that pass an env inherit
everything, which is why the Python builtins work: they pass
`builtin_python_env(base_dir)`. The NPX browser server passes nothing,
so it loses the entire container environment, including
PLAYWRIGHT_BROWSERS_PATH. It then looks for browsers in the default
cache and reports `Browser "firefox" is not installed` — with the
browser sitting one directory away, which is what makes this so hard to
place.

Second, a stdio session can disappear without the process dying: the
teardown races across asyncio tasks. `call_tool` returned early on a
missing session, and the existing recovery only ran when a call raised
— which presupposes a session. A missing one was therefore terminal,
even though reconnecting would have fixed it. Reconnection is now
attempted in that case too.

`_reconnect_builtin` also excluded the browser outright. It tested
membership against `_BUILTIN_SERVERS`, the Python-server dict, while
`is_builtin()` counts the NPX servers as builtins as well — so the one
server most likely to need a restart was the one that could never get
one. It now handles both kinds.

Together these mean a browser server that dropped mid-session stayed
down for the rest of the process lifetime, and a fresh one started
without the environment it needs. Verified on Docker/Windows: the
server reconnects on demand and reports its 30 tools, and
`browser_navigate` returns exit_code=0 against a real URL.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions github-actions Bot added the ready for review Description complete — ready for maintainer review label Jul 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready for review Description complete — ready for maintainer review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Built-in browser MCP server loses the container environment, and can never be reconnected

1 participant