Repository navigation
Description
Summary
Since upgrading to opencode 2.0.14/2.0.15, local MCP servers configured with {env:VAR} placeholders in their environment block are spawned without any of the variables stored in the service env (service.json / opencode service get env). Servers that require those vars crash on startup, so opencode mcp list reports Connection closed. Before the upgrade (2.0.13 service) all of them connected with full tool sets.
Environment
- opencode version: 2.0.15 (also reproduced on 2.0.14 service; worked on 2.0.13)
- OS: Darwin 25.6.0 (macOS, arm64)
- Terminal: Apple_Terminal, xterm-256color, truecolor
- Shell: /bin/zsh
- Install/channel: latest; background service launched from the OpenCode desktop app bundled CLI (
.../ai.opencode.desktop/cli/2.0.15/opencode-cli serve --service) - Active plugins: none found
Reproduction
- Configure a local MCP server in
~/.config/opencode/opencode.jsoncwith anenvironmentblock using substitutions:{ "mcp": { "servers": { "demo": { "type": "local", "command": ["python3", "/tmp/env_dependent_mcp.py"], "environment": { "REQUIRED_VAR": "{env:MY_VAR}" } }}}} - Store
MY_VARin the service env (e.g.~/.config/opencode/service.jsonenvblock). - Start the opencode service (2.0.14+) and run
opencode mcp list. - Observe the child process: the placeholder variable is not set (verified with a debug server that dumps its env — every key from
service.jsonis missing in the child).
Expected Behavior
{env:MY_VAR} should resolve against the service env for spawned MCP processes, as it did on the 2.0.13 service (all servers connected: atlassian tools=98, cedrus tools=8, gp_* tools=9). opencode service get env still returns all configured variables.
Actual Behavior
Every local MCP server whose environment references {env:...} fails to start: the variables are absent in the child environment, so the servers exit with their "missing config" errors and opencode mcp list shows failed: Connection closed. Concrete symptoms:
postgres-mcp(uvx): exitsValueError: Error: No database URL provided. Please specify via 'DATABASE_URI' environment variable→Connection closed.trino-mcp: exitsValueError: TRINO_USER and TRINO_PASSWORD must be set for password authentication→Connection closed.mcp-atlassianconnects but withtools=0(tokens absent) while previously it had 98 tools.
Trigger: today 07:43 UTC opencode upgrade (2.0.13 -> 2.0.14/2.0.15) followed by a service restart. Repeated failures on every subsequent connect attempt (consistent, not intermittent).
Additional Context
- Log excerpt (role=server):
mcp connect failed ... status.error="Connection closed"for each affected server;mcp connected server=<name> tools=0for atlassian. - Direct proof: a minimal MCP server that dumps
os.environwas spawned through the service and saw allservice.jsonenv keys as missing, whileopencode service get envreturned them. - Literal (non-placeholder) values in
environmentdo reach the child: a server with a literalDATABASE_URIstarted and hung on the DB connection instead of crashing instantly on the "No database URL" check. - Manual launch of the same commands with the env exported works fine (servers respond to the MCP initialize handshake).
- Workaround applied: replaced
{env:...}with literal values in the config — all five servers now connect (atlassian tools=98,cedrus tools=8,gp_smkt/gp_mm/gp_sbl tools=9).
Activity
alexklink commentedon Sep 23, 2026
Same root cause also breaks provider API-key resolution, not just MCP child processes:
"env": ["SAMOKAT_LLM_API_KEY"](openai-compatible) fails every request on 2.0.14/2.0.15 withAuthentication Error, No api key passed in.— the key stored inservice.json(opencode service set env) is no longer visible to the request path, even thoughopencode service get envreturns it.opencode run -m ...reproduces the error; directcurlwith the same key works (so the upstream API is fine)./api/integration/<id>/connect/key/opencode auth login), which bypasses env resolution entirely.{env:...}placeholders in MCPenvironmentblocks, andenv:keys in provider configs, no longer resolve against the service env after upgrading from 2.0.13.yang0228 commentedon Sep 23, 2026
I've opened #50907 against
v2for this issue.The CLI launcher injects the saved service environment, but Desktop can start
serve --servicedirectly without it. The fix loadsservice.json's environment before importing the server, so config substitution and local MCP child processes can see those values.The regression tests start a real service and MCP process, covering missing/inherited variables, empty overrides, and preservation of unrelated inherited values. They fail without the fix and pass with it. Local CLI tests: 293 passed, 9 skipped, 0 failed; repository lint/typechecks and Effect checks also passed.
The fix is at the shared service startup boundary, not MCP-specific, but I haven't independently verified the custom-provider authentication case mentioned above.
If this is still available, could you assign it to me? I'm happy to follow up on review feedback and additional reproductions.
alexklink commentedon Sep 24, 2026
Thanks for the deep dive, @yang0228 — the root cause matches what we saw exactly, and I can confirm the custom-provider case is the same code path, not a separate bug:
"env": ["SAMOKAT_LLM_API_KEY"]; when the service process lacks the injected saved env, the key isn't found at request time → noAuthorizationheader → upstream answersAuthentication Error, No api key passed in.config.envintoprocess.envat service startup (Object.assign) fixes that too, because provider env resolution readsprocess.env. So the fix is not MCP-specific — good.Behavior notes on the proposed semantics (from the test cases): service.json values take precedence over inherited ones (
configuredoverinherited), empty overrides are preserved, and unrelated inherited values survive. That looks right to us.Two small confirmations from our side:
serve --service); consistent with your analysis.We can't assign the issue from here (no triage permissions on this repo) — if you can, please assign it to yourself.
Happy to validate the fix against our Desktop setup once a build with it lands.