Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
393 changes: 389 additions & 4 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ members = [
"git-workon-fixture",
"git-workon-review",
"git-workon-annotations",
"git-workon-mcp",
]

[workspace.package]
Expand Down
77 changes: 77 additions & 0 deletions docs/adr/040-mcp-suite-crate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# 040: `git-workon-mcp`, One MCP Crate for the Whole Suite

Status: accepted (2026-09-03)

## Context

ADR-039 created `git-workon-annotations` as a lib and named `git-workon-mcp`, the MCP server
that serves the store to a coding agent over stdio, as its second consumer. It left the
server's crate home to this ADR. Two homes were on the table. The first was a feature-gated
`[[bin]]` inside `git-workon-annotations`, reached only with `--features mcp`: the fewest
workspace members, and it dodges the publish blocker the same way any `publish = false`
crate does. The second was a crate of its own. The binary and its `git workon mcp` surface
are suite-scoped, while the annotations crate is one domain: every tool it serves today is
an annotation tool, so the first home costs nothing until a worktree or stack tool needs
`git-workon-lib`, which `git-workon-annotations` has no reason to depend on. A feature-gated
bin also hides its test from a bare `cargo test --workspace`, which is what CI runs.
`docs/rfc/agent-integration.md` (Model C, "Phase 3: MCP Server (New Crate)") already
specified `git-workon-mcp` as its own crate depending on `git-workon-lib`. I took the second
home. Same eight tools as the annotations design, no worktree tools yet.

## Decision

**Own crate, bin-only, `publish = false` + `[package.metadata.dist] dist = false`, reached by
PATH dispatch, never a `Cmd::Mcp`.** The actual publish blocker this decision resolves: a
published crate cannot depend on a `publish = false` crate, and `git-workon-annotations` stays
`publish = false` (its schema and API are still settling). Building `git-workon-mcp` as its
own crate and having `git-workon`'s existing external-subcommand dispatch
(`git-workon/src/dispatch.rs`) exec it from `PATH` for `git workon mcp` keeps the user-visible
surface without the published `git-workon` binary ever taking a compile-time dependency on an
unpublished crate. A built-in `Cmd::Mcp` would look simpler today, but it would permanently
shadow the external dispatch and re-couple the crates the moment either is ready to publish,
so the dispatch route is taken now.

**Tools grouped by domain module, annotations first.** `src/tools/annotations.rs` holds the
eight annotation `#[tool]` fns, their argument structs, and their helpers (repo
discovery, store access, anchor building, JSON encoding). `src/tools/mod.rs` re-exports it and
is where a worktree or stack module would land next; `docs/rfc/agent-integration.md` Model C
is still the plan for those, not yet built here. `src/server.rs` owns `WorkonServer` and its
`ServerHandler` impl; `src/main.rs` only wires stdio and calls `serve`. rmcp's `#[tool_router]`
macro generates its router-building associated function without a visibility keyword by
default, so calling it from a sibling module (`server.rs` constructing a `WorkonServer` whose
router impl lives in `tools/annotations.rs`) needs `#[tool_router(vis = "pub(crate)")]`, the
one macro accommodation this split required. Everything else about the module boundary is
ordinary Rust visibility.

**Transport: rmcp 3.2, minimal features, confined to this one crate.** `default-features =
false, features = ["server", "macros", "transport-io"]`, run on a current-thread tokio
runtime; this pulls in serde derive, schemars, and tokio for this crate only, not for
`git-workon-review` or `git-workon-annotations`. rmcp is the official SDK, tracks MCP protocol
revisions we don't want to hand-roll (a handshake-less transport change landed 2026-07-28),
and its MSRV (1.88) already matches the workspace floor ADR-033 set. Known cost: quarterly
breaking majors, and an open upstream issue where `#[tool_router]` can silently register zero
tools: `tool_router_serves_exactly_eight_tools` asserts the count for exactly that reason.

## Consequences

- The stdio round-trip test (`tests/stdio.rs`) runs under a bare `cargo test --workspace`,
which is what CI runs; a feature-gated bin would have needed a flag CI does not pass.
- `git-workon-annotations` stays a lib with no bin target and no `rmcp`/`tokio`/`git2`
dependency: `cargo tree -p git-workon-annotations -e normal` carries none of them.
- `git-workon-mcp`'s own distribution (a second binary through cargo-dist and the homebrew
formula patch step) is still the open question ADR-033 flagged for a second binary
generally; this ADR doesn't resolve it.
- The worktree and stack tool set `docs/rfc/agent-integration.md` Model C describes is still
planned, not built: this ADR ships the annotation tools only.

## References

- `docs/rfc/workon-review.md`: Crate layout and Comments decision rows, and the Agent-loop
bullet, updated alongside this ADR
- [ADR-039](039-review-annotations-substrate.md): the annotation store this crate's tools
read and write, and the publish-blocker reasoning this ADR carries forward
- [ADR-033](033-review-crate-workspace-placement.md): the `publish = false` / `dist = false`
scaffold posture this crate adopts, and the second-binary distribution gap it already
flagged
- `docs/rfc/agent-integration.md`: Model C, Phase 3, the worktree/stack tool set this crate
is shaped to grow into next
2 changes: 1 addition & 1 deletion docs/rfc/agent-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,7 @@ For high-concurrency scenarios (many parallel agents all calling `git workon new

### Phase 3: MCP Server (New Crate)

6. **`git-workon-mcp` crate** — Standalone binary implementing the MCP stdio protocol with five tools: `worktree_list`, `worktree_create`, `worktree_find`, `worktree_remove`, `worktree_create_from_pr`. Depends on `workon` (git-workon-lib). Tool responses use the WorktreeDescriptor JSON schema. Errors use the structured error protocol from Phase 2.
6. **`git-workon-mcp` crate** — Standalone binary implementing the MCP stdio protocol with five tools: `worktree_list`, `worktree_create`, `worktree_find`, `worktree_remove`, `worktree_create_from_pr`. Depends on `workon` (git-workon-lib). Tool responses use the WorktreeDescriptor JSON schema. Errors use the structured error protocol from Phase 2. Status (2026-09-03, ADR-040): the crate exists, reached via `git workon mcp`, and today serves `git-workon-annotations`'s eight comment/walkthrough tools (ADR-039); this worktree tool set is still the plan, not yet built.

---

Expand Down
2 changes: 1 addition & 1 deletion docs/rfc/workon-review.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ The remaining roadmap is resequenced around the tool being **the author's own ev

- **Conflict resolution** *(stretch)*. Resolve merge/rebase conflicts in the SBS view. Large surface; may not make v1.

- **Agent loop** *(the eventual north star)*. **Design locked 2026-09-02 (ADR-039); the three open forks below are resolved, not just re-flagged.** One substrate serves both review comments and an integrated `/explain-diff`-style walkthrough: `AnnotationKind::{Comment, TourStop, Chapter}` in one sqlite table at `<commondir>/workon-review/annotations.db`, anchored by content-hash context (target line + 3 lines each way, re-resolved per load: exact match, then a scored windowed scan, then whitespace-tolerant, else `Orphaned` — never silently wrong) rather than the originally-proposed `(changeset_id, path, side, lnum)` key. **Comment-store home:** its own crate, `git-workon-annotations` (`publish = false`, serde-free, no git2) — `git workon mcp` is the second consumer the "no separate core crate" rule was waiting on, so that condition is now met and the rule no longer applies to it. **MCP crate/transport:** `rmcp` 3.2 (minimal features, current-thread tokio), not hand-rolled JSON-RPC, served from its own `git-workon-mcp` crate (ADR-040) and reached from `git-workon` via the existing PATH-dispatch mechanism (not a built-in `Cmd::Mcp`, and not a `git-workon-review mcp` subcommand) — this is what lets the published `git-workon` binary depend on the feature without depending on the unpublished annotations crate. Landing as five stacked slices (crate scaffold → TUI read → TUI authoring → prose/walkthrough polish → MCP crate); each lands alone. — DONE (2026-09-02): four of five shipped — `annot-crate` (the store + resolver crate), `annot-read` (gutter markers, view/reply overlay, tour stepping), `annot-write` (the multi-line annotation editor, create/reply/resolve), `annot-prose` (walkthrough chapters wrapped into the summary panel, a `stop i/n` tour-progress indicator in the diff header, and `--tour <name>` to open straight into a walkthrough). `mcp-crate` lands next.
- **Agent loop** *(the eventual north star)*. **Design locked 2026-09-02 (ADR-039); the three open forks below are resolved, not just re-flagged.** One substrate serves both review comments and an integrated `/explain-diff`-style walkthrough: `AnnotationKind::{Comment, TourStop, Chapter}` in one sqlite table at `<commondir>/workon-review/annotations.db`, anchored by content-hash context (target line + 3 lines each way, re-resolved per load: exact match, then a scored windowed scan, then whitespace-tolerant, else `Orphaned` — never silently wrong) rather than the originally-proposed `(changeset_id, path, side, lnum)` key. **Comment-store home:** its own crate, `git-workon-annotations` (`publish = false`, serde-free, no git2) — `git workon mcp` is the second consumer the "no separate core crate" rule was waiting on, so that condition is now met and the rule no longer applies to it. **MCP crate/transport:** `rmcp` 3.2 (minimal features, current-thread tokio), not hand-rolled JSON-RPC, served from its own `git-workon-mcp` crate (ADR-040) and reached from `git-workon` via the existing PATH-dispatch mechanism (not a built-in `Cmd::Mcp`, and not a `git-workon-review mcp` subcommand) — this is what lets the published `git-workon` binary depend on the feature without depending on the unpublished annotations crate. Landing as five stacked slices (crate scaffold → TUI read → TUI authoring → prose/walkthrough polish → MCP crate); each lands alone. — DONE (2026-09-03): all five shipped — `annot-crate` (the store + resolver crate), `annot-read` (gutter markers, view/reply overlay, tour stepping), `annot-write` (the multi-line annotation editor, create/reply/resolve), `annot-prose` (walkthrough chapters wrapped into the summary panel, a `stop i/n` tour-progress indicator in the diff header, and `--tour <name>` to open straight into a walkthrough), `mcp-crate` (`git-workon-mcp`'s eight tools — list/get/post/reply/update/resolve/delete/`walkthrough_put` — now its own crate per ADR-040, reached via `git workon mcp`'s PATH dispatch). No longer deferred behind the daily-driver work — it landed alongside it. A third consumer joined 2026-09-02: the user's /redline skill (section-by-section document walks) keeps its walk state in the store when `git workon mcp` is reachable — the stop manifest is a tour, closing a stop is a resolve plus a reply carrying the decision, standing rules are the chapter — which is what motivated the `annotation_update` tool.

## Orchestration notes

Expand Down
49 changes: 49 additions & 0 deletions git-workon-mcp/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
[package]
authors.workspace = true
categories = ["command-line-utilities", "development-tools"]
description = "MCP server exposing git-workon's review annotations to a coding agent"
edition.workspace = true
homepage.workspace = true
keywords = ["git", "review", "mcp", "workon"]
license.workspace = true
name = "git-workon-mcp"
repository.workspace = true
rust-version.workspace = true
version = "0.1.0"
include = [
"src/**/*",
"Cargo.toml",
"LICENSE*",
]
# Not yet published: reached only via git-workon's external-subcommand PATH dispatch, and
# its deps (rmcp, tokio) are pinned to a fast-moving SDK. Follow the ADR-033 posture:
# flipping to publish is a deferred sub-decision, not an oversight to fix later.
publish = false

[dependencies]
git-workon-annotations.workspace = true
miette.workspace = true
git2.workspace = true
serde_json.workspace = true

rmcp = { version = "3.2", default-features = false, features = [
"server",
"macros",
"transport-io",
] }
tokio = { version = "1", features = ["rt", "macros"] }
# Derive macros expand to literal `serde::`/`schemars::` paths, so rmcp's re-exports
# (`rmcp::serde`, `rmcp::schemars`) don't satisfy `#[derive(Deserialize, JsonSchema)]`
# without a `#[serde(crate = ...)]`/`#[schemars(crate = ...)]` on every type. Depending on
# these directly is simpler; the versions are already in Cargo.lock via rmcp, so this adds
# no new resolution, just a name for what's already there.
serde = { version = "1", features = ["derive"] }
schemars = { version = "1" }

[package.metadata.dist]
# Redundant with publish = false today; load-bearing if the publish flip ever lands (see
# ADR-033's posture, adopted here) so cargo-dist doesn't silently start shipping it.
dist = false

[dev-dependencies]
tempfile = "3"
22 changes: 22 additions & 0 deletions git-workon-mcp/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//! `git-workon-mcp`: MCP server for the git-workon suite (stdio transport), reached as
//! `git workon mcp` via `git-workon`'s external-subcommand PATH dispatch. This binary is
//! never a dependency of the published `git-workon` crate — see ADR-040's publish-blocker
//! reasoning (carried over from ADR-039, which first identified it).
//!
//! Tool routes live under [`tools`], grouped by domain; [`server::WorkonServer`] wires them
//! into the `ServerHandler` rmcp dispatches against.

mod server;
mod tools;

use rmcp::transport::io::stdio;
use rmcp::ServiceExt;

use server::WorkonServer;

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let service = WorkonServer::new().serve(stdio()).await?;
service.waiting().await?;
Ok(())
}
47 changes: 47 additions & 0 deletions git-workon-mcp/src/server.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//! [`WorkonServer`]: the MCP `ServerHandler` for the whole suite. Tool routes are added by
//! domain module under [`crate::tools`] (today: `annotations`; future: worktrees, stack —
//! see `docs/rfc/agent-integration.md` Model C); this file only owns the router field and
//! `get_info`.

use rmcp::handler::server::router::tool::ToolRouter;
use rmcp::model::{Implementation, ProtocolVersion, ServerCapabilities, ServerInfo};
use rmcp::{tool_handler, ServerHandler};

#[derive(Debug, Clone)]
pub struct WorkonServer {
pub(crate) tool_router: ToolRouter<Self>,
}

impl WorkonServer {
pub fn new() -> Self {
Self {
tool_router: Self::tool_router(),
}
}
}

#[tool_handler(router = self.tool_router)]
impl ServerHandler for WorkonServer {
fn get_info(&self) -> ServerInfo {
// `ServerInfo` and `Implementation` are `#[non_exhaustive]` in rmcp 3.2, so they
// can't be built with a struct literal; mutate defaults instead.
let mut server_info = Implementation::from_build_env();
// `Implementation::from_build_env()` reads `env!("CARGO_CRATE_NAME")` at the call
// site inside rmcp itself, so it would report "rmcp" here, not this binary — name
// it explicitly instead.
server_info.name = "git-workon-mcp".to_string();
server_info.version = env!("CARGO_PKG_VERSION").to_string();

let mut info = ServerInfo::default();
info.protocol_version = ProtocolVersion::default();
info.capabilities = ServerCapabilities::builder().enable_tools().build();
info.server_info = server_info;
info.instructions = Some(
"Read and write git-workon-review's annotation store: line comments, \
replies, and explain-diff-style walkthroughs. All tools take an optional \
`repo_path`, defaulting to discovery from the current directory."
.to_string(),
);
info
}
}
Loading
Loading