diff --git a/README.md b/README.md index d38ff69..55f260e 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,21 @@ # go-gerrit-mcp -An MCP (Model Context Protocol) server exposing Gerrit code review operations as capability-gated tools. It lets an -AI agent search and read changes, publish review comments, vote, and drive change-state transitions — with a safety -posture that makes every write capability an explicit operator opt-in. +An MCP (Model Context Protocol) server that exposes Gerrit code review operations as capability-gated tools. An AI +agent can search and read changes, publish review comments, vote, and drive change-state transitions. Every write +capability is an explicit operator opt-in. -- **Transport:** stdio -- **Target platform:** Gerrit 3.13+, authenticated via HTTP credentials -- **Output:** llmxml — semantically tagged text meant to be read by a model (see [Output format](#output-format)) +- Transport: stdio +- Target platform: Gerrit 3.13+, authenticated via HTTP credentials +- Output: llmxml, semantically tagged text meant to be read by a model (see [Output format](#output-format)) ## Safety posture Two defaults encode it: -- **Zero configuration exposes the `read` group only.** Write capability never appears unless enabled via `--groups`. -- **The own-changes restriction is on by default.** Even with write groups enabled, trail-leaving operations — - comments, votes, state changes, anything other humans see — are refused on changes the authenticated account does - not own, until the operator explicitly passes `--own-changes-only=false`. +- Zero configuration exposes the `read` group only. Write capability never appears unless enabled via `--groups`. +- The own-changes restriction is on by default. Even with write groups enabled, trail-leaving operations (comments, + votes, state changes, anything other humans see) are refused on changes the authenticated account does not own, + until the operator explicitly passes `--own-changes-only=false`. An agent leaving unwanted trail on colleagues' changes is an externally visible failure; a missing capability is a locally discoverable inconvenience. The defaults are chosen accordingly. Widen deliberately: @@ -27,7 +27,7 @@ go-gerrit-mcp --groups read,comment,transition --own-changes-only=false --projec ## Install -**Binary release** — download the binary for your platform from +Binary release: download the binary for your platform from [GitHub Releases](https://github.com/GaijinEntertainment/go-gerrit-mcp/releases), make it executable, and put it on your `PATH`: @@ -38,13 +38,13 @@ curl -Lo /usr/local/bin/go-gerrit-mcp \ chmod +x /usr/local/bin/go-gerrit-mcp ``` -**Docker:** +Docker: ```sh docker pull ghcr.io/gaijinentertainment/go-gerrit-mcp:latest ``` -**go install:** +go install: ```sh go install dev.gaijin.team/go/go-gerrit-mcp/cmd/go-gerrit-mcp@latest @@ -52,7 +52,7 @@ go install dev.gaijin.team/go/go-gerrit-mcp/cmd/go-gerrit-mcp@latest ## Quick start -The server reads connection identity from environment variables only — credentials never travel through flags: +The server reads connection identity from environment variables only; credentials never travel through flags: | Variable | Meaning | | ----------------- | -------------------------------------------------- | @@ -116,12 +116,12 @@ claude mcp add gerrit \ ## Per-project configuration in Claude Code -Everything the server needs is read from the environment — the identity variables above plus a `GERRIT_MCP_*` -mirror for every flag (see [Configuration reference](#configuration-reference)) — and MCP server processes inherit -the session environment. In Claude Code that turns one user-level registration into per-project configuration, -down to different Gerrit instances with different credentials per repository. +Everything the server needs is read from the environment: the identity variables above plus a `GERRIT_MCP_*` mirror +for every flag (see [Configuration reference](#configuration-reference)). MCP server processes inherit the session +environment, so in Claude Code one user-level registration turns into per-project configuration, down to different +Gerrit instances with different credentials per repository. -**`~/.claude.json`** — the registration; no `env` block: +`~/.claude.json` holds the registration, with no `env` block: ```json { @@ -133,7 +133,7 @@ down to different Gerrit instances with different credentials per repository. } ``` -**`/.claude/settings.local.json`** — the project's environment; every `env` entry reaches the server +`/.claude/settings.local.json` carries the project's environment; every `env` entry reaches the server process: ```json @@ -148,22 +148,22 @@ process: } ``` -Values shared by most projects can sit one layer down in `~/.claude/settings.json` — settings files merge with -`.claude/settings.local.json` over `.claude/settings.json` over `~/.claude/settings.json` — so a project declares -only its deltas. Anything no layer sets falls back to the server's own defaults: read-only, own changes. +Values shared by most projects can sit one layer down in `~/.claude/settings.json`. Settings files merge, with +`.claude/settings.local.json` over `.claude/settings.json` over `~/.claude/settings.json`, so a project declares only +its deltas. Anything no layer sets falls back to the server's own defaults: read-only, own changes. -The registration's `env` block stays empty for a reason: a variable named there shadows every settings layer, and -references are not a way around that — they never resolve from settings files. `${VAR}` expands only from the -shell environment that launched `claude`, and anything unresolved reaches the server as a literal string. +The registration's `env` block stays empty for a reason. A variable named there shadows every settings layer, and +references are no workaround: `${VAR}` expands only from the shell environment that launched `claude`, never from +settings files, and anything unresolved reaches the server as a literal string. Other MCP clients inherit their launch environment the same way, so per-directory tooling such as [direnv](https://direnv.net/) achieves the identical split without client support. ## Capability groups -Capability is selected at startup via `--groups` as a comma-separated list. Groups are independent and combinable — -there is no ladder; each write-capable group bundles the minimal change-read subset it needs to function on its own, -and enabled groups union. +Capability is selected at startup via `--groups` as a comma-separated list. Groups are independent and combinable, +with no privilege ladder: each write-capable group bundles the minimal change-read subset it needs to work on its +own, and enabled groups union. | Group | Tools | | ------------ | ---------------------------------------------------------------------------------------- | @@ -173,63 +173,63 @@ and enabled groups union. ### Tools -- `search_changes` — query changes with Gerrit's change query syntax, paginated. -- `get_change` — one change in review-relevant detail: status, owner, labels with votes, current revision, messages. -- `list_change_files` — files touched by a revision, with per-file change stats. -- `get_file_diff` — the diff of one file in a revision. -- `get_change_comments` — comment threads on a change, with resolution state and comment ids. Returns unresolved +- `search_changes`: query changes with Gerrit's change query syntax, paginated. +- `get_change`: one change in review-relevant detail: status, owner, labels with votes, current revision, messages. +- `list_change_files`: files touched by a revision, with per-file change stats. +- `get_file_diff`: the diff of one file in a revision. +- `get_change_comments`: comment threads on a change, with resolution state and comment ids. Returns unresolved threads only by default; `status=all` fetches the full history, `status=resolved` the settled threads. -- `post_comments` — publish a review in one call: optional top-level message plus inline, range, file-level, and - reply comments; replies anchor to comment ids from `get_change_comments`; `resolved` toggles the thread state. -- `set_vote` — set a label vote (e.g. `Code-Review`) with an optional message; value `0` clears an own vote. -- `transition_change` — move a change's state: `submit`, `abandon`, `restore`, `wip`, or `ready`, with an optional - message (submit accepts none). Gerrit's refusal — a blocked submit, a restore of a merged change — is reported +- `post_comments`: publish a review in one call: optional top-level message plus inline, range, file-level, and + reply comments. Replies anchor to comment ids from `get_change_comments`; `resolved` toggles the thread state. +- `set_vote`: set a label vote (e.g. `Code-Review`) with an optional message; value `0` clears an own vote. +- `transition_change`: move a change's state: `submit`, `abandon`, `restore`, `wip`, or `ready`, with an optional + message (submit accepts none). Gerrit's refusal (a blocked submit, a restore of a merged change) is reported verbatim. ## Review notifications -An opt-in push channel for review activity. The agent subscribes to a change (`subscribe_change`), and from then on -new change messages, votes, inline comment threads, and status transitions arrive in the session by themselves as -`review_activity` blocks — the same llmxml vocabulary the read tools emit, activity carried whole, so nothing needs -fetching afterwards. `unsubscribe_change` ends a subscription early; a merged or abandoned change ends its own with -a final notification saying so, and a change that becomes unreadable (deleted, or no longer visible to the account) -does the same naming the reason. +An opt-in push channel for review activity. The agent subscribes to a change with `subscribe_change`; from then on, +new change messages, votes, inline comment threads, and status transitions arrive in the session on their own as +`review_activity` blocks. The payload uses the same llmxml vocabulary the read tools emit and carries the activity +whole, so nothing needs fetching afterwards. `unsubscribe_change` ends a subscription early. A merged or abandoned +change ends its own subscription with a final notification that says so, and a change that becomes unreadable +(deleted, or no longer visible to the account) does the same, with the reason spelled out. Subscriptions are per-session and in-memory: they leave no trace on the Gerrit instance, end with the session, and -after a server restart the agent subscribes again. With the feature off — the default — the server is byte-identical -to its pre-feature self: no extra tools, no extra capability, no background polling. +after a server restart the agent subscribes again. With the feature off (the default) the server is byte-identical +to its pre-feature self, with no extra tools or capabilities and no background polling. Enabling takes both sides: -1. **Server** — `--review-notifications=true` (or its mirror). The server registers both subscription tools and - polls Gerrit every `--review-notifications-poll-interval` (default `60s`): one batched query per tick over all - subscribed changes, detail fetches only for changes that actually moved. -2. **Client** — delivery rides the Claude Code **channels** contract (research preview, Claude Code ≥ 2.1.80). +1. Server side: pass `--review-notifications=true` (or its mirror). The server registers both subscription tools + and polls Gerrit every `--review-notifications-poll-interval` (default `60s`): one batched query per tick over + all subscribed changes, with detail fetches only for changes that actually moved. +2. Client side: delivery uses the Claude Code channels contract (research preview, Claude Code 2.1.80 or newer). For a server registered plainly under `mcpServers`, launch with - `claude --dangerously-load-development-channels server:`, where `` is the registration key — it + `claude --dangerously-load-development-channels server:`, where `` is the registration key; it becomes the `source` attribute of the injected `` blocks. Allowlisted channel plugins load with `claude --channels` instead. Research-preview caveats: organization policy can disable channels entirely; the flag syntax may change between -Claude Code releases; and a client without channel support silently drops the events — the server then degrades to -exactly its pre-feature behavior, with no errors on either side. +Claude Code releases; and a client without channel support silently drops the events, in which case the server +behaves exactly as if the feature were off, with no errors on either side. -Noise control is operator configuration, not server heuristics — nothing is filtered by message tag, because a CI -verdict is often exactly the awaited outcome: +Noise control is operator configuration. The server applies no heuristics of its own and filters nothing by message +tag, because a bot's verdict is often exactly the outcome the agent is waiting for: - the authenticated account's own activity is skipped by default (`--review-notifications-include-own` keeps it); - `--review-notifications-exclude-accounts` silences accounts by username or numeric ID; - `--review-notifications-exclude-patterns` drops events whose message or comment text matches a regular - expression; an invalid pattern fails startup naming it. + expression; an invalid pattern fails startup with an error naming it. -Every flag has a `GERRIT_MCP_*` mirror riding the same settings layering as the rest of the configuration (see -[Per-project configuration](#per-project-configuration-in-claude-code)), so a project can enable notifications and -pick its exclusions in its own settings file. +Every flag has a `GERRIT_MCP_*` mirror that follows the same settings layering as the rest of the configuration +(see [Per-project configuration](#per-project-configuration-in-claude-code)), so a project can enable notifications +and pick exclusions in its own settings file. ## Configuration reference Behavior is configured by CLI flags, each mirrored by an environment variable so one configuration style works for -binary and Docker invocations alike. Precedence: **flag wins over its mirror, the mirror wins over the default.** +binary and Docker invocations alike. Precedence: the flag wins over its mirror, and the mirror wins over the default. | Flag | Mirror | Default | Meaning | | -------------------- | ----------------------------- | ------- | -------------------------------------------------------------- | @@ -248,18 +248,17 @@ Notes: - `--own-changes-only` takes an explicit boolean value: `--own-changes-only=false`. A bare flag without a value is a configuration error. -- **Project scoping** (`--projects`) is enforced server-side: a project clause is injected into every change query +- Project scoping (`--projects`) is enforced server-side: a project clause is injected into every change query regardless of what the agent composed, and direct operations on out-of-scope changes are refused. -- **Tool filters** only narrow. `--exclude-tools` removes tools from what the groups resolved; `--include-tools` - keeps only the listed subset of it. A tool outside the enabled groups can never be activated by a filter, and - exclude wins over include. Filter entries naming no known tool fail startup, so misconfigurations surface - immediately. +- Tool filters only narrow. `--exclude-tools` removes tools from what the groups resolved; `--include-tools` keeps + only the listed subset of it. A tool outside the enabled groups can never be activated by a filter, and exclude + wins over include. Filter entries naming no known tool fail startup, so misconfigurations surface immediately. - Configuration errors are aggregated: the server reports every problem at once, then exits non-zero. ## Output format -Every tool responds in **llmxml**: an LLM-digestible subset of XML — line-structured, semantically tagged text meant -to be read by a model. Attributes carry metadata, element bodies carry content: +Every tool responds in llmxml, an LLM-digestible subset of XML: line-structured, semantically tagged text meant to +be read by a model. Attributes carry metadata; element bodies carry content: ``` @@ -268,7 +267,7 @@ to be read by a model. Attributes carry metadata, element bodies carry content: ``` There is no XML declaration, no namespaces, and no schema; nothing parses it back. If you need machine-readable -Gerrit data, use Gerrit's REST API directly — this format is for model consumption. +Gerrit data, use Gerrit's REST API directly; this format is for model consumption. ## License