@rethunk/mcp-multi-root-git 2.1.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/mcp-tools.md CHANGED
@@ -17,15 +17,17 @@ MCP clients expose tools as `{serverName}_{toolName}`. With the server registere
17
17
  | `list_presets` | `rethunk-git_list_presets` | List preset names/counts from `.rethunk/git-mcp-presets.json`; invalid JSON/schema surface as errors. Workspace pick + `format` only. |
18
18
  | `git_log` | `rethunk-git_git_log` | Path-filtered, time-windowed `git log` across one or more workspace roots. Returns commit history with author, date, subject, and shortstat. Args: `since`, `paths`, `grep`, `author`, `maxCommits`, `branch`, plus workspace pick args + `format`. |
19
19
  | `git_diff_summary` | `rethunk-git_git_diff_summary` | Structured, token-efficient diff viewer. Returns per-file diffs with additions/deletions counts, truncated to configurable line limits, with lock files/dist/vendor excluded by default. Args: `range`, `fileFilter`, `maxLinesPerFile`, `maxFiles`, `excludePatterns`, plus workspace pick args + `format`. **Read-only.** |
20
- | `batch_commit` | `rethunk-git_batch_commit` | Create multiple sequential git commits in a single call. Each entry stages the listed files then commits with the given message. Stops on first failure. Args: `commits` (array of `{message, files}`), plus workspace pick args + `format`. **Mutating — not idempotent.** |
20
+ | `batch_commit` | `rethunk-git_batch_commit` | Create multiple sequential git commits in a single call. Each entry stages the listed files then commits with the given message. Stops on first failure. Optional `push: "after"` pushes the current branch to its upstream once every commit lands. Args: `commits` (array of `{message, files}`), `push?`, plus workspace pick args + `format`. **Mutating — not idempotent.** |
21
+ | `git_merge` | `rethunk-git_git_merge` | Merge one or more source branches into a destination. Default strategy `auto` cascades fast-forward → rebase → merge-commit per source, preferring linear history. Refuses on dirty tree; stops on first conflict with structured path report. Optional `deleteMergedBranches` / `deleteMergedWorktrees` cascade cleanup, always skipping protected names (main/master/dev/develop/stable/trunk/prod/production/release\*/hotfix\*). Args: `sources`, `into?`, `strategy?`, `message?`, cleanup flags + workspace pick + `format`. **Mutating.** |
22
+ | `git_cherry_pick` | `rethunk-git_git_cherry_pick` | Play commits from one or more sources onto a destination. Sources may be SHAs, `A..B` ranges, or branch names (expanded to `onto..<branch>`, oldest-first). Uses `--empty=drop` so patch-equivalent re-applies add nothing. Refuses on dirty tree; stops on first conflict, aborting cleanly. Same cleanup flags as `git_merge` (branch-kind sources only, protected names skipped). Args: `sources`, `onto?`, cleanup flags + workspace pick + `format`. **Mutating.** |
21
23
 
22
24
  Pass **`format: "json"`** on any tool for structured JSON instead of markdown (default).
23
25
 
24
26
  ## JSON responses
25
27
 
26
- Tool JSON bodies are minified and contain only the payload — no `rethunkGitMcp` envelope. Current `MCP_JSON_FORMAT_VERSION` is **`"2"`**; server + format version are discoverable via MCP `initialize`. Payload keys (`groups`, `inventories`, `parity`, `roots`) are stable within a given format version. Preset-related responses may include **`presetSchemaVersion`**.
28
+ Tool JSON bodies are minified and contain only the payload — no `rethunkGitMcp` envelope. Current `MCP_JSON_FORMAT_VERSION` is **`"3"`**; server + format version are discoverable via MCP `initialize`. Payload keys (`groups`, `inventories`, `parity`, `roots`) are stable within a given format version. Preset-related responses may include **`presetSchemaVersion`**.
27
29
 
28
- ### v2 field omission (consumer contract)
30
+ ### v2/v3 field omission (consumer contract)
29
31
 
30
32
  To keep responses compact, **optional fields are omitted when they would be empty, `null`, or `false`** — they are not emitted as `null`. Consumers must test for *presence*, not compare to `null`.
31
33
 
@@ -66,17 +68,15 @@ To keep responses compact, **optional fields are omitted when they would be empt
66
68
  ```json
67
69
  {
68
70
  "groups": [{
69
- "workspace_root": "/abs/path",
71
+ "workspaceRoot": "/abs/path",
70
72
  "repo": "my-repo",
71
73
  "branch": "main",
72
74
  "commits": [{
73
- "sha7": "a1bf184",
74
- "shaFull": "a1bf184c3d...",
75
+ "sha": "a1bf184c3d…",
75
76
  "subject": "feat(satcom): upgrade to PROTOCOL_VERSION 4",
76
77
  "author": "Damon Blais",
77
78
  "email": "damon@example.com",
78
79
  "date": "2026-04-12T18:32:01-07:00",
79
- "ageRelative": "42m ago",
80
80
  "filesChanged": 4,
81
81
  "insertions": 16,
82
82
  "deletions": 5
@@ -87,14 +87,16 @@ To keep responses compact, **optional fields are omitted when they would be empt
87
87
  }
88
88
  ```
89
89
 
90
- v2 field-omission rules: `filesChanged`, `insertions`, `deletions` are omitted when zero/absent (new file with no shortstat). `truncated` and `omittedCount` are omitted when `false`/`0`. A group emits `error` instead of `commits` when git fails for that root.
90
+ v3 changes from v2: `sha7` removed (use `sha.slice(0,7)` for display); `ageRelative` removed (use `date` ISO 8601); `email` omitted when empty; `workspace_root` renamed to `workspaceRoot` (camelCase consistency).
91
+
92
+ v2 field-omission rules still apply: `filesChanged`, `insertions`, `deletions` omitted when zero/absent. `truncated` and `omittedCount` omitted when `false`/`0`. A group emits `error` instead of `commits` when git fails for that root.
91
93
 
92
94
  ### `git_log` — error codes
93
95
 
94
96
  | Code | Meaning |
95
97
  |------|---------|
96
98
  | `git_not_found` | `git` binary not on `PATH`. |
97
- | `not_a_git_repo` | The resolved workspace root is not inside a git repository. |
99
+ | `not_a_git_repository` | The resolved workspace root is not inside a git repository. |
98
100
  | `invalid_since` | The `since` string contains shell metacharacters and was rejected. |
99
101
  | `invalid_paths` | One of the `paths` entries contains shell metacharacters and was rejected. |
100
102
  | `git_log_failed` | `git log` exited non-zero (e.g. unknown branch ref). |
@@ -152,6 +154,7 @@ v2 field-omission rules: `filesChanged`, `insertions`, `deletions` are omitted w
152
154
  | Parameter | Type | Notes |
153
155
  |-----------|------|-------|
154
156
  | `commits` | `{message: string, files: string[]}[]` | Commits to create in order. 1–50 entries. Each `files` entry is a path relative to the git root; all must stay within the git toplevel. |
157
+ | `push` | `"never"` \| `"after"` | Default `"never"`. `"after"` pushes the current branch to its upstream **once all commits succeed**. Never auto-sets upstream — branches without an upstream fail with `push_no_upstream`. Commits are **not** rolled back on push failure. Enum reserved for future modes such as `"force-with-lease"`. |
155
158
  | `workspaceRoot` | string | Explicit root; highest priority. |
156
159
  | `rootIndex` | int | Pick one of several MCP roots (0-based). |
157
160
  | `format` | `"markdown"` \| `"json"` | Output format. Default: `"markdown"`. |
@@ -175,12 +178,19 @@ v2 field-omission rules: `filesChanged`, `insertions`, `deletions` are omitted w
175
178
  "sha": "b2c3d4e",
176
179
  "message": "chore: update config",
177
180
  "files": ["config.json"]
178
- }]
181
+ }],
182
+ "push": {
183
+ "ok": true,
184
+ "branch": "main",
185
+ "upstream": "origin/main"
186
+ }
179
187
  }
180
188
  ```
181
189
 
182
190
  On first failure `ok` is `false`, `committed` reflects only the entries that succeeded before the error, and the failing entry includes `error` and `detail` fields. Remaining entries are skipped and not included in `results`.
183
191
 
192
+ The `push` object is present only when `push: "after"` was requested **and** every commit landed. On push failure the top-level `ok` stays `true` (the commits themselves succeeded) while `push.ok` is `false` and `push.error` carries the code.
193
+
184
194
  ### `batch_commit` — error codes (per-result `error` field)
185
195
 
186
196
  | Code | Meaning |
@@ -190,6 +200,247 @@ On first failure `ok` is `false`, `committed` reflects only the entries that suc
190
200
  | `commit_failed` | `git commit` failed (e.g. nothing staged, hooks rejected). |
191
201
  | `not_a_git_repository` | The resolved workspace root is not inside a git repository. |
192
202
 
203
+ ### `batch_commit` — push error codes (`push.error` field)
204
+
205
+ | Code | Meaning |
206
+ |------|---------|
207
+ | `push_detached_head` | HEAD is detached; no branch to push. |
208
+ | `push_no_upstream` | Current branch has no configured upstream. `batch_commit` will not auto-set one — do `git push -u origin <branch>` yourself (or re-run without `push`). |
209
+ | `push_failed` | `git push` exited non-zero (network error, non-fast-forward, hook rejection). `detail` carries the stderr/stdout from git. |
210
+
211
+ ---
212
+
213
+ ### `git_merge` — parameters
214
+
215
+ | Parameter | Type | Notes |
216
+ |-----------|------|-------|
217
+ | `sources` | `string[]` | Source branches to merge, in order. 1–20 entries. Each must be a valid git ref token. |
218
+ | `into` | string | Destination branch. Defaults to the currently checked-out branch. Rejected when HEAD is detached. |
219
+ | `strategy` | `"auto"` \| `"ff-only"` \| `"rebase"` \| `"merge"` | Default `"auto"`: cascade **fast-forward → rebase → merge-commit** per source. `"ff-only"` fails on divergence. `"rebase"` rebases source onto destination and fast-forwards; no merge-commit fallback. `"merge"` always creates a merge commit (`--no-ff`). |
220
+ | `message` | string | Merge commit message, used only when a merge commit is created. Defaults to `Merge branch '<source>' into <into>`. |
221
+ | `deleteMergedBranches` | boolean | Default `false`. After **all** sources land cleanly, delete each source branch locally (`git branch -d`). **Protected names always skipped** (main, master, dev, develop, stable, trunk, prod, production, `release/*`, `release-*`, `hotfix/*`, `hotfix-*`). Never touches remote refs. |
222
+ | `deleteMergedWorktrees` | boolean | Default `false`. After success, remove any local worktree currently checked out on a source branch (`git worktree remove`). Protected tails always skipped. |
223
+ | `workspaceRoot`, `rootIndex`, `format` | — | Standard workspace pick + output format. |
224
+
225
+ ### `git_merge` — JSON shape (`format: "json"`)
226
+
227
+ ```json
228
+ {
229
+ "ok": true,
230
+ "into": "main",
231
+ "strategy": "auto",
232
+ "headSha": "a1b2c3d4e5f6…",
233
+ "applied": 2,
234
+ "total": 2,
235
+ "results": [
236
+ {
237
+ "source": "feature/a",
238
+ "ok": true,
239
+ "outcome": "fast_forward",
240
+ "mergedSha": "a1b2c3d4e5f6…",
241
+ "branchDeleted": true,
242
+ "worktreeRemoved": "/tmp/agent-a"
243
+ },
244
+ {
245
+ "source": "feature/b",
246
+ "ok": true,
247
+ "outcome": "rebase_then_ff",
248
+ "mergedSha": "b2c3d4e5f6a1…"
249
+ }
250
+ ]
251
+ }
252
+ ```
253
+
254
+ **`outcome`** (per source): `fast_forward`, `rebase_then_ff`, `merge_commit`, `up_to_date`, or `conflicts`. Cleanup fields (`branchDeleted`, `worktreeRemoved`) are only emitted when the corresponding flag was set and the operation actually ran — both are omitted for up-to-date sources and are never populated on partial-failure runs.
255
+
256
+ On conflict: top-level `ok` is `false`, the conflicting entry has `ok: false` with `conflictStage` (`"rebase"` or `"merge"`), `conflictPaths` (array of paths with unresolved markers), and an `error` code. Remaining sources are not attempted.
257
+
258
+ ### `git_merge` — error codes
259
+
260
+ | Code | Meaning |
261
+ |------|---------|
262
+ | `unsafe_ref_token` | A source or `into` contains characters outside the argv-safe subset (spaces, shell meta, `..`, `@{`, leading `-`, trailing `.lock`). |
263
+ | `into_detached_head` | HEAD is detached and no `into` was given — the tool needs a concrete destination branch. |
264
+ | `working_tree_dirty` | Uncommitted changes present. Commit, stash, or discard before merging. |
265
+ | `checkout_failed` | Could not switch to `into`. `detail` carries git's stderr. |
266
+ | `destination_not_found` | `into` does not resolve to a commit. |
267
+ | `not_a_git_repository` | The resolved workspace root is not inside a git repository. |
268
+ | `source_not_found` (per source) | A source branch name does not resolve. |
269
+ | `cannot_fast_forward` (per source) | `strategy: "ff-only"` refused because branches have diverged. |
270
+ | `rebase_conflicts` (per source) | Rebase encountered conflicts. Repo state is cleaned before returning. |
271
+ | `merge_conflicts` (per source) | Merge commit encountered conflicts. Repo state is cleaned before returning. |
272
+ | `merge_failed` (per source) | `git merge --ff-only` failed unexpectedly. `detail` carries stderr. |
273
+ | `merge_base_failed` (per source) | `git merge-base` failed (usually unrelated histories). |
274
+
275
+ ---
276
+
277
+ ### `git_cherry_pick` — parameters
278
+
279
+ | Parameter | Type | Notes |
280
+ |-----------|------|-------|
281
+ | `sources` | `string[]` | Source specs. 1–50 entries. Each entry is one of: a full/short SHA, an `A..B` / `A...B` range, or a branch name. Branch names expand to `onto..<branch>` (oldest-first). |
282
+ | `onto` | string | Destination branch. Defaults to the currently checked-out branch. Rejected when HEAD is detached. |
283
+ | `deleteMergedBranches` | boolean | Default `false`. After all commits apply, delete each **branch-kind** source locally (`git branch -d`) when it is fully merged into the destination by SHA-reachability (not patch-equivalence). Protected names always skipped; never touches remote refs. |
284
+ | `deleteMergedWorktrees` | boolean | Default `false`. After success, remove any local worktree attached to a branch-kind source (`git worktree remove`). Protected tails always skipped. |
285
+ | `workspaceRoot`, `rootIndex`, `format` | — | Standard workspace pick + output format. |
286
+
287
+ ### `git_cherry_pick` — JSON shape (`format: "json"`)
288
+
289
+ ```json
290
+ {
291
+ "ok": true,
292
+ "onto": "main",
293
+ "headSha": "a1b2c3d…",
294
+ "picked": 3,
295
+ "applied": 2,
296
+ "results": [
297
+ { "source": "feature/a", "kind": "branch", "resolvedCommits": 2, "keptCommits": 2 },
298
+ { "source": "abcdef1", "kind": "sha", "resolvedCommits": 1, "keptCommits": 1 }
299
+ ]
300
+ }
301
+ ```
302
+
303
+ **`picked`** is the number of unique SHAs fed to `git cherry-pick` after SHA-reachability filtering. **`applied`** is the number of new commits actually added to HEAD — may be less than `picked` because the tool passes `--empty=drop` to git, so patch-equivalent commits are skipped at apply time without error.
304
+
305
+ **`kind`** is `"sha"`, `"range"`, or `"branch"`. **`resolvedCommits`** is how many commits the source expanded to; **`keptCommits`** is how many survived SHA-reachability dedupe. Cleanup fields (`branchDeleted`, `worktreeRemoved`) are only emitted for branch-kind sources when the corresponding flag was set and the operation succeeded.
306
+
307
+ On conflict, the response has `ok: false` and a top-level `conflict` object:
308
+
309
+ ```json
310
+ {
311
+ "ok": false,
312
+ "onto": "main",
313
+ "picked": 2,
314
+ "applied": 0,
315
+ "results": [ ... ],
316
+ "conflict": {
317
+ "stage": "cherry-pick",
318
+ "commit": "abcdef1",
319
+ "paths": ["src/foo.ts"],
320
+ "detail": "…git stderr…"
321
+ }
322
+ }
323
+ ```
324
+
325
+ Repo state is cleaned (`git cherry-pick --abort`) before returning — no partially-applied index.
326
+
327
+ ### `git_cherry_pick` — error codes
328
+
329
+ | Code | Meaning |
330
+ |------|---------|
331
+ | `unsafe_ref_token` | A source or `onto` contains characters outside the argv-safe subset. |
332
+ | `onto_detached_head` | HEAD is detached and no `onto` was given. |
333
+ | `working_tree_dirty` | Uncommitted changes present. Commit, stash, or discard before cherry-picking. |
334
+ | `checkout_failed` | Could not switch to `onto`. |
335
+ | `destination_not_found` | `onto` does not resolve to a commit. |
336
+ | `source_not_found` | A source spec resolves to neither a branch, a range, nor a commit. |
337
+ | `range_resolution_failed` | `git rev-list` failed to expand a range spec. |
338
+ | `not_a_git_repository` | The resolved workspace root is not inside a git repository. |
339
+
340
+ ---
341
+
342
+ ### `git_push` — parameters
343
+
344
+ | Parameter | Type | Notes |
345
+ |-----------|------|-------|
346
+ | `remote` | string | Remote to push to. Defaults to the remote inferred from the upstream tracking ref, or `origin` when `setUpstream` is true. |
347
+ | `branch` | string | Branch to push. Defaults to the currently checked-out branch. Rejected on detached HEAD. |
348
+ | `setUpstream` | boolean | Default `false`. Pass `-u` to set the upstream tracking ref; remote defaults to `origin`. |
349
+ | `workspaceRoot`, `rootIndex`, `format` | — | Standard workspace pick + output format. |
350
+
351
+ ### `git_push` — JSON shape (`format: "json"`)
352
+
353
+ ```json
354
+ { "ok": true, "branch": "feature/x", "remote": "origin", "upstream": "origin/feature/x" }
355
+ ```
356
+
357
+ ### `git_push` — error codes
358
+
359
+ | Code | Meaning |
360
+ |------|---------|
361
+ | `push_detached_head` | HEAD is detached; no branch name to push. |
362
+ | `push_no_upstream` | Branch has no configured upstream and `setUpstream` was not requested. |
363
+ | `push_failed` | `git push` exited non-zero. `detail` carries stderr. |
364
+ | `unsafe_ref_token` | `branch` value contains characters outside the safe token set. |
365
+ | `unsafe_remote_token` | `remote` value contains disallowed characters. |
366
+ | `not_a_git_repository` | The resolved workspace root is not inside a git repository. |
367
+
368
+ ---
369
+
370
+ ### `git_reset_soft` — parameters
371
+
372
+ | Parameter | Type | Notes |
373
+ |-----------|------|-------|
374
+ | `ref` | string | Target commit: `HEAD~N`, branch name, or full/short SHA. |
375
+ | `workspaceRoot`, `rootIndex`, `format` | — | Standard workspace pick + output format. |
376
+
377
+ ### `git_reset_soft` — JSON shape (`format: "json"`)
378
+
379
+ ```json
380
+ { "ok": true, "ref": "HEAD~2", "beforeSha": "a1b2c3d…", "afterSha": "f9e8d7c…", "stagedCount": 5 }
381
+ ```
382
+
383
+ ### `git_reset_soft` — error codes
384
+
385
+ | Code | Meaning |
386
+ |------|---------|
387
+ | `unsafe_ref_token` | `ref` contains characters outside the ancestor-safe token set. |
388
+ | `working_tree_dirty` | Working tree has uncommitted/unstaged changes; clean up before resetting. |
389
+ | `status_failed` | `git status` failed unexpectedly. |
390
+ | `reset_failed` | `git reset --soft` failed (e.g. ref does not exist). |
391
+ | `not_a_git_repository` | The resolved workspace root is not inside a git repository. |
392
+
393
+ ---
394
+
395
+ ### `git_worktree_list` — JSON shape (`format: "json"`)
396
+
397
+ ```json
398
+ { "worktrees": [{ "path": "/abs/path", "branch": "feature/x", "head": "a1b2c3d…" }] }
399
+ ```
400
+
401
+ `branch` is `null` for detached-HEAD worktrees.
402
+
403
+ ### `git_worktree_add` — parameters
404
+
405
+ | Parameter | Type | Notes |
406
+ |-----------|------|-------|
407
+ | `path` | string | Filesystem path for the new worktree. Relative paths are resolved from the git toplevel. |
408
+ | `branch` | string | Branch to check out. Created from `baseRef` if it does not already exist. |
409
+ | `baseRef` | string | Commit-ish for branch creation. Default: `HEAD`. Ignored when `branch` already exists. |
410
+ | `workspaceRoot`, `rootIndex`, `format` | — | Standard workspace pick + output format. |
411
+
412
+ ### `git_worktree_add` — JSON shape (`format: "json"`)
413
+
414
+ ```json
415
+ { "ok": true, "path": "/abs/worktree", "branch": "feature/x", "created": true, "baseRef": "main" }
416
+ ```
417
+
418
+ ### `git_worktree_add` — error codes
419
+
420
+ | Code | Meaning |
421
+ |------|---------|
422
+ | `unsafe_ref_token` | `branch` or `baseRef` contains disallowed characters. |
423
+ | `protected_branch` | `branch` is on the protected names list. |
424
+ | `worktree_add_failed` | `git worktree add` exited non-zero. `detail` carries stderr. |
425
+ | `not_a_git_repository` | The resolved workspace root is not inside a git repository. |
426
+
427
+ ### `git_worktree_remove` — parameters
428
+
429
+ | Parameter | Type | Notes |
430
+ |-----------|------|-------|
431
+ | `path` | string | Path of the worktree to remove. |
432
+ | `force` | boolean | Default `false`. Pass `--force` to allow removal with uncommitted changes. |
433
+ | `workspaceRoot`, `rootIndex`, `format` | — | Standard workspace pick + output format. |
434
+
435
+ ### `git_worktree_remove` — error codes
436
+
437
+ | Code | Meaning |
438
+ |------|---------|
439
+ | `cannot_remove_main_worktree` | `path` resolves to the main (non-linked) worktree. |
440
+ | `worktree_not_found` | `path` is not registered as a worktree in this repo. |
441
+ | `worktree_remove_failed` | `git worktree remove` failed. Pass `force: true` if there are uncommitted changes. |
442
+ | `not_a_git_repository` | The resolved workspace root is not inside a git repository. |
443
+
193
444
  ---
194
445
 
195
446
  ## Resource
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rethunk/mcp-multi-root-git",
3
- "version": "2.1.0",
3
+ "version": "2.3.0",
4
4
  "description": "MCP stdio server: multi-root git status, inventory, and HEAD parity checks. Generic tools usable by any workspace.",
5
5
  "type": "module",
6
6
  "private": false,
@@ -14,6 +14,7 @@
14
14
  "docs/install.md",
15
15
  "docs/mcp-tools.md",
16
16
  "AGENTS.md",
17
+ "CHANGELOG.md",
17
18
  "HUMANS.md",
18
19
  "README.md",
19
20
  "LICENSE"
@@ -59,9 +60,9 @@
59
60
  "zod": "^4.3.6"
60
61
  },
61
62
  "devDependencies": {
62
- "@biomejs/biome": "^2.4.10",
63
- "@types/node": "^22.0.0",
64
- "rimraf": "^6.0.1",
63
+ "@biomejs/biome": "^2.4.11",
64
+ "@types/node": "^22.19.17",
65
+ "rimraf": "^6.1.3",
65
66
  "typescript": "^6.0.2"
66
67
  }
67
68
  }