Skip to content

feat(mcp): manage MCP servers per provider and per agent - #1144

Merged
Juliusolsson05 merged 11 commits into
mainfrom
feat/user-mcp-servers
Sep 23, 2026
Merged

Juliusolsson05 merged 11 commits into
mainfrom
feat/user-mcp-servers

Conversation

@Juliusolsson05

@Juliusolsson05 Juliusolsson05 commented Sep 23, 2026 •

Copy link
Copy Markdown
Owner

Problem

Agent Code could not add, configure or switch off a third-party MCP server. A user who wanted, say, the Beeper Desktop MCP server had to hand-edit ~/.claude.json / ~/.codex/config.toml outside the app. The server then landed on every agent of that provider, could not be turned off for one agent, and never appeared in Agent Code. Built-in MCP choices were also spread across eight Settings rows and ten palette commands, and each command reloaded the agent on its own.

What this does

One MCP interface for every MCP server

  • Settings → MCP (new category): Agent Code's built-in servers (TLDR, Goal, Goal Loop, Orchestration, Transcripts, Agent Management, AI Workspace, Workflows) and the user's own servers share one grid, with one column per provider enabled in Settings → Providers (feat(usage): provider enablement, Grok and z.ai usage readers, scaled usage modal #1126).
    • Every checkbox means the same thing: "new agents of this provider get it".
    • Built-in defaults are now per provider. A pre-existing flat list is copied to every provider, so upgrading changes nobody's agents.
    • The External operator MCP row moves here unchanged.
  • Add MCP Server…: paste the config from a server's README. mcpServers blocks, the VS Code servers/inputs form, a bare map, or a single entry all work.
    • Any stdio, Streamable HTTP or SSE server can be added. SSE is Claude-only, because Codex has no SSE transport.
    • Every env and header value is moved into an encrypted secret, referenced as ${input:id} (VS Code's syntax). README placeholders such as YOUR_TOKEN_HERE are recognized and left unset.
  • Agent MCP Servers…: one picker for the focused agent covering built-in and user servers. Changes are staged and applied with one reload.
  • Servers the CLIs already load from ~/.claude.json / config.toml are listed read-only, with every value withheld. Copy in imports one as a managed server.
  • A server that cannot attach produces a toast naming the server and the reason.

Agents can manage it too (off by default)

  • A new mcp_servers built-in capability gives agents mcp_servers_list/add/update/remove/set_secret, through the same service Settings uses.
  • An agent proposes, the user approves. Anything an agent adds, or points at a different URL, command, arguments or environment, is saved switched off and flagged "waiting for your review". Only the user can turn a server on.
  • No tool returns a secret or a raw entry: agents see redacted summaries. Every agent change raises a toast.
  • Orchestration children cannot be given mcp_servers or root_management unless the parent holds them. This closes a path around off-by-default; the same hole existed for root_management before this PR.

Delivery at launch, no provider config writes

  • Main owns mcp-servers.json (mode 0600) and the safeStorage secret blobs. The renderer only sends a pane's explicit per-agent choices, as user:<id> keys in the existing override map.
  • At launch, main applies:
    • the master switch and per-provider defaults;
    • secret readiness and transport support;
    • Codex name collisions;
    • Claude's enterprise MCP policy.
  • It reports what actually attached (userMcpServerIds), the same way builtInMcpDomains is reported.
  • Claude: resolved user entries go into the same private 0600 --mcp-config file as the built-in server, never into Claude's environment.
    • Claude's environment is inherited by every MCP child and by the model's Bash tool.
    • The file is deleted once Claude is ready, on stop and on rollback, and swept at startup after a crash.
  • Codex: --config mcp_servers.<name>.*. Headers go through env_http_headers, and stdio secrets through env_vars. No secret reaches argv.
    • Every carried secret is excluded from Codex's model shells via shell_environment_policy. It uses whichever form (filters or legacy exclude) the user's own config across all layers already uses, because mixing the two fails Codex's config load.
  • Secret redirection: changing anything about where a server connects (any literal in the entry) forgets its stored secrets.
  • A bad or unready server never fails an agent launch. It is dropped with a reason (user-mcp-unavailable toast, journal entry).

Design decisions

The full evidence and rationale are in docs/superpowers/specs/2026-09-22-user-mcp-servers-design.md; see the Revision 2 section for the unified interface.

  • Launch-time injection instead of writing provider config.
    • No race with live Claude processes rewriting ~/.claude.json.
    • No codex mcp add table rewrite.
    • A per-agent subset comes free.
  • No aggregator/proxy (MetaMCP, 1MCP…). One would break each CLI's native OAuth, rename tools and add a runtime we would have to supervise.
  • No new dependency. add-mcp's per-client transform idea was copied, not depended on, because its Codex transform is wrong.
  • OAuth is delegated to the CLIs. The edit dialog shows a copyable codex mcp login <name> -c … command and Claude /mcp guidance. Generated secret variable names are deterministic, so Claude's OAuth token key (name|hash(type,url,headers)) stays stable across launches.
  • User choices live in the existing override map under user: keys. Every path that already carries per-agent MCP choices (spawn, reload, recovery, undo, provider switch, duplicate, control API) carries them without being re-threaded.

Review

Two adversarial rounds, each with 2 Claude, 1 Grok and 2 OpenCode reviewers; findings files are in the branch's git-ignored tmp/review/. Every valid finding was fixed. Round 1 was mostly secret delivery and editor state; round 2 was mostly the agent tools' authority. Accepted residuals:

  • Codex hooks and notify inherit Codex's full environment, including carried secrets. Closing that needs a file-fed wrapper (follow-up).
  • Downgrading and re-upgrading resets the per-provider built-in defaults.

Verification

  • npx tsc -b: clean. npm run test:contract and check:keybindings: OK.
  • Full npm test: 5,756 passed. The 10 failures in the first run were tests pinning the retired commands and the old flat default shape; all are fixed here and re-run green.
  • New tests use Beeper's official published snippets as fixtures:
    • importer and validation;
    • translators, including "no token in argv" and "no token in the config file";
    • service resolution rules;
    • SessionManager (dropped servers never fail a launch; a resolver failure is tolerated);
    • the Settings grid, the dialog and the per-agent picker.
  • Real binary: codex mcp list --json (codex-cli 0.155.1) parsed the generated overrides for an HTTP server (env_http_headers, auth_status: bearer_token) and a stdio mcp-remote server (env_vars) exactly as intended.
  • Not verified against a live Claude process. claude mcp list ignores --mcp-config, and a real check needs a model call. The file uses the same shape as the built-in server's working private config, and was checked against Claude's zod schema and ${VAR} expansion in vendor/claude-code-src.
  • The app was not launched (repo rule). The UI is covered by renderer tests only.

Known limitations / follow-ups

  • Changes need an agent reload; neither TUI reloads MCP config live.
  • User servers are Claude + Codex only. OpenCode and Grok show "not supported yet".
  • Claude's persistent disabledMcpServers (set via /mcp in a repo) also applies by name to injected servers.
  • Secrets reach the agent's process environment, so the agent's own tools can read them. This is the same as the built-in bearer, and the UI says so.
  • Follow-ups: Add from MCP Registry, OpenCode/Grok user servers, workflow subagents, project-scope native listing, Host user-authored MCP servers as Agent Code extensions #244 hosted servers.

Fixes #1143
Refs #244

🤖 Generated with Claude Code

Juliusolsson05 and others added 11 commits September 22, 2026 19:11
Agent Code has no way to add or toggle third-party MCP servers. The
design settles on launch-time injection through the existing built-in
MCP path rather than writing provider config, the de facto mcpServers
shape with ${input:id} secret references, and per-provider defaults
plus per-agent overrides. The evidence behind each decision is recorded
in the spec so implementation does not re-derive it.

Refs #1143
Refs #244

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ervers

The user approved a unified interface: built-in and user servers share
one Settings grid with per-provider columns, and one staged per-agent
modal replaces the individual Enable * MCP commands. Recorded as a
Revision 2 section so the superseded decisions stay visible.

Refs #1143

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The model stores servers in the de facto mcpServers entry shape so a
README snippet round-trips losslessly, with VS Code style ${input:id}
secret references that may only appear where they can travel through
the environment. The translators turn resolved servers into Claude's
private --mcp-config entries and Codex -c overrides without putting a
secret on argv or on disk, and drop a server rather than fail a launch.

Refs #1143

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Main owns the server document, the encrypted secrets and the decision of
what attaches to each launch: the renderer only contributes the pane's
explicit per-agent choices. A requested server that cannot attach is
reported with a reason and never fails the launch, and the servers each
backend actually launched with are reported on its snapshot the same way
built-in MCP domains are. Also lists the servers each CLI loads from its
own user-scope config, read-only and with every value withheld.

Refs #1143

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Settings gains an MCP category whose grid lists Agent Code's built-in
servers and the user's own servers with one column per enabled provider,
so every checkbox means the same thing: new agents of that provider get
it. Built-in defaults become per provider; a pre-existing flat list is
copied to every provider so upgrading changes nobody's agents.

Per-agent choices move to one staged Agent MCP Servers picker that
applies with a single reload and keeps Root Management behind its
confirmation. It retires the nine per-capability MCP commands and Use
Global MCP Settings, and keeps their rule that removing Goal Loop's
tools ends a running loop first. User-server choices ride the existing
override map under user:<id> keys, so every path that already carries
per-agent MCP choices carries them too. Adding a server is paste-first,
with tokens lifted into encrypted secrets.

Refs #1143

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…aults

Adds the MCP feature's control reference, README and architecture notes,
and moves the remaining tests that pinned the retired commands or the flat
default list onto the new contracts.

Refs #1143

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Adds an mcp_servers built-in capability (off by default, configurable in
Settings → MCP) whose tools list, add, update and remove the user's MCP
servers and set secrets through the same service Settings uses. A skill
pointing at mcp-servers.json would not work: main owns that document
behind one write queue and secrets are not in it. No tool can read a
secret back, and every agent-made change is announced to the user and
journaled, because this list decides what runs in every future agent.

Refs #1143

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Stops a running goal loop only on the reload that actually runs, so a
declined Root Management confirmation no longer leaves the loop dead
while the agent keeps its tools. Refuses MCP writes while the document
cannot be read, instead of replacing the user's servers with an empty
list. Surfaces the Codex protected-env rule and user-scope name
collisions in Settings and the per-agent picker, and stops counting user
servers towards a pending reload, so Apply can no longer stay armed for
a server that is dropped at every launch. Documents the downgrade
limitation of per-provider defaults.

Refs #1143

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude user-server secrets now resolve into the private per-launch config
file instead of Claude's environment, which every child process and model
shell inherits; Codex keeps env delivery but excludes every carried
secret from its model shells, in the form the user's own config already
uses so the two never mix. Changing where a server connects forgets its
secrets, so neither an edit nor an agent can redirect a stored token.

Also: dotted env/header names are rejected before they can fail a Codex
launch; generated secret variables are collision-free; proxy, CA and
loader variables are protected from Codex;  placeholders are not
stored as secrets; summaries and the native list no longer carry
credential-looking args or URL parts; a crashed run's private configs are
swept at startup; a failed save rolls back. The editor refuses to save
over a server changed elsewhere, an emptied secret field no longer clears
the secret, the sign-in command is shell-quoted, a multi-server paste can
be retried, and the per-agent picker can always reload.

Refs #1143

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…und 2

An agent can now only propose MCP servers: anything it adds, or points at a
different URL, command, arguments or environment, is saved switched off and
flagged for review, and only the user can turn a server on. Orchestration
children can no longer be handed mcp_servers or root_management unless the
parent holds them, closing a path around the off-by-default gate.

Any literal change to an entry now counts as a new destination and forgets
its secrets (a literal NODE_OPTIONS could run attacker code with the token),
secret values containing ${ are refused because Claude would expand them,
and the private Claude config file is removed once Claude is ready.

Codex: shell exclusions are deduplicated case-insensitively (Codex rejects
case-only duplicates and would fail the launch), a secret never silently
replaces the user's own variable, and project config is read from every
ancestor .codex/config.toml the way Codex loads it.

Also: the goal-loop stop runs inside the reload after its checks; saves
persist before touching secret blobs; the add dialog keeps typed secrets and
saved servers across a re-parse; the edit dialog warns before a URL change
forgets secrets; agents get redacted summaries, not raw entries; Claude name
collisions are shown; per-session maps no longer leak.

Refs #1143

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… is set

Refs #1143

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Juliusolsson05
Juliusolsson05 merged commit f11308f into main Sep 23, 2026
2 checks passed
@Juliusolsson05
Juliusolsson05 deleted the feat/user-mcp-servers branch September 23, 2026 07:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(mcp): manage user MCP servers per provider and per agent

1 participant