-
Notifications
You must be signed in to change notification settings - Fork 0
feat(mcp-client): scaffold built-in MCP client contribution #25
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
fb2f544
feat(mcp-client): scaffold built-in MCP client contribution
kanywst 08165da
docs: fix relative path to design doc (5 levels, not 6)
kanywst 7c82c69
docs(mcp-client): trust prompt for RCE, secret refs, tool namespacing…
kanywst e800853
docs(mcp-client): full-config fingerprint, no SSE trust bypass, refus…
kanywst 052ad90
docs(mcp-client): canonical-JSON fingerprint, dash/EncodedCommand, re…
kanywst b4b6414
docs(mcp-client): reserve 'zeus' server name, clarify fingerprint is …
kanywst File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| # MCP client (built-in) | ||
|
|
||
| Zeus consumes MCP servers listed in `.zeus/mcp.json` and exposes their tools to the editor's AI features. This is the **client** half of the MCP-first design; the **server** half lives in `feat/mcp-server`. | ||
|
|
||
| ## Where this lives | ||
|
|
||
| `src/vs/workbench/contrib/mcpClient/` — a workbench contribution split across the `common/`, `browser/`, and `node/` layers per vscode's architecture: | ||
|
|
||
| - `common/` — config schema, types, the workspace-side `McpToolAggregator` that hands a unified registry to the agent runtime | ||
| - `browser/` — UI surfaces (status bar entries, error notifications, the trust-prompt for newly-added stdio entries) | ||
| - `node/` — process spawning. `stdio` MCP servers must be launched from the main / node side; `browser/` cannot spawn child processes in vscode's sandbox. SSE connections can live in either layer | ||
|
|
||
| Behaviour: | ||
|
|
||
| - Reads `.zeus/mcp.json` from the workspace root | ||
| - Spawns stdio MCP servers (`node/`) as subprocesses, or opens SSE connections | ||
| - Aggregates the tool list and exposes it to the agent runtime | ||
| - Reloads on `.zeus/mcp.json` change | ||
|
|
||
| This is intentionally a built-in contribution rather than a VS Code extension. MCP server lifecycles are too important to let users disable accidentally; we want them tied to the workspace lifecycle. | ||
|
|
||
| ## Trust model — RCE risk | ||
|
|
||
| `.zeus/mcp.json` lists *commands to execute*. Anyone with commit rights can add an arbitrary command, and a colleague who pulls and opens the workspace would silently spawn it. That's a real RCE vector. | ||
|
|
||
| Mitigations: | ||
|
|
||
| - **Trust prompt** — the first time a workspace is opened with a non-empty `mcp.json`, *or* whenever a server entry is added **or modified** (any change to `command`, `args`, `env`, `url`, or `transport`), Zeus blocks startup of those servers and shows a per-server confirmation pane (similar to VS Code's "Restricted Mode" workspace trust). The fingerprint stored in per-user (not in-git) state is `sha256(canonical_json(serverConfig))`, where `canonical_json` (a) sorts object keys lexicographically at every depth, (b) resolves relative `command:` paths against the workspace root to absolute, (c) drops trailing whitespace in env values, and (d) uses LF line endings. The fingerprint is intentionally **per-user / per-machine**, not cross-platform — that's the point. Two machines that would launch a different binary for the same `command:` value (e.g. `/usr/local/bin/node` vs `/opt/homebrew/bin/node`) deserve to re-consent, because the actual code being executed differs. Trust travels with the user, never with the repository. Within a single machine, equal-meaning configs (key order, JSON whitespace, line-ending variance) hash identically, so a benign reformat doesn't re-trigger the prompt. | ||
| - **Inherit Workspace Trust** — if the workspace is in Restricted Mode, refuse to spawn any server (stdio *and* SSE). A remote SSE endpoint never executes local code itself, but the tools it exposes can still cause file writes, shell calls, or prompt-injection via the agent, so the trust prompt covers it too. | ||
| - **Refuse shell wrappers, not just `bash -c`** — `command:` must resolve to an actual executable path; argument vectors must go through `args:`. Reject `command:` values whose basename matches any shell (`sh`, `dash`, `bash`, `zsh`, `ksh`, `fish`, `pwsh`, `cmd`, `cmd.exe`, `powershell`, `powershell.exe`) when paired with an execution flag in `args:`: `-c` / `/c` / `-Command` / `-EncodedCommand`. (PowerShell's `-EncodedCommand` is a common obfuscation vector and gets the same treatment as `-Command`.) The point is to make the executable + argv structurally visible, not to chase shell-specific bypasses. | ||
|
|
||
| ## Secret storage | ||
|
|
||
| `mcp.json` lives in git. We only allow `${env:NAME}` and `${secret:<store>:NAME}` references in `env` blocks for credentials: | ||
|
|
||
| - `${env:NAME}` resolves at spawn time from the user's environment | ||
| - `${secret:keychain:NAME}` reads from `vscode.SecretStorage` (per-user, OS-keychain backed) | ||
| - Plain string values are accepted only for non-secret config. The loader **refuses to start** a server (rather than just warning) when a field name matches the heuristic list (`*_TOKEN`, `*_KEY`, `*_SECRET`, `PASSWORD`) and the value is not a `${env:...}` / `${secret:...}` reference. The user sees a per-server error in the status bar with a "Move to keychain" quick-fix that creates the secret and rewrites the reference for them. | ||
| - A separate high-entropy heuristic (≥ 32 chars, base64 / hex-ish) on **any** plain `env` value triggers the same refuse-and-prompt path, so secrets that don't match the field-name list still get caught. | ||
|
|
||
| Never write secret values back into the file. The loader is read-only against `mcp.json`. | ||
|
|
||
| ## Why not [VS Code's `vscode.lm` tool API](https://code.visualstudio.com/api/extension-guides/tools)? | ||
|
|
||
| We want the MCP-first stance to be honest. VS Code's `vscode.lm.registerTool` is a fine API but it's vscode-specific. By going through `@modelcontextprotocol/sdk` directly, the same `.zeus/mcp.json` works in: | ||
|
|
||
| - Claude Code CLI (already MCP-native) | ||
| - ChatGPT desktop / Codex (MCP support shipping) | ||
| - Future agents (MCP is an open spec) | ||
|
|
||
| VS Code extensions can still register `lm` tools — those continue to work — but Zeus's first-class story is MCP. | ||
|
|
||
| ## Loader | ||
|
|
||
| ```text | ||
| .zeus/mcp.json | ||
| ↓ | ||
| McpConfigLoader (watches file, validates schema) | ||
| ↓ | ||
| McpClientRegistry (one McpClient per server entry) | ||
| ↓ | ||
| McpToolAggregator (combined tool list, dispatches calls) | ||
| ↓ | ||
| IAgentRuntime (Agent SDK PR consumes this) | ||
| ``` | ||
|
|
||
| ## Sub-PRs needed before this can land | ||
|
|
||
| 1. `feat/zeus-conventions` (`.zeus/mcp.json` schema) — PR #23 | ||
| 2. This PR — scaffold + design | ||
| 3. Follow-up — `@modelcontextprotocol/sdk` dep + real implementation | ||
|
|
||
| ## Acceptance criteria (real impl) | ||
|
|
||
| - [ ] Loads `.zeus/mcp.json` at workbench startup | ||
| - [ ] Spawns each `stdio` server as a child process | ||
| - [ ] Connects to each `sse` server with bearer auth, where the token **must** come from a `${secret:keychain:...}` (or `${env:...}`) reference in the server entry — hard-coded bearer tokens in `mcp.json` are refused by the same secret-storage rule above | ||
| - [ ] Aggregates all tool definitions into a single registry | ||
| - [ ] Reloads on file change without restarting unaffected servers | ||
| - [ ] Surfaces server connection errors in the status bar | ||
| - [ ] Zeus's own MCP **server** half publishes its tools under the `zeus__` prefix (double underscore, e.g. `zeus__buffer_read`, `zeus__editor_open`) | ||
| - [ ] **`zeus` is a reserved server name** in `.zeus/mcp.json`. The loader refuses any third-party entry whose `name` (after lowercasing) is `zeus`, with a clear error message pointing at the offending entry. This is what actually prevents collision with the internal `zeus__` namespace — a double-underscore separator alone isn't enough, because a third-party server *literally* named `zeus` would still get mapped to `zeus__<tool>` by the namespacing rule and collide | ||
| - [ ] **All** third-party tools are always exposed as `<server-name>__<tool-name>` in the aggregated registry, regardless of whether another server has registered the same short name. Always-namespacing (rather than namespacing-on-collision) means the tool name the agent sees is stable: it does not change when a new server is added later. UI surfaces still show the short tool name with the server name as secondary text. The underlying call dispatches to the originally-named tool on the right server. | ||
|
|
||
| ## Status | ||
|
|
||
| Scaffold only. Slot reserved at `src/vs/workbench/contrib/mcpClient/`. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| # `mcpClient` contribution | ||
|
|
||
| Slot for the built-in MCP client. Design lives at [`docs/zeus-mcp-client.md`](../../../../../docs/zeus-mcp-client.md). | ||
|
|
||
| When the real implementation lands, this directory will contain (VS Code layering: `common/` is platform-agnostic, `browser/` is renderer, `node/` is the Node-only half that can spawn subprocesses): | ||
|
|
||
| - `common/mcpTypes.ts` — shared types | ||
| - `common/mcpToolAggregator.ts` — unified, namespaced tool registry | ||
| - `browser/mcpClient.contribution.ts` — workbench registration + status bar | ||
| - `browser/mcpConfigLoader.ts` — `.zeus/mcp.json` watcher + schema validation | ||
| - `browser/mcpTrustPrompt.ts` — user confirmation before spawning new stdio servers | ||
| - `node/mcpStdioRegistry.ts` — per-server stdio child processes (cannot live in `browser/`) | ||
| - `node/mcpSseRegistry.ts` — SSE connections (can also be browser; placed here for symmetry) | ||
| - `test/node/*.test.ts` — unit tests for the spawn paths | ||
|
|
||
| Until then, this README is the placeholder so other PRs can reference the path without merge conflicts. |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The heuristic list for secret detection (
*_TOKEN,*_KEY,*_SECRET,PASSWORD) is somewhat narrow. Many services use variations likeAPI_KEY,ACCESS_TOKEN,PRIVATE_KEY, orAUTH_TOKEN. Consider expanding this list or using a more comprehensive set of common secret naming patterns to improve the effectiveness of the security check and prevent accidental commits of sensitive data.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The name-based heuristic (
*_TOKEN/*_KEY/*_SECRET/PASSWORD) is intentionally narrow because it must be high-precision to avoid false positives. Variants likeGITHUB_PAT/PRIVATE_PEM/GH_TOKENare caught by the high-entropy fallback on every plainenvvalue (≥32 chars, base64/hex-ish) — see the second bullet of 'Secret storage'. That's the layered approach: precise name match catches obvious cases with a clear error message, entropy heuristic catches the rest.