Repository navigation
mcp: remote server {env:...} header credentials resolve empty until service restart (global config cached forever) #54205
Description
Summary
Remote MCP server header credentials using {env:VAR} substitution send an empty token when the background service has been running since before the variable was available: global config is cached forever (infinite TTL) inside the service, so external config edits / newly-set env vars are never picked up — GitHub's remote MCP endpoint then rejects the request with HTTP 400: Authorization header is badly formatted. A missing {env:...} variable is also silently substituted with an empty string instead of producing a diagnostic.
Environment
- opencode version: 2.0.26
- OS: Windows 10.0.26200 (win32 x64)
- Terminal: VSCode integrated terminal (TERM_PROGRAM=vscode, COLORTERM=truecolor)
- Shell: C:\WINDOWS\system32\cmd.exe
- Install/channel: latest
- Active plugins: none found in config
Reproduction
- Add a remote MCP server to the global config (
~/.config/opencode/opencode.json), e.g. the GitHub remote MCP server, using header auth with env substitution:{ "mcp": { "servers": { "github": { "type": "remote", "url": "https://api.githubcopilot.com/mcp/", "oauth": false, "headers": { "Authorization": "Bearer {env:GITHUB_PERSONAL_ACCESS_TOKEN}" } } } } } - Start the background service (
opencode serve --service); at this point the env var is not yet set in the machine/user environment. - Set the env var persistently (e.g. via
setx GITHUB_PERSONAL_ACCESS_TOKEN ...or the Windows environment dialog) and verify the token is valid — a directPOSTto the endpoint withAuthorization: Bearer <token>returns HTTP 200. - Restart the OpenCode client (or start a new session) and attempt to connect the
githubMCP server (e.g./mcps, or a new session that auto-connects).
Expected Behavior
- After the env var is set (step 3), the running service should either pick up the change (re-substitute / re-load config on file change), or clearly report that the service must be restarted so the header resolves correctly.
- If
{env:VAR}resolves to an unset variable, config loading should emit a visible warning/diagnostic naming the variable (e.g.environment variable GITHUB_PERSONAL_ACCESS_TOKEN not found while loading config) instead of silently substituting an empty string that later surfaces as a confusing upstream400from the MCP endpoint.
Actual Behavior
The github MCP server fails on every connect attempt with:
Error POSTing to endpoint: bad request: Authorization header is badly formatted
(HTTP 400)
opencode api get /api/config shows the header the service actually uses — the placeholder is gone but the value is empty:
"github":{"type":"remote","url":"https://api.githubcopilot.com/mcp/","headers":{"Authorization":"Bearer "}
Re-connecting via /mcps does not help; only opencode service restart (so the freshly started process inherits the new user environment and re-loads config) resolves it. A direct probe with an empty Bearer token reproduces the exact 400 from GitHub's endpoint, and a direct probe with the real token returns HTTP 200 — confirming the config syntax, endpoint, and token are all fine.
Additional Context
Relevant log lines (~/.local/share/opencode/log/opencode.log), failing in both the home directory and a project directory:
timestamp=2026-10-09T21:00:00.389Z level=WARN server=github message="mcp transport error" error="Error POSTing to endpoint: bad request: Authorization header is badly formatted\n (HTTP 400)"
timestamp=2026-10-09T21:00:00.597Z level=WARN server=github message="mcp connect failed" status.status=failed status.error="Error POSTing to endpoint: bad request: Authorization header is badly formatted\n (HTTP 400)"
Suspect source locations (from sst/opencode):
packages/opencode/src/config/config.ts: global config is loaded once and cached with an infinite TTL —Effect.cachedInvalidateWithTTL(loadGlobal(), Duration.infinity)— andinvalidateis only invoked fromupdateGlobal(config writes made through OpenCode itself), so external config file edits and newly-set env vars are never reflected in the running service.packages/opencode/src/config/variable.ts: substitution is(input.env?.[varName] ?? process.env[varName]) || ""— an unset variable silently becomes an empty string. TheSubstituteInputalready supportsmissing?: "error" | "empty", so surfacing missing variables (or at least logging a warning) seems feasible.
Frequency: consistent — fails every connect attempt until the service is restarted. Workaround: opencode service restart (from a terminal that has the env var), then reconnect.
Secrets: none included; the config file contains only the {env:...} placeholder.
Activity
opencode-agent commentedon Oct 9, 2026
Thanks for the detailed report and for probing the endpoint directly. I think this is a duplicate of #33853.
There are two parts to this:
ConfigVariable.substitute()inpackages/core/src/config/variable.tsstill replaces an unset{env:VAR}with"". Themissingoption you found only applies to{file:...}references, not to{env:...}. Missing {env:VAR} in config silently resolves to empty string #33853 asks for exactly the diagnostic you describe: name the unset variable at config load instead of letting it show up later as an opaque 400/401 from the MCP endpoint.packages/opencode/src/config/config.tswith an infinite-TTL cache) is the v1 code. v2 loads config inpackages/core/src/config.ts. More importantly,setxand the Windows environment dialog only affect processes started after the change. A service that is already running keeps the environment it started with, so even reloading the config would still give an empty token. Restarting the service is the expected way to pick up a newly set OS variable. You could also try setting it withopencode service set env GITHUB_PERSONAL_ACCESS_TOKEN <token>, which stores it in the service's own config instead of relying on the inherited environment. I haven't checked whether that takes effect without a restart. What makes this confusing is that nothing tells you the variable was empty, and that's Missing {env:VAR} in config silently resolves to empty string #33853.Possible duplicates and related issues, ordered from most to least likely:
{env:VAR}silently resolves to"", shown with a remote MCPBearer {env:...}header (same problem){env:...}values not passed to local MCP servers (related, but a different bug)auth.jsonchange (similar "needs a restart" symptom, different area)The bot will close this issue as a duplicate in 1 day unless you reply explaining how your problem is different.
olscode commentedon Oct 9, 2026
Agreed — this is a duplicate of #33853. Thanks for the quick triage, and for the
opencode service set envtip: that is a much better workaround than depending on the inherited OS environment.For anyone landing here, confirming the full picture: the running service resolved the header to
Authorization: Bearer(empty) — visible viaopencode api get /api/config— and GitHub's remote MCP endpoint rejects it withHTTP 400: Authorization header is badly formatted. The token itself is valid (a direct probe withBearer <token>returns HTTP 200), and the failure disappears afteropencode service restart. The only genuinely fix-worthy item is the silent empty substitution for unset{env:VAR}, which #33853 already tracks. Closing as a duplicate.opencode-agent commentedon Oct 9, 2026
Thanks for confirming and for writing up the full picture. That summary will help anyone who hits the same
HTTP 400: Authorization header is badly formattederror. If you'd like the silent empty substitution prioritised, a 👍 or a short comment on #33853 helps, since that's where the fix is tracked. The bot will close this issue as a duplicate of #33853 at the scheduled time.opencode-agent commentedon Oct 10, 2026
Closing this as a duplicate of #33853, as agreed above. That issue tracks the actual bug: an unset
{env:VAR}becomes an empty string with no warning, so the remote MCP fails later with an unclearHTTP 400. Until it's fixed, setting the token withopencode service set env GITHUB_PERSONAL_ACCESS_TOKEN <token>is the more reliable option. If you change an OS variable withsetx, you need to runopencode service restartafterwards. Thanks again for the clear report. If something here turns out to be different from #33853, reply and we can reopen it.