Skip to content

claude: Phase 10 — workbench client tools via in-process MCP server#317685

Merged
TylerLeonhardt merged 6 commits into
mainfrom
tyleonha/claude-phase10-client-tools
May 21, 2026
Merged

claude: Phase 10 — workbench client tools via in-process MCP server#317685
TylerLeonhardt merged 6 commits into
mainfrom
tyleonha/claude-phase10-client-tools

Conversation

@TylerLeonhardt
Copy link
Copy Markdown
Member

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-session SessionClientToolsModel. A SessionClientToolsDiff observes the model and dirties on any change.
  • On materialize / rebind, claudeMaterializer._buildClientMcpServers consumes the diff and builds an in-process MCP server whose tool handlers park on a PendingRequestRegistry<CallToolResult> keyed by SDK tool_use_id.
  • IAgent.onClientToolCallComplete(session, toolCallId, result) resolves the parked deferred after converting protocol ToolCallResult → MCP CallToolResult.
  • On a setClientTools mid-session, session.send detects toolDiff.hasDifference and yield-restarts via the existing pipeline rematerializer hook.

Notable refactors bundled in this PR

  • New clientTools/ subfolder under node/claude/ for MCP factory, result converter, model+diff, and JSON-Schema → zod converter (zod is now a direct dep).
  • Pipeline / router / mapper own a single cached clientId (setter, no threaded closures).
  • Per-method IClaudeAgentSdkService shim with a compile-time AssertBindingsMatchSdk mapped type to catch upstream SDK drift.
  • readClaudePermissionMode helper extracted so session and materializer both read live permission mode without threading callbacks through the agent.

Council review fixes

  • setClientTools lost during the materialize commit gap — re-sync provisional.clientTools/clientId onto the live session before publishing to _sessions.
  • setClientTools dropped during resume bootstrap — register the resume provisional in _provisionalSessions for the duration so the call has somewhere to land.
  • Rebind-startup failure consumed the diff without restarting — wrap the rematerializer closure in try/catch and call session.toolDiff.markDirty() on failure.

Tests

  • 1802 agentHost tests passing locally.
  • 3 new race-/rebind-failure tests in claudeAgent.test.ts:
    • setClientTools landing during the materialize gap is re-synced into the live session
    • setClientTools landing during the resume bootstrap gap is re-synced into the live session
    • rebind failure leaves the client-tool diff dirty so the next send retries
  • New unit suites for claudeJsonSchemaToZod, claudeClientToolMcpServer, claudeClientToolResult, claudeSessionClientToolsModel under test/node/clientTools/.

E2E verification

Launched Code OSS with these changes, restored a prior claude-code session via the updated _resumeSession path. The chat displays the persisted client__openBrowserPage + client__screenshotPage MCP 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

  • Phase 11 customizations (setClientCustomizations / setCustomizationEnabled still throw).
  • External MCP servers via Query.setMcpServers (Phase 10 only covers in-process tools).

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).
Copilot AI review requested due to automatic review settings May 21, 2026 03:20
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.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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

Comment thread src/vs/platform/agentHost/node/claude/claudeMapSessionEvents.ts
Comment thread src/vs/platform/agentHost/common/pendingRequestRegistry.ts
Comment thread test/unit/electron/renderer.html
Comment thread src/vs/platform/agentHost/node/claude/claudeAgentSdkService.ts Outdated
- 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.
@TylerLeonhardt TylerLeonhardt marked this pull request as ready for review May 21, 2026 05:41
@TylerLeonhardt TylerLeonhardt merged commit 54eb46a into main May 21, 2026
25 checks passed
@TylerLeonhardt TylerLeonhardt deleted the tyleonha/claude-phase10-client-tools branch May 21, 2026 13:27
@vs-code-engineering vs-code-engineering Bot added this to the 1.122.0 milestone May 21, 2026
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.

3 participants