Problem
docs/mcp/configuration.md documents env placeholders in MCP auth config (client_id: ${LINEAR_OAUTH_CLIENT_ID}), but nothing expands them: LoadForgeConfig → ParseForgeConfig 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
Problem
docs/mcp/configuration.mddocuments env placeholders in MCP auth config (client_id: ${LINEAR_OAUTH_CLIENT_ID}), but nothing expands them:LoadForgeConfig→ParseForgeConfigis a plainyaml.Unmarshal, and the onlyos.Expandanywhere 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
urlliteral + placeholders for the connection config: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(viaos.Expandagainst the process env, same semantics asexpandEgressDomains) 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)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.MCPDomainscomputes 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.Expanddefault (empty string) — which then correctly failsresolveOAuthConfig's explicit-match and falls to the fail-closed refresh path rather than dialing a literal${…}URL.Acceptance
resolveOAuthConfigexplicit branch, verbatim endpoints, no discovery/DCR.${…}dial.security.MCPDomainssees expanded values.docs/mcp/configuration.mdactually works.🤖 Generated with Claude Code