Skip to content

Expand ${VAR} env placeholders in MCP config fields at load #321

Description

@initializ-mk

Problem

docs/mcp/configuration.md documents env placeholders in MCP auth config (client_id: ${LINEAR_OAUTH_CLIENT_ID}), but nothing expands them: LoadForgeConfigParseForgeConfig is a plain yaml.Unmarshal, and the only os.Expand anywhere is egress domains at runtime (runner.go expandEgressDomains). A ${…} in an MCP field survives as a literal string and breaks at runtime.

This is now load-bearing for the platform: as of initializ/agent-builder#80, the platform's MCP tool registry resolves + pins a server's OAuth chain at ADMISSION (RFC 9728 → 8414) and mints one DCR client (RFC 7591) against the platform callback. The generated forge.yaml deliberately bakes only the SHAPE — server url literal + placeholders for the connection config:

mcp:
  servers:
    - name: linear
      url: https://mcp.linear.app/mcp
      transport: http
      auth:
        type: oauth
        client_id: ${MCP_LINEAR_CLIENT_ID}
        authorize_url: ${MCP_LINEAR_AUTHORIZE_URL}
        token_url: ${MCP_LINEAR_TOKEN_URL}
        scopes: ["${MCP_LINEAR_SCOPES}"]
      tools: { allow: [create_issue] }

The VALUES are injected at deploy into the agent's env (from the registry entry), so rotating the pinned client is a redeploy, not a rebuild — and with all three of client_id/authorize_url/token_url present post-expansion, resolveOAuthConfig (#320) lands on the EXPLICIT branch: no runtime discovery, no runtime DCR, exactly the managed-mode contract.

Requested change

Expand ${VAR} / $VAR (via os.Expand against the process env, same semantics as expandEgressDomains) in:

  • MCPServer.URL (optional — the platform keeps it literal, but symmetry with docs)
  • MCPAuth.AuthorizeURL, MCPAuth.TokenURL, MCPAuth.ClientID, MCPAuth.TokenEnv? (no — TokenEnv is a NAME by design, exclude it)
  • each element of MCPAuth.Scopes, splitting a post-expansion value on whitespace (so one ${…_SCOPES} env var carrying "read write" becomes two scopes)

Ordering matters: expansion must run BEFORE security.MCPDomains computes the egress allowlist, or the authorize/token hosts are parsed from the unexpanded placeholder strings (the platform compensates today by merging AS hosts into the envelope allowlist at build, but forge standalone should be correct on its own).

An unset variable should expand per os.Expand default (empty string) — which then correctly fails resolveOAuthConfig's explicit-match and falls to the fail-closed refresh path rather than dialing a literal ${…} URL.

Acceptance

  • forge.yaml above + env set → resolveOAuthConfig explicit branch, verbatim endpoints, no discovery/DCR.
  • Same yaml, env unset → behaves as unconfigured oauth (discovery at login only), never a literal-${…} dial.
  • security.MCPDomains sees expanded values.
  • The documented example in docs/mcp/configuration.md actually works.

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions