feat(settings): list the MCP servers Claude Code loads - #5227
Conversation
T3 Code injects itself into every session as an MCP server but has no vocabulary for the MCP servers the harness CLIs load, so nothing can report them. Adds `McpServerInventory` plus a `GET /api/mcp-servers` endpoint on the environment HTTP API, and the client-runtime accessor for it. The server list is a `ForwardCompatibleArray` because `transport` and `scope` are closed literal unions that will grow: a client one release behind should drop the rows it cannot decode, not fail the whole page. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S2pbgjTL7nueRLDrgKhp9T
There is no way to see which MCP servers an agent will actually load without opening a terminal on the machine running it — awkward, since that machine is often being driven remotely through this GUI. Adds `apps/server/src/mcpServers/`, which reads Claude Code's own config: user scope and workspace `local` scope from `.claude.json`, plus approved `.mcp.json` project servers. Reading the files directly avoids `claude mcp list`, which health-checks every server over the network and is far too slow for a settings page. Nothing is ever written back. Two things the read is careful about: - `complete` is false whenever a config exists but cannot be parsed, and whenever no workspace cwd was supplied — the `local` and `project` scopes are then unreadable, so the list is knowingly partial. A future caller that replaces the CLI's own resolution with `--strict-mcp-config` must refuse to act on an incomplete list or it will silently drop working servers. - The `projects` lookup resolves the real path first. Claude Code keys that map by resolved path, so a cwd arriving through a symlink (`/tmp` on macOS, a linked worktree) would otherwise miss in silence. Secrets stop at the boundary: `env` is never serialised, and arguments and URLs are rendered through a shape allowlist plus a credential-flag check. Neither signal is sufficient alone — shape catches `postgres://u:p@host/db` but would print a bare `hunter2`; a flag blocklist catches `--token secret` but misses everything positional. Only Claude is covered. Codex needs a `codex mcp list --json` subprocess, and Cursor, Grok, and OpenCode run over ACP, which never reports the servers the agent loads from its own config. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S2pbgjTL7nueRLDrgKhp9T
Surfaces the inventory: one collapsible group per connected computer, one per Claude provider inside it, with transport and scope badges, the redacted command line or address, and a click-to-copy config path. Search filters by name, detail, harness, or path. The page is read-only. It reports what Claude Code will load; changing that still belongs to the harness. Registered in `settingsSearch.ts` rather than the nav list directly, since the sidebar derives its items from `SETTINGS_SECTION_LABELS` — so the section is reachable from both the sidebar and the settings search, and `SETTINGS_SECTION_ICONS` being a total record keeps the two from drifting apart. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S2pbgjTL7nueRLDrgKhp9T
`docs/` had no mention of MCP at all, and this adds a user-visible concept, so both audiences need an entry point. `docs/user/mcp.md` covers the page, which harnesses are and are not listed and why, what "config unreadable" means, and how to actually remove a server (through Claude Code). `docs/internals/providers.md` gets the maintainer view, including the two invariants that are easy to break later: `complete` is load-bearing, and nothing secret crosses the wire. Two glossary entries disambiguate the collision between `apps/server/src/mcp/` (T3 Code hosting an MCP server) and `apps/server/src/mcpServers/` (the user's own). Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S2pbgjTL7nueRLDrgKhp9T
…vers" Adversarial testing turned up a lying empty state. When `.claude.json` exists but cannot be parsed — malformed JSON, or larger than the read guard — discovery recovers zero entries, so the per-row "config unreadable" marker has nothing to attach to and the page says "No MCP servers configured for Claude Code on this computer." The config is right there; we just could not read it. Reports unreadable configs on their own channel in the inventory rather than as a property of rows that may not exist, and the empty state now says the servers are unknown instead of claiming there are none. Also bounds and sanitises server names. They come from a file we do not control, and the same pass produced entries carrying a U+202E override (which reorders how a row reads on screen) and a 5000-character name. React escapes markup, so this is about what a name can look like, not injection. Checked while I was in there, all clean: `__proto__` / `constructor` / `prototype` as server names do not pollute anything (`Object.entries` plus a `Map`, never assignment); a `command` that is an array or number, a null entry, and an unknown transport all degrade instead of throwing; 5000 levels of nesting parse in 2ms; the 8 MB guard catches a 12 MB config. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S2pbgjTL7nueRLDrgKhp9T
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
ApprovabilityVerdict: Needs human review 1 blocking correctness issue found. This PR introduces a substantial new feature: a settings page listing MCP servers Claude Code loads, with new API endpoint, contracts, and UI components across 1805 new lines. New features of this scope warrant human review even when well-implemented and read-only. You can customize Macroscope's approvability policy. Learn more. |
Review found three ways a credential could reach the inventory response: a flag value starting with `-` skipped redaction, `command` was echoed verbatim, and a remote URL kept its path. All three now go through the same policy the arguments already used. An unreadable `.mcp.json` also reported `.claude.json` as the broken file and marked every valid row "config unreadable". Read now names each file that failed, and rows carry no status — every row came out of a config that parsed. A `.claude.json` that exists as a directory is unreadable, not absent. Claude-Session: https://claude.ai/code/session_01Wwhp8pRacpU9VBcpSwXfgE
…while searching Always redacting the argument after a credential flag consumed a following flag as that value, disarming the next one: `--token --api-key hunter2` rendered the secret verbatim. Either reading of the middle argument leaves `hunter2` a value, so redaction re-arms instead of clearing. The unreadable-config warning was hidden while a search was active, which is when it matters most — a config the search could not scan is exactly what a bare "no match" hides. Claude-Session: https://claude.ai/code/session_01Wwhp8pRacpU9VBcpSwXfgE
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 5b90083. Configure here.
| * token containing `+`, `$`, or `:` — fails and is redacted. | ||
| */ | ||
| const SAFE_ARGUMENT_PATTERN = | ||
| /^(?:-{0,2}[A-Za-z0-9]|[.~]?[\\/]|[A-Za-z]:[\\/])[A-Za-z0-9._/\\@_-]*$/; |
There was a problem hiding this comment.
Scoped packages wrongly redacted
Medium Severity
SAFE_ARGUMENT_PATTERN is documented as allowing package specifiers, but a leading @ never matches, so scoped npm packages such as @modelcontextprotocol/server-filesystem are replaced with … in stdio detail. That is the usual shape of Claude MCP configs, so many rows lose the argument that actually identifies the server.
Reviewed by Cursor Bugbot for commit 5b90083. Configure here.


What Changed
Adds Settings → MCP: a read-only listing of the MCP servers Claude Code loads, on every connected computer, so you can see them without opening a terminal or a config file.
apps/server/src/mcpServers/reads Claude Code's own config — user scope and workspacelocalscope from.claude.json, plus approved.mcp.jsonproject servers. Reading the files directly avoidsclaude mcp list, which health-checks every server over the network and is far too slow for a settings page. Nothing is ever written back.GET /api/mcp-serverson the environment HTTP API, with the matching client-runtime accessor. Per-environment, so local, relay, and tunnel all work the same way.Two things the read is deliberate about:
completeis load-bearing. It goes false whenever a config exists but cannot be parsed, and whenever no workspacecwdwas supplied — thelocalandprojectscopes are unreadable then, so the list is knowingly partial. A later caller that replaces the CLI's own resolution with--strict-mcp-configmust refuse to act on an incomplete list, or it will silently drop working servers. An unreadable config is reported on its own channel rather than as a property of rows, because it usually yields no rows at all and the empty state would otherwise claim there are no servers.envis never serialised. Arguments and URLs go through a shape allowlist plus a credential-flag check — neither alone is enough: shape catchespostgres://u:p@host/dbandAuthorization: Bearer …but would happily print a barehunter2, while a flag blocklist catches--token secretbut misses everything positional. Server names are stripped of control characters and length-bounded, since a U+202E override reorders how a row reads on screen.Only Claude is covered. Codex needs a
codex mcp list --jsonsubprocess; Cursor, Grok, and OpenCode run over ACP, which can add a server to a session but never reports the ones the agent loads from its own config, so listing them would be misleading.Why
This is a rebased, re-scoped port of #4634, which @juliusmarminge closed on 2026-07-30:
#2829 is still open and currently conflicted, and the gap is still open too — #277. T3 Code injects itself into every session as an MCP server but gives users no view of the ones their own harness loads. So this targets today's
main, and is packaged to survive the V2 migration rather than to dodge it:apps/server/src/mcpServers/,packages/contracts/,packages/client-runtime/,apps/web/.ClaudeAdapter.tsorCodexAdapter.tsat all — the files V2 replaces. It is pure discovery plus a settings page; nothing in the session launch path changes.Happy to re-cut against V2 the moment #2829 lands. Turning servers off is deliberately not in this PR — that is where the adapter changes live, and it is worth its own review.
Per-surface decisions
searchableSetting, so a search hit scrolls to it. The command palette has no settings navigation today, so nothing to add there.docs/user/mcp.mdlinked fromdocs/README.md, adocs/internals/providers.mdsection, and two glossary entries —docs/had no mention of MCP before this.Verification
vp test runon the touched files (83 passing across 14 files),vp lintandvp fmton the changed scope, andtypecheckon@t3tools/contracts,@t3tools/client-runtime,t3, and@t3tools/web.routeTree.gen.tsis generator output, not hand-edited.Exercised against a real 25-server config in the desktop app, plus adversarial configs:
__proto__/constructoras server names (no prototype pollution —Object.entriesand aMap, never assignment),commandas an array or number, null entries, unknown transports, 5000 levels of nesting, and a 12 MB config against the 8 MB read guard. All degrade rather than throw; the empty-state fix above came out of that pass.UI Changes
Checklist
Claude Opus 5 (1M context), via Claude Code.
Note
Medium Risk
Introduces a new authenticated environment HTTP endpoint that reads local Claude config and returns derived server metadata; credential redaction is load-bearing for remote settings access, but session launch and harness config writes are untouched.
Overview
Adds Settings → MCP, a read-only view of MCP servers Claude Code would load on each connected environment, without changing session launch or harness adapters.
Backend: New
apps/server/src/mcpServers/parses.claude.json(user + workspacelocal) and approved.mcp.jsonproject entries directly—avoidingclaude mcp listnetwork health checks.GET /api/mcp-serversaggregates per provider instance withcomplete/unreadablesemantics when configs are missing, malformed, oversized (>8MB), or workspace scopes cannot be read. Command lines and URLs are redacted before serialization; server names are sanitized for display.Clients: Shared contracts in
mcpInventory.ts,fetchEnvironmentMcpInventoryin client-runtime, and a settings page with per-environment fetch, search, refresh, harness grouping, and warnings when inventory is partial.Scope: Claude provider instances only; Codex and ACP harnesses are not listed yet. T3 Code’s built-in
t3-codeMCP server is unchanged and not shown on this page.Reviewed by Cursor Bugbot for commit 5b90083. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Add Settings → MCP page listing Claude Code MCP servers per connected environment
/settings/mcproute that displays all MCP servers loaded by Claude Code, grouped by provider instance, with search, refresh, and config path copy actions.GET /api/mcp-serverson the environment HTTP API, aggregating servers from user, local, and project (.mcp.json) scopes with approval filtering and scope precedence.mcpInventory.tsand a client-side fetch helper instate/mcp.ts.Macroscope summarized 5b90083.