feat(mcp): add MCP server exposing worktree tools to external agents#60
Merged
Conversation
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>
Contributor
There was a problem hiding this comment.
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-serverpackage (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
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>
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.
Type
Summary
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)
createMcpExpressApp()+StreamableHTTPServerTransportfrom the official MCP SDK) after review found the bridge script depended on systemnodebeing on PATH (never guaranteed, never verified in a packaged build). The HTTP server binds to127.0.0.1only, with SDK-providedHostheader 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. Evaluatedmcp-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.create_worktree/remove_worktreetools originally called@sproutgit/gitdirectly, bypassing the hooks/provenance/terminal-cleanup orchestration the UI's worktree creation/deletion flow does. Consolidated both intoworktree-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:deletegained new optional fields but existing callers are unaffected.Test plan
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..mcp.jsongets asproutgitentry withtype: "http", the righturl, and anAuthorizationheader; separately, confirm a hook configured forafter_worktree_createstill fires when creating a worktree from the UI.