Skip to content

feat(mcp): add MCP server exposing worktree tools to external agents#60

Merged
liam-russell merged 7 commits into
mainfrom
claude/mcp-server
Jul 5, 2026
Merged

feat(mcp): add MCP server exposing worktree tools to external agents#60
liam-russell merged 7 commits into
mainfrom
claude/mcp-server

Conversation

@liam-russell

@liam-russell liam-russell commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Type

  • feat — new feature or user-visible capability

Summary

  • Added an MCP (Model Context Protocol) server so any MCP-capable agent can list worktrees, check worktree status, get workspace info, and (once a future permission setting enables it) create/remove worktrees for an open SproutGit workspace.
  • Added a Settings → MCP Server card per workspace: an enable toggle, an editable port (in case of conflicts), one-click config writers for Claude Code, Gemini CLI, Codex CLI, Cursor, and Kiro, and a manual copy-paste snippet for any other client.
  • Consolidated worktree create/remove into a single shared backend path (app/src/main/worktree-lifecycle.ts) used by both the UI and the MCP tools, so an agent-created worktree runs the same lifecycle hooks and provenance recording as a UI-created one instead of silently skipping them.

Why

Foundational groundwork for a later orchestration feature (an AI that splits a project into tasks, each becoming its own worktree + agent session), and independently useful today for any agent already working in a worktree.

Architecture (revised twice after review — see commit history for the full story)

  1. Transport: started as a Unix socket + spawned stdio bridge script. Switched to a per-workspace HTTP server (createMcpExpressApp() + StreamableHTTPServerTransport from the official MCP SDK) after review found the bridge script depended on system node being on PATH (never guaranteed, never verified in a packaged build). The HTTP server binds to 127.0.0.1 only, with SDK-provided Host header validation defeating DNS rebinding, plus a per-workspace bearer token (generated once, persisted) defending against other local processes — a loopback port, unlike a Unix socket, is reachable by any process regardless of which user owns it. Port is a stable per-workspace default with a Settings override for the rare real conflict, so a conflict doesn't require silently changing ports and invalidating already-written client configs. Evaluated mcp-framework (a community wrapper around the same SDK) and passed — its value-add (tool auto-discovery, pre-built OAuth/JWT/API-key auth) doesn't fit 5 tools needing one custom bearer check and N per-workspace server instances in one process.
  2. DRY: the MCP create_worktree/remove_worktree tools originally called @sproutgit/git directly, bypassing the hooks/provenance/terminal-cleanup orchestration the UI's worktree creation/deletion flow does. Consolidated both into worktree-lifecycle.ts, injected into the MCP tool context so both entry points call the identical function.

Screenshots / recordings

New "MCP Server" card in Settings (enable toggle, port field, per-client config buttons, manual snippet copy) — not captured here; see test plan for manual verification steps since no GUI target was available in this session.

Breaking changes

None. New IPC channels only; worktree:create/worktree:delete gained new optional fields but existing callers are unaffected.

Test plan

  • Unit tests cover: every MCP tool handler against a real temp git repo (packages/mcp-server/src/__tests__/tools.test.ts), the HTTP transport's auth/DNS-rebinding/protocol behavior (http-server.test.ts), the socket-to-port server lifecycle (mcp-bridge.test.ts), the full IPC handler stack against a real sqlite-backed workspace DB (ipc/__tests__/mcp.test.ts), every client's exact config output (mcp-config-writers.test.ts), the Settings UI component (McpSection.test.tsx), and the new shared worktree lifecycle including hook-call-order and hook-skip-when-no-window (worktree-lifecycle.test.ts).
  • pnpm typecheck, pnpm lint, pnpm test (all unit tests across every package + the full 13-file e2e suite, including the worktree create/delete flows that exercise the lifecycle consolidation most directly) all pass.
  • Not yet manually verified in a running Electron window (no GUI target available in this session) — to check by hand: open a workspace → Settings → MCP Server → enable → click "Claude Code" → confirm .mcp.json gets a sproutgit entry with type: "http", the right url, and an Authorization header; separately, confirm a hook configured for after_worktree_create still fires when creating a worktree from the UI.

Adds a new @sproutgit/mcp-server package with a first tool set
(list_worktrees, get_worktree_status, get_workspace_info, plus
create_worktree/remove_worktree gated behind a not-yet-built permission
setting) wrapping @sproutgit/git, served over a per-workspace Unix
socket/named pipe (no bearer token needed — OS file permissions scope
access to the local user). A zero-dependency stdio<->socket bridge
script lets stdio-only MCP clients (Claude Code, Gemini CLI, Codex CLI,
Kiro CLI) connect. Adds IPC to start/stop/query the server and toggle
it per workspace from Settings, plus auto-config writers for Claude
Code, Gemini CLI, Codex CLI, Cursor, and Kiro, with a manual
copy-paste fallback for any other client.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 5, 2026 02:25
Comment thread packages/mcp-server/bin/bridge.mjs Fixed

Copilot AI 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.

Pull request overview

Adds a new local MCP (Model Context Protocol) server integration to SproutGit so external MCP-capable agents can query workspace/worktree information via a local socket, with per-workspace enablement and one-click client configuration writing.

Changes:

  • Introduces a new @sproutgit/mcp-server package (tool handlers + socket transport + bridge script) and wires an MCP socket server into the Electron main process.
  • Adds new MCP IPC channels/types and a Settings UI section to enable/auto-start the server and generate client configs/snippets.
  • Packages the stdio↔socket bridge script into the app and updates lint/workspace dependencies accordingly.

Reviewed changes

Copilot reviewed 23 out of 25 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
pnpm-lock.yaml Adds lock entries for the new workspace package and MCP SDK dependencies.
packages/types/src/mcp.ts Defines shared MCP-related types (status, client IDs, config write result).
packages/types/src/ipc.ts Adds new MCP IPC channel constants and typed IPC mappings.
packages/types/src/index.ts Re-exports MCP types.
packages/mcp-server/tsconfig.json TypeScript config for the new MCP server package.
packages/mcp-server/src/tools.ts Implements MCP tool handlers and registers them with the MCP SDK.
packages/mcp-server/src/socket-transport.ts Adds newline-delimited JSON transport over net.Socket.
packages/mcp-server/src/server.ts Creates an McpServer instance and registers SproutGit tools.
packages/mcp-server/src/index.ts Exposes the new package’s public API surface.
packages/mcp-server/src/context.ts Defines the per-workspace MCP server context and mutating-tools gate.
packages/mcp-server/src/tests/tools.test.ts Unit tests for tool handlers against a temp git repo.
packages/mcp-server/src/tests/socket-transport.test.ts Unit tests for socket transport framing/parsing behavior.
packages/mcp-server/package.json Declares the new workspace package and its dependencies/scripts.
packages/mcp-server/bin/bridge.mjs Adds a stdio↔socket proxy script for MCP clients that only support stdio transport.
package.json Extends lint scope to include the new package.
app/src/renderer/settings/McpSection.tsx Adds the Settings UI section for enabling MCP and writing configs/copying snippets.
app/src/renderer/routes/workspace.tsx Auto-starts MCP server for enabled workspaces on workspace mount.
app/src/renderer/routes/settings.tsx Wires the new MCP section into the Settings page.
app/src/preload/index.ts Exposes MCP IPC APIs to the renderer via preload.
app/src/main/mcp-config-writers.ts Implements per-client config writers and manual snippet generation.
app/src/main/mcp-bridge.ts Implements per-workspace MCP socket server lifecycle + socket-path resolution.
app/src/main/ipc/workspace.ts Stops MCP server on workspace close.
app/src/main/ipc/mcp.ts Adds IPC handlers for MCP status/enable/start/config write/snippet.
app/src/main/index.ts Registers MCP IPC handlers and stops all MCP servers on app quit.
app/package.json Packages the bridge script via extraResources and adds the new workspace dependency.
Files not reviewed (1)
  • pnpm-lock.yaml: Generated file

Comment thread app/src/main/ipc/mcp.ts Outdated
Comment thread app/src/main/ipc/mcp.ts Outdated
Comment thread app/src/main/mcp-bridge.ts Outdated
Comment thread packages/mcp-server/src/tools.ts Outdated
Comment thread packages/mcp-server/src/tools.ts Outdated
Comment thread packages/mcp-server/src/tools.ts Outdated
liam-russell and others added 6 commits July 5, 2026 12:56
Replaces the Unix-socket/named-pipe transport and its stdio bridge
script with a per-workspace HTTP server bound to 127.0.0.1, using the
MCP SDK's own createMcpExpressApp() (Host-header validation defeats
DNS rebinding) plus a per-workspace bearer token (defends against
other local processes, since a loopback TCP port — unlike a Unix
socket — is reachable regardless of which user owns the process).

This removes the bridge script and its dependency on the user having
Node on PATH outside the Electron process, and removes the packaged-
app extraResources step that shipped it. Client configs now point
directly at http://127.0.0.1:<port>/mcp with an Authorization header,
verified against each client's actual HTTP-transport config schema
(Claude Code, Cursor, Kiro use url/headers; Gemini CLI uses httpUrl;
Codex CLI's config.toml uses url + http_headers).

The port is a stable per-workspace default (derived from the
workspace path so concurrently open workspaces don't collide) with a
per-workspace override persisted via a new Settings field, so a port
conflict with something else on the machine doesn't require silently
picking a different port and invalidating already-written client
configs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The previous commits only had unit tests for the mcp-server package
(tool handlers + HTTP transport). Everything gluing that together in
the actual app — mcp-bridge.ts's lifecycle, ipc/mcp.ts's token/port
persistence, mcp-config-writers.ts's exact per-client output, and the
McpSection settings UI — had zero test coverage. Adds real functional
tests for all four:

- mcp-bridge.test.ts: deriveDefaultPort determinism/range, a real HTTP
  server actually reachable at the returned port, start/stop
  idempotency, and the EADDRINUSE error path.
- mcp-config-writers.test.ts: asserts the literal JSON/TOML written for
  each of the 5 clients matches their researched schema exactly
  (previously only verified by reading docs, never executed), plus
  merge-preserves-existing-content and re-write-updates-in-place cases.
- ipc/__tests__/mcp.test.ts: the full IPC handler stack against a real
  sqlite-backed workspace DB and a real HTTP server — enable/disable,
  ensureStarted surviving a simulated app restart, port override +
  live restart, and token persistence.
- settings/__tests__/McpSection.test.tsx: renders the component and
  drives it via Testing Library — toggle, port save/reset, per-client
  config buttons, and the manual-snippet clipboard copy.

Also: mcp-bridge.ts now reads back the OS-assigned port via
server.address() instead of trusting the requested port verbatim,
which is both more correct in general and what let these tests use
port 0 to avoid collisions instead of picking fixed ports and hoping.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…x Windows CI

- app/src/main/ipc/mcp.ts: readState/writeState/deleteState never closed
  their sqlite handle, leaking a connection (and WAL lock) on every
  status/enable/port call. On Windows this held the file open long
  enough to make afterEach's directory cleanup fail with EBUSY, which
  is exactly what broke the Windows unit-test job. Close in a
  try/finally for all three.

- packages/mcp-server/src/tools.ts: remove_worktree's containment
  check now canonicalizes both paths (realpath) before isPathWithin(),
  mirroring packages/git/src/worktrees.ts's own isWithinCanonical() —
  isPathWithin() alone is pure string comparison and can be fooled by
  a symlinked worktree path. Also changed deleteBranch's default from
  true to false: with the old default, calling the tool with only
  worktreePath (the simplest possible call) always failed validation
  since branchName was still required whenever deleteBranch was true.

- app/src/main/__tests__/mcp-config-writers.test.ts: the Gemini/Codex
  writer tests override HOME to redirect node:os's homedir() into a
  temp dir, but Windows homedir() reads USERPROFILE, not HOME — this
  silently wrote to the real CI user's home directory instead of the
  test's temp dir, which is what actually failed Unit Tests
  (windows-latest). Set/restore both env vars.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ycle

The MCP create_worktree/remove_worktree tools were calling
@sproutgit/git's createManagedWorktree/deleteManagedWorktree directly,
while the UI's worktree:create/worktree:delete IPC handlers wrapped
the same calls with hook triggers (before/after_worktree_create,
before/after_worktree_remove), issue-ref provenance recording, and
terminal cleanup — all orchestrated ad hoc in the renderer across
several sequential IPC calls. An MCP-driven worktree change silently
skipped every bit of that, and the orchestration itself was duplicated
in spirit between "what the UI does" and "what the tool does".

Adds app/src/main/worktree-lifecycle.ts as the single place this
happens: createWorktreeWithHooks() and removeWorktreeWithHooks() run
the hooks, record provenance, and clean up terminals around the
actual git operation, plus (for remove) validate the target against
the repo's real registered worktrees. Both the renderer's
worktree:create/worktree:delete IPC handlers (git.ts) and the MCP
tools (via injected context.createWorktree/removeWorktree functions,
wired in mcp-bridge.ts) now call through these two functions — an
agent-created worktree behaves the same as a UI-created one.

Supporting extractions to make this possible without duplicating
their internals: hooks.ts's HOOK_RUN_TRIGGER handler body is now the
standalone exported runTriggerHooks(), and workspace.ts's
WORKTREE_SET_META handler body is now the standalone exported
setWorktreeMeta() — both callable directly from worktree-lifecycle.ts
without a client-side IPC round-trip.

Deliberately NOT consolidated: the renderer's own active-worktree
switching and its before/after_worktree_switch hooks around deleting
the currently-viewed worktree. That's UI navigation state with no MCP
equivalent (an agent has no "worktree currently in view"), so it stays
in workspace.tsx, passed into the shared functions only as an explicit
afterRemoveWorktreePath/initiatingWorktreePath parameter where it
affects hook context.

New tests: app/src/main/__tests__/worktree-lifecycle.test.ts covers
real worktree creation/removal, hook-skip-when-no-window, hook call
order when a window is open, issue-ref provenance recording, terminal
cleanup, and the registered-worktree validation. Existing tools.test.ts
updated for the injected context functions; git.test.ts/mcp-bridge.test.ts/
mcp.test.ts updated for the new getWindow parameter threaded through
registerGitHandlers/startMcpServer/registerMcpHandlers.

Full unit (all packages) and e2e (13/13 spec files, including the
worktree create/delete flows that exercise this most directly) suites
pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… cleanup

worktree-lifecycle.test.ts's "records issue-ref provenance" test calls
createWorktreeWithHooks() with an issueRef, which goes through
setWorktreeMeta() — and that uses workspace.ts's cached db connection,
which (by design) stays open for the process lifetime rather than
closing after each call, unlike the rest of that file's per-call
open/close pattern. The test's afterEach then tried to rmSync() the
temp workspace directory while that connection was still open, which
Windows refuses (EBUSY) even though POSIX allows it — exactly the
Windows-specific failure mode already fixed once in ipc/mcp.ts.

Extracted the close+evict logic already inside the WORKSPACE_CLOSE
handler into an exported closeWorkspaceDbCache(), which the handler
now calls too (no behavior change there), and had the test call it in
afterEach before removing the directory.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Unrelated to MCP, but hit while chasing down PR #60's Windows CI job:
convertToBareWithWorktree's tests do several real filesystem-heavy git
operations (init, commit, directory rename, worktree add/checkout,
sometimes a rollback on top of that) that are comfortably under
vitest's 5s default on macOS/Linux but measurably slower on Windows
CI runners, and intermittently timed out there under load (confirmed
transient by rerunning the same job unchanged, which then passed) —
likely aggravated by this PR's own new test files adding parallel load
across the monorepo's `pnpm test` run. Raised the suite's timeout to
20s rather than papering over it by retrying CI indefinitely.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@liam-russell liam-russell merged commit 4ead283 into main Jul 5, 2026
13 checks passed
@liam-russell liam-russell deleted the claude/mcp-server branch July 5, 2026 04:01
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.

2 participants