claude: Phase 10 — workbench client tools via in-process MCP server#317685
Merged
Conversation
Surface workbench-registered tools (IAgent.setClientTools) to the Claude SDK through an in-process MCP server (createSdkMcpServer + Options.mcpServers['client']). Tool calls park on a per-session PendingRequestRegistry keyed by SDK tool_use_id; IAgent.onClientToolCallComplete resolves the parked deferred with the protocol-to-MCP result conversion. Plumbing: new clientTools/ subfolder (MCP server factory, result converter, session model+diff, JSON Schema -> zod converter); pipeline / router / mapper own a single cached clientId (setter, no threaded closures); materializer rebinds the SDK on tool-set diff via the rematerializer hook; per-method IClaudeAgentSdkService shim with compile-time AssertBindingsMatchSdk to catch SDK drift; zod added as a direct dep; permission-mode resolution extracted to readClaudePermissionMode helper. Bug fixes (council review): setClientTools race during materialize commit gap (re-sync at publish time); setClientTools dropped during resume bootstrap (register the resume provisional); rebind-startup failure consumed the diff without restarting (markDirty in the rematerializer catch). Tests: 1833 agentHost tests passing (3 new for the race-/rebind-failure paths).
The live stream mapper already strips the in-process MCP server prefix so the workbench resolves client tools by their unprefixed name. The replay mapper (resumed sessions) and subagent inner-tool mapper did not, so persisted / subagent client-tool calls rendered as 'Run MCP tool client__<name>' instead of the tool's rich invocation message.
Contributor
There was a problem hiding this comment.
Pull request overview
This PR implements Claude “Phase 10” client-provided tools by bridging workbench-registered tools into the Claude SDK via an in-process MCP server (Options.mcpServers.client). It introduces per-session client-tool state/diffing, JSON-Schema→zod conversion for tool inputs, MCP server/tool wiring, and completion routing back into parked tool calls, with broad unit/integration coverage updates.
Changes:
- Add a per-session client-tools model + diff, and use it to drive yield-restart rematerialization when client tools change.
- Implement MCP server construction for client tools (schema conversion + handler parking) and tool-result conversion back to MCP
CallToolResult. - Update Claude materialize/resume/rebind plumbing, session permission-mode reads, and expand tests + dependencies (
zod, MCP SDK types).
Show a summary per file
| File | Description |
|---|---|
| test/unit/electron/renderer.html | Skip exporting reserved-word identifiers from CommonJS modules when generating ESM blobs (avoids parse failures with deps like zod). |
| src/vs/platform/agentHost/test/node/clientTools/claudeSessionClientToolsModel.test.ts | Adds unit tests for the per-session client-tools diff behavior (dirty/consume/equality). |
| src/vs/platform/agentHost/test/node/clientTools/claudeJsonSchemaToZod.test.ts | Adds unit tests for JSON Schema → zod raw-shape conversion. |
| src/vs/platform/agentHost/test/node/clientTools/claudeClientToolResult.test.ts | Adds unit tests for protocol tool-result → MCP result conversion. |
| src/vs/platform/agentHost/test/node/clientTools/claudeClientToolMcpServer.test.ts | Adds unit tests for MCP server/tool registration and handler parking behavior. |
| src/vs/platform/agentHost/test/node/claudeSubagentResolver.test.ts | Updates test fake SDK service to include new MCP-related methods. |
| src/vs/platform/agentHost/test/node/claudeSdkPipeline.test.ts | Adjusts pipeline harness construction for new optional constructor parameter(s). |
| src/vs/platform/agentHost/test/node/claudeSdkMessageRouter.test.ts | Adjusts router harness construction for new optional constructor parameter(s). |
| src/vs/platform/agentHost/test/node/claudeAgent.test.ts | Integrates Phase 10 behavior in end-to-end agent tests and updates fakes for MCP server/tool wiring. |
| src/vs/platform/agentHost/test/node/claudeAgent.integrationTest.ts | Updates integration test fake SDK service for new MCP-related methods. |
| src/vs/platform/agentHost/test/common/pendingRequestRegistry.test.ts | Adds coverage for new rejectAll behavior. |
| src/vs/platform/agentHost/node/claude/roadmap.md | Marks Phase 10 as DONE. |
| src/vs/platform/agentHost/node/claude/phase10-plan.md | Adds/updates the detailed Phase 10 plan and implementation notes. |
| src/vs/platform/agentHost/node/claude/clientTools/claudeSessionClientToolsModel.ts | Introduces observable client-tools state model and a dirty-bit diff helper. |
| src/vs/platform/agentHost/node/claude/clientTools/claudeJsonSchemaToZod.ts | Implements JSON Schema → zod raw-shape conversion for tool input schemas. |
| src/vs/platform/agentHost/node/claude/clientTools/claudeClientToolResult.ts | Implements protocol ToolCallResult → MCP CallToolResult conversion. |
| src/vs/platform/agentHost/node/claude/clientTools/claudeClientToolMcpServer.ts | Builds the in-process MCP server and tool handlers for client tools. |
| src/vs/platform/agentHost/node/claude/claudeSessionPermissionMode.ts | Extracts helper for reading/narrowing the live session permission mode. |
| src/vs/platform/agentHost/node/claude/claudeSdkPipeline.ts | Adds restart rebind wrapper and clientId setter plumbing into the router. |
| src/vs/platform/agentHost/node/claude/claudeSdkMessageRouter.ts | Caches clientId and threads it into stream event mapping. |
| src/vs/platform/agentHost/node/claude/claudeMaterializer.ts | Centralizes materialize context, builds MCP servers from diffs, and wires rematerializer with failure dirtying. |
| src/vs/platform/agentHost/node/claude/claudeMapSessionEvents.ts | Normalizes MCP-prefixed tool names and stamps toolClientId for client tools in emitted actions. |
| src/vs/platform/agentHost/node/claude/claudeAgentSession.ts | Adds client-tool diff + pending registry, rebind-on-diff behavior, and completion routing for parked tool calls. |
| src/vs/platform/agentHost/node/claude/claudeAgentSdkService.ts | Extends the SDK shim to expose MCP server/tool creation and adds compile-time drift detection. |
| src/vs/platform/agentHost/node/claude/claudeAgent.ts | Implements setClientTools / onClientToolCallComplete and integrates tool resync on materialize/resume gaps. |
| src/vs/platform/agentHost/common/pendingRequestRegistry.ts | Adds register and rejectAll to support parked MCP calls and cancellation semantics. |
| remote/package.json | Adds zod dependency to remote package dependencies. |
| remote/package-lock.json | Locks zod dependency for remote package. |
| package.json | Adds zod dependency at the root. |
| package-lock.json | Locks zod dependency at the root. |
| eslint.config.js | Allows zod and MCP SDK type imports in agentHost contexts. |
Copilot's findings
Files not reviewed (1)
- remote/package-lock.json: Language not supported
- Files reviewed: 31/33 changed files
- Comments generated: 4
- claudeMapSessionEvents: gate subagent-spawn detection on !isClientTool so a workbench tool named 'Task' / 'Agent' can't impersonate the SDK's spawn tools. - pendingRequestRegistry.register: reject a pre-existing deferred with CancellationError before overwriting, so duplicate-key registrations don't leak the prior awaiter. - renderer.html: hoist RESERVED_EXPORT_NAMES + VALID_IDENTIFIER regex out of asRequireBlobUri so they aren't reallocated per module in the test bootstrap import map. - claudeAgentSdkService: drop the export on _assertBindingsMatchSdk; the constant is only used for the compile-time type check, so it doesn't need to be on the module's public surface.
…ched
zod's CJS exports include reserved-word keys (enum, default, function, ...) which the renderer's import-map blob generator turns into invalid ESM ('export const enum = ...'). Earlier commits patched renderer.html to skip those keys; that file isn't Phase 10's business so this reverts the renderer change and instead loads zod at runtime via require(). The bogus blob is still generated but never imported, so it never gets parsed.
…o handle reserved-word CJS exports The renderer test bootstrap generates an ESM blob per dep that re-exports every key (`export const X = _mod[X]`). zod's CJS exports include reserved-word keys (enum, default, function) which produce un-parseable ESM. Skip those keys in the blob generator — generic fix that any future package with reserved-word exports would also need.
zhichli
approved these changes
May 21, 2026
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Summary
Phase 10 of the Claude agent host: surface workbench-registered tools to the Claude SDK as an in-process MCP server (
createSdkMcpServer+Options.mcpServers['client']). Matches the Copilot agent's client-tool behavior end-to-end.Closes the Phase 10 row on roadmap.md. Full plan and implementation notes in phase10-plan.md.
How it works
IAgent.setClientTools(session, clientId, tools)writes into a per-sessionSessionClientToolsModel. ASessionClientToolsDiffobserves the model and dirties on any change.claudeMaterializer._buildClientMcpServersconsumes the diff and builds an in-process MCP server whose tool handlers park on aPendingRequestRegistry<CallToolResult>keyed by SDKtool_use_id.IAgent.onClientToolCallComplete(session, toolCallId, result)resolves the parked deferred after converting protocolToolCallResult→ MCPCallToolResult.setClientToolsmid-session,session.senddetectstoolDiff.hasDifferenceand yield-restarts via the existing pipeline rematerializer hook.Notable refactors bundled in this PR
clientTools/subfolder undernode/claude/for MCP factory, result converter, model+diff, and JSON-Schema → zod converter (zod is now a direct dep).clientId(setter, no threaded closures).IClaudeAgentSdkServiceshim with a compile-timeAssertBindingsMatchSdkmapped type to catch upstream SDK drift.readClaudePermissionModehelper extracted so session and materializer both read live permission mode without threading callbacks through the agent.Council review fixes
setClientToolslost during the materialize commit gap — re-syncprovisional.clientTools/clientIdonto the live session before publishing to_sessions.setClientToolsdropped during resume bootstrap — register the resume provisional in_provisionalSessionsfor the duration so the call has somewhere to land.session.toolDiff.markDirty()on failure.Tests
setClientTools landing during the materialize gap is re-synced into the live sessionsetClientTools landing during the resume bootstrap gap is re-synced into the live sessionrebind failure leaves the client-tool diff dirty so the next send retriesclaudeJsonSchemaToZod,claudeClientToolMcpServer,claudeClientToolResult,claudeSessionClientToolsModelunder test/node/clientTools/.E2E verification
Launched Code OSS with these changes, restored a prior
claude-codesession via the updated_resumeSessionpath. The chat displays the persistedclient__openBrowserPage+client__screenshotPageMCP tool roundtrips from that session — the model successfully consumed the screenshot result, confirming the full bridge works against this codebase. No errors in the agent host log.Out of scope
setClientCustomizations/setCustomizationEnabledstill throw).Query.setMcpServers(Phase 10 only covers in-process tools).