Skip to content

mcp: remote server {env:...} header credentials resolve empty until service restart (global config cached forever) #54205

Closed as duplicate

Description

@olscode

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

  1. 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}"
            }
          }
        }
      }
    }
  2. Start the background service (opencode serve --service); at this point the env var is not yet set in the machine/user environment.
  3. 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 direct POST to the endpoint with Authorization: Bearer <token> returns HTTP 200.
  4. Restart the OpenCode client (or start a new session) and attempt to connect the github MCP 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 upstream 400 from 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) — and invalidate is only invoked from updateGlobal (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. The SubstituteInput already supports missing?: "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

  1. opencode-agent commented on Oct 9, 2026

    @opencode-agent
    Contributor

    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:

    • The empty value with no warning: in v2, ConfigVariable.substitute() in packages/core/src/config/variable.ts still replaces an unset {env:VAR} with "". The missing option 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.
    • Needing a service restart: the source you quoted (packages/opencode/src/config/config.ts with an infinite-TTL cache) is the v1 code. v2 loads config in packages/core/src/config.ts. More importantly, setx and 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 with opencode 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:

    The bot will close this issue as a duplicate in 1 day unless you reply explaining how your problem is different.

  2. olscode commented on Oct 9, 2026

    @olscode
    Author

    Agreed — this is a duplicate of #33853. Thanks for the quick triage, and for the opencode service set env tip: 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 via opencode api get /api/config — and GitHub's remote MCP endpoint rejects it with HTTP 400: Authorization header is badly formatted. The token itself is valid (a direct probe with Bearer <token> returns HTTP 200), and the failure disappears after opencode 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.

  3. opencode-agent commented on Oct 9, 2026

    @opencode-agent
    Contributor

    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 formatted error. 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.

  4. opencode-agent commented on Oct 10, 2026

    @opencode-agent
    Contributor

    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 unclear HTTP 400. Until it's fixed, setting the token with opencode service set env GITHUB_PERSONAL_ACCESS_TOKEN <token> is the more reliable option. If you change an OS variable with setx, you need to run opencode service restart afterwards. Thanks again for the clear report. If something here turns out to be different from #33853, reply and we can reopen it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions