Skip to content

feat(settings): list the MCP servers Claude Code loads - #5227

Open
jash90 wants to merge 8 commits into
pingdotgg:mainfrom
jash90:feat/mcp-inventory
Open

feat(settings): list the MCP servers Claude Code loads#5227
jash90 wants to merge 8 commits into
pingdotgg:mainfrom
jash90:feat/mcp-inventory

Conversation

@jash90

@jash90 jash90 commented Aug 2, 2026

Copy link
Copy Markdown

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 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.
  • GET /api/mcp-servers on the environment HTTP API, with the matching client-runtime accessor. Per-environment, so local, relay, and tunnel all work the same way.
  • The page groups by computer and then by provider instance, with transport and scope badges, the redacted command line or address, and a click-to-copy config path.

Two things the read is deliberate about:

  • complete is load-bearing. It goes false whenever a config exists but cannot be parsed, and whenever no workspace cwd was supplied — the local and project scopes are unreadable then, so the list is knowingly partial. A later 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. 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.
  • Nothing secret crosses the wire. env is never serialised. Arguments and URLs go through a shape allowlist plus a credential-flag check — neither alone is enough: shape catches postgres://u:p@host/db and Authorization: Bearer … but would happily print a bare hunter2, while a flag blocklist catches --token secret but 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 --json subprocess; 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:

Several of these are real gaps we still want fixed; the base just moved out from under them. Once #2829 merges, please rebase onto main, port the change to the V2 equivalent, and reopen (or open a fresh PR).

#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:

  • Everything substantive lives in modules V2 keeps: apps/server/src/mcpServers/, packages/contracts/, packages/client-runtime/, apps/web/.
  • This PR does not touch ClaudeAdapter.ts or CodexAdapter.ts at 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

  • Entry points. Sidebar nav and the Settings search catalog. The section is anchored via searchableSetting, so a search hit scrolls to it. The command palette has no settings navigation today, so nothing to add there.
  • Providers. Claude only, for the reason above. Codex is a follow-up; ACP harnesses are not listable.
  • Clients. Web and desktop. Mobile has no Providers screen either, so MCP is out of scope there.
  • Connection modes. One collapsible group per environment, each issuing its own authenticated HTTP request; a response from a replaced connection is dropped rather than written over live data.
  • Reverse states. "Refresh all" re-reads from disk, so a config edited outside T3 Code shows up.
  • Docs. docs/user/mcp.md linked from docs/README.md, a docs/internals/providers.md section, and two glossary entries — docs/ had no mention of MCP before this.

Verification

vp test run on the touched files (83 passing across 14 files), vp lint and vp fmt on the changed scope, and typecheck on @t3tools/contracts, @t3tools/client-runtime, t3, and @t3tools/web. routeTree.gen.ts is generator output, not hand-edited.

Exercised against a real 25-server config in the desktop app, plus adversarial configs: __proto__/constructor as server names (no prototype pollution — Object.entries and a Map, never assignment), command as 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

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes — n/a, no motion

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 + workspace local) and approved .mcp.json project entries directly—avoiding claude mcp list network health checks. GET /api/mcp-servers aggregates per provider instance with complete / unreadable semantics 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, fetchEnvironmentMcpInventory in 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-code MCP 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

  • Adds a new /settings/mcp route that displays all MCP servers loaded by Claude Code, grouped by provider instance, with search, refresh, and config path copy actions.
  • Introduces GET /api/mcp-servers on the environment HTTP API, aggregating servers from user, local, and project (.mcp.json) scopes with approval filtering and scope precedence.
  • Redacts secrets from stdio command args and remote URLs before surfacing them in the UI; sanitizes and bounds server display names.
  • Tracks and surfaces unreadable config file warnings per instance when inventory reads are incomplete.
  • Adds shared contracts in mcpInventory.ts and a client-side fetch helper in state/mcp.ts.

Macroscope summarized 5b90083.

jash90 added 5 commits August 2, 2026 12:33
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
@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 45db39a8-b794-44a3-89d3-bfd276c2e7af

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Aug 2, 2026
Comment thread apps/server/src/mcpServers/McpServerInventory.ts
Comment thread apps/server/src/mcpServers/McpServerInventory.ts
Comment thread apps/server/src/mcpServers/ClaudeMcpConfig.ts Outdated
Comment thread docs/user/mcp.md Outdated
Comment thread apps/server/src/mcpServers/McpServerInventory.ts Outdated
Comment thread apps/server/src/mcpServers/McpServerInventory.ts Outdated
Comment thread apps/server/src/mcpServers/McpServerInventory.ts Outdated
Comment thread apps/server/src/mcpServers/McpServerInventory.ts Outdated
Comment thread apps/server/src/mcpServers/McpServerInventory.ts
@macroscopeapp

macroscopeapp Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: 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
Comment thread apps/web/src/components/settings/McpServersSettings.tsx Outdated
Comment thread apps/server/src/mcpServers/McpServerInventory.ts
jash90 added 2 commits August 2, 2026 13:32
…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

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ 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._/\\@_-]*$/;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 5b90083. Configure here.

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

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant