@rethunk/mcp-multi-root-git 2.6.0 → 2.8.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.
Files changed (38) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +21 -0
  3. package/README.md +1 -1
  4. package/dist/server/batch-commit-tool.js +7 -6
  5. package/dist/server/error-codes.js +95 -0
  6. package/dist/server/git-blame-tool.js +227 -0
  7. package/dist/server/git-branch-list-tool.js +140 -0
  8. package/dist/server/git-cherry-pick-tool.js +11 -10
  9. package/dist/server/git-diff-summary-tool.js +3 -2
  10. package/dist/server/git-diff-tool.js +56 -12
  11. package/dist/server/git-fetch-tool.js +142 -14
  12. package/dist/server/git-inventory-tool.js +5 -4
  13. package/dist/server/git-log-tool.js +10 -5
  14. package/dist/server/git-merge-tool.js +15 -14
  15. package/dist/server/git-parity-tool.js +3 -2
  16. package/dist/server/git-push-tool.js +9 -5
  17. package/dist/server/git-reflog-tool.js +126 -0
  18. package/dist/server/git-reset-soft-tool.js +4 -3
  19. package/dist/server/git-show-tool.js +83 -23
  20. package/dist/server/git-stash-tool.js +2 -1
  21. package/dist/server/git-tag-tool.js +8 -7
  22. package/dist/server/git-worktree-tool.js +8 -7
  23. package/dist/server/git.js +70 -4
  24. package/dist/server/list-presets-tool.js +2 -1
  25. package/dist/server/presets-resource.js +3 -2
  26. package/dist/server/presets.js +11 -5
  27. package/dist/server/roots.js +11 -10
  28. package/dist/server/tool-parameter-schemas.js +9 -0
  29. package/dist/server/tools.js +6 -0
  30. package/docs/mcp-tools.md +119 -6
  31. package/package.json +2 -2
  32. package/schemas/git_blame.json +52 -0
  33. package/schemas/git_branch_list.json +36 -0
  34. package/schemas/git_diff.json +14 -1
  35. package/schemas/git_reflog.json +44 -0
  36. package/schemas/git_show.json +12 -1
  37. package/schemas/index.json +15 -0
  38. package/tool-parameters.schema.json +152 -2
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { z } from "zod";
4
+ import { ERROR_CODES } from "./error-codes.js";
4
5
  /**
5
6
  * Schema for `.rethunk/git-mcp-presets.json` at the workspace root.
6
7
  * Each named entry defines roots for `git_inventory` and/or pairs for `git_parity`.
@@ -85,16 +86,21 @@ export function presetLoadErrorPayload(gitTop, fail) {
85
86
  const presetFile = join(gitTop, PRESET_FILE_PATH);
86
87
  if (fail.reason === "invalid_json") {
87
88
  return {
88
- error: "preset_file_invalid",
89
+ error: ERROR_CODES.PRESET_FILE_INVALID,
89
90
  kind: "invalid_json",
90
91
  presetFile,
91
92
  message: fail.message,
92
93
  };
93
94
  }
94
95
  if (fail.reason === "schema") {
95
- return { error: "preset_file_invalid", kind: "schema", presetFile, issues: fail.issues };
96
+ return {
97
+ error: ERROR_CODES.PRESET_FILE_INVALID,
98
+ kind: "schema",
99
+ presetFile,
100
+ issues: fail.issues,
101
+ };
96
102
  }
97
- return { error: "preset_file_invalid", presetFile };
103
+ return { error: ERROR_CODES.PRESET_FILE_INVALID, presetFile };
98
104
  }
99
105
  function getPresetEntry(gitTop, presetName) {
100
106
  const loaded = loadPresetsFromGitTop(gitTop);
@@ -103,7 +109,7 @@ function getPresetEntry(gitTop, presetName) {
103
109
  return {
104
110
  ok: false,
105
111
  error: {
106
- error: "preset_not_found",
112
+ error: ERROR_CODES.PRESET_NOT_FOUND,
107
113
  preset: presetName,
108
114
  presetFile: join(gitTop, PRESET_FILE_PATH),
109
115
  },
@@ -116,7 +122,7 @@ function getPresetEntry(gitTop, presetName) {
116
122
  return {
117
123
  ok: false,
118
124
  error: {
119
- error: "preset_not_found",
125
+ error: ERROR_CODES.PRESET_NOT_FOUND,
120
126
  preset: presetName,
121
127
  presetFile: join(gitTop, PRESET_FILE_PATH),
122
128
  },
@@ -1,5 +1,6 @@
1
1
  import { basename, resolve } from "node:path";
2
2
  import { fileURLToPath } from "node:url";
3
+ import { ERROR_CODES } from "./error-codes.js";
3
4
  import { gateGit, gitTopLevel } from "./git.js";
4
5
  import { loadPresetsFromGitTop, presetLoadErrorPayload } from "./presets.js";
5
6
  import { MAX_ABSOLUTE_GIT_ROOTS } from "./schemas.js";
@@ -61,7 +62,7 @@ export function resolveAbsoluteGitRootsList(raw) {
61
62
  return {
62
63
  ok: false,
63
64
  error: {
64
- error: "absolute_git_roots_too_many",
65
+ error: ERROR_CODES.ABSOLUTE_GIT_ROOTS_TOO_MANY,
65
66
  max: MAX_ABSOLUTE_GIT_ROOTS,
66
67
  count: raw.length,
67
68
  },
@@ -72,12 +73,12 @@ export function resolveAbsoluteGitRootsList(raw) {
72
73
  for (const item of raw) {
73
74
  const trimmed = item.trim();
74
75
  if (trimmed.length === 0) {
75
- return { ok: false, error: { error: "invalid_absolute_git_root", path: item } };
76
+ return { ok: false, error: { error: ERROR_CODES.INVALID_ABSOLUTE_GIT_ROOT, path: item } };
76
77
  }
77
78
  const abs = resolve(trimmed);
78
79
  const top = gitTopLevel(abs);
79
80
  if (!top) {
80
- return { ok: false, error: { error: "invalid_absolute_git_root", path: abs } };
81
+ return { ok: false, error: { error: ERROR_CODES.INVALID_ABSOLUTE_GIT_ROOT, path: abs } };
81
82
  }
82
83
  if (seen.has(top))
83
84
  continue;
@@ -85,7 +86,7 @@ export function resolveAbsoluteGitRootsList(raw) {
85
86
  tops.push(top);
86
87
  }
87
88
  if (tops.length === 0) {
88
- return { ok: false, error: { error: "absolute_git_roots_empty" } };
89
+ return { ok: false, error: { error: ERROR_CODES.ABSOLUTE_GIT_ROOTS_EMPTY } };
89
90
  }
90
91
  return { ok: true, roots: tops };
91
92
  }
@@ -104,7 +105,7 @@ function resolveWorkspaceRoots(server, args) {
104
105
  return {
105
106
  ok: false,
106
107
  error: {
107
- error: "root_index_out_of_range",
108
+ error: ERROR_CODES.ROOT_INDEX_OUT_OF_RANGE,
108
109
  rootIndex: args.rootIndex,
109
110
  rootCount: fileRoots.length,
110
111
  },
@@ -163,10 +164,10 @@ export function requireGitAndRoots(server, args, presetName) {
163
164
  const abs = args.absoluteGitRoots;
164
165
  if (abs != null && abs.length > 0) {
165
166
  if (presetName) {
166
- return { ok: false, error: { error: "absolute_git_roots_preset_conflict" } };
167
+ return { ok: false, error: { error: ERROR_CODES.ABSOLUTE_GIT_ROOTS_PRESET_CONFLICT } };
167
168
  }
168
169
  if (hasExclusiveWorkspacePick(args)) {
169
- return { ok: false, error: { error: "absolute_git_roots_exclusive" } };
170
+ return { ok: false, error: { error: ERROR_CODES.ABSOLUTE_GIT_ROOTS_EXCLUSIVE } };
170
171
  }
171
172
  return resolveAbsoluteGitRootsList(abs);
172
173
  }
@@ -191,16 +192,16 @@ export function requireSingleRepo(server, args, presetName = undefined) {
191
192
  return {
192
193
  ok: false,
193
194
  error: {
194
- error: "absolute_git_roots_single_repo_only",
195
+ error: ERROR_CODES.ABSOLUTE_GIT_ROOTS_SINGLE_REPO_ONLY,
195
196
  rootCount: pre.roots.length,
196
197
  },
197
198
  };
198
199
  }
199
200
  const rootInput = pre.roots[0];
200
201
  if (!rootInput)
201
- return { ok: false, error: { error: "no_workspace_root" } };
202
+ return { ok: false, error: { error: ERROR_CODES.NO_WORKSPACE_ROOT } };
202
203
  const top = gitTopLevel(rootInput);
203
204
  if (!top)
204
- return { ok: false, error: { error: "not_a_git_repository", path: rootInput } };
205
+ return { ok: false, error: { error: ERROR_CODES.NOT_A_GIT_REPOSITORY, path: rootInput } };
205
206
  return { ok: true, gitTop: top };
206
207
  }
@@ -1,5 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { registerBatchCommitTool } from "./batch-commit-tool.js";
3
+ import { registerGitBlameTool } from "./git-blame-tool.js";
4
+ import { registerGitBranchListTool } from "./git-branch-list-tool.js";
3
5
  import { registerGitCherryPickTool } from "./git-cherry-pick-tool.js";
4
6
  import { registerGitDiffSummaryTool } from "./git-diff-summary-tool.js";
5
7
  import { registerGitDiffTool } from "./git-diff-tool.js";
@@ -9,6 +11,7 @@ import { registerGitLogTool } from "./git-log-tool.js";
9
11
  import { registerGitMergeTool } from "./git-merge-tool.js";
10
12
  import { registerGitParityTool } from "./git-parity-tool.js";
11
13
  import { registerGitPushTool } from "./git-push-tool.js";
14
+ import { registerGitReflogTool } from "./git-reflog-tool.js";
12
15
  import { registerGitResetSoftTool } from "./git-reset-soft-tool.js";
13
16
  import { registerGitShowTool } from "./git-show-tool.js";
14
17
  import { registerGitStashApplyTool, registerGitStashListTool } from "./git-stash-tool.js";
@@ -29,6 +32,9 @@ export const READ_ONLY_SINGLE_REPO_TOOLS = [
29
32
  "git_show",
30
33
  "git_worktree_list",
31
34
  "git_stash_list",
35
+ "git_blame",
36
+ "git_branch_list",
37
+ "git_reflog",
32
38
  ];
33
39
  export const MUTATING_TOOLS = [
34
40
  "git_fetch",
@@ -71,6 +77,9 @@ export function captureToolParameterSchemas() {
71
77
  registerGitWorktreeListTool(server);
72
78
  registerGitStashListTool(server);
73
79
  registerGitFetchTool(server);
80
+ registerGitBlameTool(server);
81
+ registerGitBranchListTool(server);
82
+ registerGitReflogTool(server);
74
83
  registerBatchCommitTool(server);
75
84
  registerGitPushTool(server);
76
85
  registerGitMergeTool(server);
@@ -1,4 +1,6 @@
1
1
  import { registerBatchCommitTool } from "./batch-commit-tool.js";
2
+ import { registerGitBlameTool } from "./git-blame-tool.js";
3
+ import { registerGitBranchListTool } from "./git-branch-list-tool.js";
2
4
  import { registerGitCherryPickTool } from "./git-cherry-pick-tool.js";
3
5
  import { registerGitDiffSummaryTool } from "./git-diff-summary-tool.js";
4
6
  import { registerGitDiffTool } from "./git-diff-tool.js";
@@ -8,6 +10,7 @@ import { registerGitLogTool } from "./git-log-tool.js";
8
10
  import { registerGitMergeTool } from "./git-merge-tool.js";
9
11
  import { registerGitParityTool } from "./git-parity-tool.js";
10
12
  import { registerGitPushTool } from "./git-push-tool.js";
13
+ import { registerGitReflogTool } from "./git-reflog-tool.js";
11
14
  import { registerGitResetSoftTool } from "./git-reset-soft-tool.js";
12
15
  import { registerGitShowTool } from "./git-show-tool.js";
13
16
  import { registerGitStashApplyTool, registerGitStashListTool } from "./git-stash-tool.js";
@@ -29,6 +32,9 @@ export function registerRethunkGitTools(server) {
29
32
  registerGitWorktreeListTool(server);
30
33
  registerGitStashListTool(server);
31
34
  registerGitFetchTool(server);
35
+ registerGitBlameTool(server);
36
+ registerGitBranchListTool(server);
37
+ registerGitReflogTool(server);
32
38
  // Mutating tools
33
39
  registerBatchCommitTool(server);
34
40
  registerGitPushTool(server);
package/docs/mcp-tools.md CHANGED
@@ -17,11 +17,14 @@ 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 (includes `absoluteGitRoots`). **Read-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 (`absoluteGitRoots` for sibling clones) + `format` (`markdown`/`json`/`oneline`). **Read-only.** |
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 (optional single-entry `absoluteGitRoots`) + `format`. **Read-only.** |
20
- | `git_diff` | `rethunk-git_git_diff` | Raw diff text for a single repo. Supports unstaged, staged, or `base..head` ranges, optionally scoped to one path. Args: `workspaceRoot`, `rootIndex`, `format`, `base?`, `head?`, `path?`, `staged?`. No `absoluteGitRoots` or `allWorkspaceRoots`. **Read-only.** |
21
- | `git_show` | `rethunk-git_git_show` | Inspect one commit or ref. Returns commit message plus diff, or file content at `path` for that ref. Args: `ref`, `path?`, plus single-repo workspace pick + `format`. **Read-only.** |
20
+ | `git_diff` | `rethunk-git_git_diff` | Raw diff text for a single repo. Supports unstaged, staged, or `base..head` ranges, scoped to one or more paths with configurable context width. Args: `workspaceRoot`, `rootIndex`, `format`, `base?`, `head?`, `path?`, `paths?`, `unified?`, `staged?`. No `absoluteGitRoots` or `allWorkspaceRoots`. **Read-only.** |
21
+ | `git_show` | `rethunk-git_git_show` | Inspect one commit or ref. Returns commit message plus diff (or `--stat` diffstat), or file content at `path` for that ref. Args: `ref`, `path?`, `paths?`, `stat?`, plus single-repo workspace pick + `format`. **Read-only.** |
22
22
  | `git_worktree_list` | `rethunk-git_git_worktree_list` | List all worktrees (`git worktree list --porcelain`). Workspace pick + `format`. **Read-only.** |
23
23
  | `git_stash_list` | `rethunk-git_git_stash_list` | List `git stash` entries for one repo. Args: single-repo workspace pick + `format`. **Read-only.** |
24
- | `git_fetch` | `rethunk-git_git_fetch` | Fetch from a remote without modifying the working tree. Updates refs only and reports updated/new refs. Args: `remote?`, `branch?`, `prune?`, `tags?`, plus single-repo workspace pick + `format`. **Mutating — refs only.** |
24
+ | `git_blame` | `rethunk-git_git_blame` | Line-by-line authorship for a file: commit SHA, author, date, summary per line. Args: `path` (required), `ref?`, `startLine?`, `endLine?`, plus single-repo workspace pick + `format`. **Read-only.** |
25
+ | `git_branch_list` | `rethunk-git_git_branch_list` | List local branches (sha, current marker, upstream); optional `includeRemotes` adds remote-tracking branches. Args: `includeRemotes?`, plus single-repo workspace pick + `format`. **Read-only.** |
26
+ | `git_reflog` | `rethunk-git_git_reflog` | Show the reflog for a ref (default `HEAD`) — recent HEAD movements with selector, SHA, and message. Args: `ref?`, `maxEntries?`, plus single-repo workspace pick + `format`. **Read-only.** |
27
+ | `git_fetch` | `rethunk-git_git_fetch` | Fetch from a remote without modifying the working tree. Updates refs only and reports updated/new refs, plus structured `updated`/`created`/`pruned` deltas on git ≥ 2.41. Args: `remote?`, `branch?`, `prune?`, `tags?`, plus single-repo workspace pick + `format`. **Mutating — refs only.** |
25
28
  | `git_push` | `rethunk-git_git_push` | Push the current branch to its upstream. Optional `remote`, `branch`, `setUpstream` (passes `-u`). Refuses on detached HEAD; never force-pushes. Workspace pick + `format`. **Mutating.** |
26
29
  | `git_tag` | `rethunk-git_git_tag` | Create/delete annotated or lightweight tags for one repo. Args: `tag`, `message?`, `ref?`, `delete?`, plus single-repo workspace pick + `format`. **Mutating.** |
27
30
  | `git_worktree_add` | `rethunk-git_git_worktree_add` | Create a new linked worktree, creating the branch from `baseRef` if it does not yet exist. Refuses on protected branch names. Args: `path`, `branch`, `baseRef?`, plus workspace pick + `format`. **Mutating.** |
@@ -415,7 +418,9 @@ The response contains one **`parity[]`** entry per resolved git toplevel. `absol
415
418
  |-----------|------|---------|-------|
416
419
  | `base` | string | — | Base ref for a revision diff. When omitted with no `staged`, the tool shows unstaged changes. |
417
420
  | `head` | string | `HEAD` | Head ref for a revision diff. Used only when `base` is provided. |
418
- | `path` | string | — | Optional single file path to scope the diff. |
421
+ | `path` | string | — | Optional single file path to scope the diff. Confined to the repo (`path_escapes_repo` on escape). |
422
+ | `paths` | string[] | — | Multiple file paths to scope the diff; unioned with `path` (deduped). Each confined to the repo. |
423
+ | `unified` | integer | — | Context lines around each change (passed as `-U<n>`, 0–100). Omit for git's default (3). |
419
424
  | `staged` | boolean | `false` | When `true`, runs `git diff --staged`. Ignored when `base` is provided. |
420
425
  | `workspaceRoot` | string | — | Explicit root; highest priority. |
421
426
  | `rootIndex` | int | — | Pick one of several MCP roots (0-based). |
@@ -437,6 +442,7 @@ The response contains one **`parity[]`** entry per resolved git toplevel. `absol
437
442
  | Code | Meaning |
438
443
  |------|---------|
439
444
  | `unsafe_range_token` | `base` or `head` contains characters outside the argv-safe subset. |
445
+ | `path_escapes_repo` | A `path` / `paths` entry resolves outside the git toplevel. |
440
446
  | `git_diff_failed` | `git diff` exited non-zero. |
441
447
  | `not_a_git_repository` | The resolved workspace root is not inside a git repository. |
442
448
 
@@ -448,6 +454,8 @@ The response contains one **`parity[]`** entry per resolved git toplevel. `absol
448
454
  |-----------|------|-------|
449
455
  | `ref` | string | Commit, branch, tag, or other git rev-spec to inspect. |
450
456
  | `path` | string | Optional single path. When provided, the response shows that path's content at `ref` instead of the full commit diff. |
457
+ | `stat` | boolean | When `true`, runs `git show --stat` — commit message plus per-file diffstat, no full patch (`statOutput` in JSON). |
458
+ | `paths` | string[] | Filter the shown patch/stat to these repo-relative paths; unioned with `path`. Each confined to the repo. |
451
459
  | `workspaceRoot`, `rootIndex`, `format` | — | Standard single-repo workspace pick + output format. |
452
460
 
453
461
  ### `git_show` — JSON shape (`format: "json"`)
@@ -461,7 +469,7 @@ The response contains one **`parity[]`** entry per resolved git toplevel. `absol
461
469
  }
462
470
  ```
463
471
 
464
- `path` is omitted when not requested. `diff` is omitted when `git show` returns only a commit message.
472
+ `path` is omitted when not requested; `paths` is present when multiple paths were given. `diff` is omitted when `git show` returns only a commit message. With `stat: true`, `stat` is `true` and `statOutput` carries the diffstat instead of a full `diff`.
465
473
 
466
474
  ### `git_show` — error codes
467
475
 
@@ -517,11 +525,14 @@ The response contains one **`parity[]`** entry per resolved git toplevel. `absol
517
525
  "remote": "origin",
518
526
  "updatedRefs": ["abc1234..def5678 main -> origin/main"],
519
527
  "newRefs": ["[new tag] v2.0.0 -> v2.0.0"],
528
+ "updated": [{ "ref": "refs/remotes/origin/main", "oldSha": "abc1234…", "newSha": "def5678…", "flag": " " }],
529
+ "created": [{ "ref": "refs/tags/v2.0.0", "newSha": "0a1b2c3…", "flag": "*" }],
530
+ "pruned": [{ "ref": "refs/remotes/origin/old" }],
520
531
  "output": "From origin\n..."
521
532
  }
522
533
  ```
523
534
 
524
- Fetch failures are reported as `ok: false` with the captured git output in `output`.
535
+ `updated` / `created` / `pruned` are structured ref deltas parsed from `git fetch --porcelain` (git ≥ 2.41), each omitted when empty. On older git the `--porcelain` option is detected as unsupported and the tool falls back to a plain fetch, omitting the structured arrays; the `updatedRefs` / `newRefs` string fields are always present for back-compat. Fetch failures are reported as `ok: false` with the captured git output in `output`.
525
536
 
526
537
  ### `git_fetch` — error codes
527
538
 
@@ -533,6 +544,101 @@ Fetch failures are reported as `ok: false` with the captured git output in `outp
533
544
 
534
545
  ---
535
546
 
547
+ ### `git_blame` — parameters
548
+
549
+ | Parameter | Type | Default | Notes |
550
+ |-----------|------|---------|-------|
551
+ | `path` | string | — | **Required.** Repo-relative file to annotate. Confined to the repo (`path_escapes_repo` on escape). |
552
+ | `ref` | string | working tree | Commit-ish to blame at. Validated as a safe ref token. |
553
+ | `startLine` | int | — | Start of a line range (`-L`). Requires `endLine`. |
554
+ | `endLine` | int | — | End of the line range, inclusive. Requires `startLine`. |
555
+ | `workspaceRoot`, `rootIndex`, `format` | — | Standard single-repo workspace pick + output format. |
556
+
557
+ ### `git_blame` — JSON shape (`format: "json"`)
558
+
559
+ ```json
560
+ {
561
+ "ref": "HEAD",
562
+ "path": "src/server.ts",
563
+ "lines": [
564
+ { "line": 1, "sha": "a1b2c3d4…", "author": "Damon Blais", "date": "2026-04-12T18:32:01-07:00", "summary": "feat: add tool", "content": "import { FastMCP } from \"fastmcp\";" }
565
+ ]
566
+ }
567
+ ```
568
+
569
+ `ref` is omitted when blaming the working tree.
570
+
571
+ ### `git_blame` — error codes
572
+
573
+ | Code | Meaning |
574
+ |------|---------|
575
+ | `path_escapes_repo` | `path` resolves outside the git toplevel. |
576
+ | `unsafe_ref_token` | `ref` contains characters outside the argv-safe subset. |
577
+ | `invalid_line_range` | Only one of `startLine`/`endLine` was given, or `startLine > endLine`. |
578
+ | `git_blame_failed` | `git blame` exited non-zero (unknown path/ref). `detail` carries stderr. |
579
+ | `not_a_git_repository` | The resolved workspace root is not inside a git repository. |
580
+
581
+ ---
582
+
583
+ ### `git_branch_list` — parameters
584
+
585
+ | Parameter | Type | Default | Notes |
586
+ |-----------|------|---------|-------|
587
+ | `includeRemotes` | boolean | `false` | Also list remote-tracking branches (`refs/remotes`); symbolic `origin/HEAD` is skipped. |
588
+ | `workspaceRoot`, `rootIndex`, `format` | — | Standard single-repo workspace pick + output format. |
589
+
590
+ ### `git_branch_list` — JSON shape (`format: "json"`)
591
+
592
+ ```json
593
+ {
594
+ "branches": [
595
+ { "name": "main", "sha": "a1b2c3d4…", "current": true, "upstream": "origin/main" },
596
+ { "name": "feature/x", "sha": "b2c3d4e5…", "current": false }
597
+ ],
598
+ "remotes": [{ "name": "origin/main", "sha": "a1b2c3d4…" }]
599
+ }
600
+ ```
601
+
602
+ `upstream` is omitted when a branch has no upstream. `remotes` is present only when `includeRemotes: true`.
603
+
604
+ ### `git_branch_list` — error codes
605
+
606
+ | Code | Meaning |
607
+ |------|---------|
608
+ | `branch_list_failed` | `git for-each-ref` exited non-zero. `detail` carries stderr. |
609
+ | `not_a_git_repository` | The resolved workspace root is not inside a git repository. |
610
+
611
+ ---
612
+
613
+ ### `git_reflog` — parameters
614
+
615
+ | Parameter | Type | Default | Notes |
616
+ |-----------|------|---------|-------|
617
+ | `ref` | string | `HEAD` | Ref whose reflog to show. Validated as a safe ref token. |
618
+ | `maxEntries` | int | `30` | Max entries to return (1–200). |
619
+ | `workspaceRoot`, `rootIndex`, `format` | — | Standard single-repo workspace pick + output format. |
620
+
621
+ ### `git_reflog` — JSON shape (`format: "json"`)
622
+
623
+ ```json
624
+ {
625
+ "ref": "HEAD",
626
+ "entries": [
627
+ { "sha": "a1b2c3d4…", "selector": "HEAD@{0}", "message": "commit: feat: add tool" }
628
+ ]
629
+ }
630
+ ```
631
+
632
+ ### `git_reflog` — error codes
633
+
634
+ | Code | Meaning |
635
+ |------|---------|
636
+ | `unsafe_ref_token` | `ref` contains characters outside the argv-safe subset. |
637
+ | `reflog_failed` | `git reflog show` exited non-zero. `detail` carries stderr. |
638
+ | `not_a_git_repository` | The resolved workspace root is not inside a git repository. |
639
+
640
+ ---
641
+
536
642
  ### `batch_commit` — atomic staging semantics
537
643
 
538
644
  **Critical for AI agents:** Each call to `batch_commit` is **self-contained and atomic per-commit entry**.
@@ -917,6 +1023,13 @@ For deletions, `type` is `"deleted"` and `sha` is an empty string.
917
1023
 
918
1024
  ---
919
1025
 
1026
+ ## Environment
1027
+
1028
+ | Variable | Default | Notes |
1029
+ |----------|---------|-------|
1030
+ | `GIT_SUBPROCESS_PARALLELISM` | CPU-based | Max concurrent git subprocesses for multi-root fan-out (`git_inventory`, `git_parity`, multi-root `git_log`). |
1031
+ | `GIT_SUBPROCESS_TIMEOUT_MS` | `120000` | Per-subprocess timeout in ms; on expiry the child is killed (SIGTERM) and the call resolves as failed. Set `0` (or negative) to disable (unbounded). |
1032
+
920
1033
  ## Resource
921
1034
 
922
1035
  | URI | Purpose |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rethunk/mcp-multi-root-git",
3
- "version": "2.6.0",
3
+ "version": "2.8.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,
@@ -69,7 +69,7 @@
69
69
  "zod": "^4.4.3"
70
70
  },
71
71
  "devDependencies": {
72
- "@biomejs/biome": "^2.4.15",
72
+ "@biomejs/biome": "^2.4.16",
73
73
  "@types/node": "^25.9.1",
74
74
  "rimraf": "^6.1.3",
75
75
  "typescript": "^6.0.3"
@@ -0,0 +1,52 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "title": "@rethunk/mcp-multi-root-git: git_blame",
4
+ "description": "Parameter schema for the 'git_blame' MCP tool.",
5
+ "type": "object",
6
+ "properties": {
7
+ "workspaceRoot": {
8
+ "description": "Highest-priority override.",
9
+ "type": "string"
10
+ },
11
+ "rootIndex": {
12
+ "description": "0-based index into the MCP file roots list; ignored when workspaceRoot is set.",
13
+ "type": "integer",
14
+ "minimum": 0,
15
+ "maximum": 9007199254740991
16
+ },
17
+ "format": {
18
+ "default": "markdown",
19
+ "type": "string",
20
+ "enum": [
21
+ "markdown",
22
+ "json"
23
+ ]
24
+ },
25
+ "path": {
26
+ "type": "string",
27
+ "minLength": 1,
28
+ "description": "Repo-relative path to the file to blame."
29
+ },
30
+ "ref": {
31
+ "description": "Optional commit-ish (SHA, branch, tag) to blame at.",
32
+ "type": "string"
33
+ },
34
+ "startLine": {
35
+ "description": "First line of the range to blame (1-based). Requires endLine.",
36
+ "type": "integer",
37
+ "minimum": 1,
38
+ "maximum": 9007199254740991
39
+ },
40
+ "endLine": {
41
+ "description": "Last line of the range to blame (1-based, inclusive). Requires startLine.",
42
+ "type": "integer",
43
+ "minimum": 1,
44
+ "maximum": 9007199254740991
45
+ }
46
+ },
47
+ "required": [
48
+ "format",
49
+ "path"
50
+ ],
51
+ "additionalProperties": false
52
+ }
@@ -0,0 +1,36 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "title": "@rethunk/mcp-multi-root-git: git_branch_list",
4
+ "description": "Parameter schema for the 'git_branch_list' MCP tool.",
5
+ "type": "object",
6
+ "properties": {
7
+ "workspaceRoot": {
8
+ "description": "Highest-priority override.",
9
+ "type": "string"
10
+ },
11
+ "rootIndex": {
12
+ "description": "0-based index into the MCP file roots list; ignored when workspaceRoot is set.",
13
+ "type": "integer",
14
+ "minimum": 0,
15
+ "maximum": 9007199254740991
16
+ },
17
+ "format": {
18
+ "default": "markdown",
19
+ "type": "string",
20
+ "enum": [
21
+ "markdown",
22
+ "json"
23
+ ]
24
+ },
25
+ "includeRemotes": {
26
+ "default": false,
27
+ "description": "When true, also return remote-tracking branches from refs/remotes (excluding symbolic origin/HEAD).",
28
+ "type": "boolean"
29
+ }
30
+ },
31
+ "required": [
32
+ "format",
33
+ "includeRemotes"
34
+ ],
35
+ "additionalProperties": false
36
+ }
@@ -31,13 +31,26 @@
31
31
  "type": "string"
32
32
  },
33
33
  "path": {
34
- "description": "Scope diff to a single file path (e.g. \"src/main.ts\").",
34
+ "description": "Scope diff to a single file path (e.g. \"src/main.ts\"). For multiple files, prefer `paths`. If both `path` and `paths` are given, they are unioned.",
35
35
  "type": "string"
36
36
  },
37
+ "paths": {
38
+ "description": "Scope diff to multiple file paths (e.g. [\"src/a.ts\", \"src/b.ts\"]). Each path is validated and must lie within the repository root. If both `path` and `paths` are given, they are unioned.",
39
+ "type": "array",
40
+ "items": {
41
+ "type": "string"
42
+ }
43
+ },
37
44
  "staged": {
38
45
  "default": false,
39
46
  "description": "If true, show staged changes (git diff --staged). Ignored if `base` is provided.",
40
47
  "type": "boolean"
48
+ },
49
+ "unified": {
50
+ "description": "Number of context lines to show around each change (passed as -U<n> to git diff). Defaults to git's built-in default (3). Use 0 for no context.",
51
+ "type": "integer",
52
+ "minimum": 0,
53
+ "maximum": 100
41
54
  }
42
55
  },
43
56
  "required": [
@@ -0,0 +1,44 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "title": "@rethunk/mcp-multi-root-git: git_reflog",
4
+ "description": "Parameter schema for the 'git_reflog' MCP tool.",
5
+ "type": "object",
6
+ "properties": {
7
+ "workspaceRoot": {
8
+ "description": "Highest-priority override.",
9
+ "type": "string"
10
+ },
11
+ "rootIndex": {
12
+ "description": "0-based index into the MCP file roots list; ignored when workspaceRoot is set.",
13
+ "type": "integer",
14
+ "minimum": 0,
15
+ "maximum": 9007199254740991
16
+ },
17
+ "format": {
18
+ "default": "markdown",
19
+ "type": "string",
20
+ "enum": [
21
+ "markdown",
22
+ "json"
23
+ ]
24
+ },
25
+ "ref": {
26
+ "default": "HEAD",
27
+ "description": "Ref whose reflog to show (branch name, HEAD, etc.). Default: HEAD.",
28
+ "type": "string"
29
+ },
30
+ "maxEntries": {
31
+ "default": 30,
32
+ "description": "Maximum reflog entries to return (hard cap 200). Default 30.",
33
+ "type": "integer",
34
+ "minimum": 1,
35
+ "maximum": 200
36
+ }
37
+ },
38
+ "required": [
39
+ "format",
40
+ "ref",
41
+ "maxEntries"
42
+ ],
43
+ "additionalProperties": false
44
+ }
@@ -28,8 +28,19 @@
28
28
  "description": "Commit reference (SHA, branch, tag, or any git rev-spec)."
29
29
  },
30
30
  "path": {
31
- "description": "Optional file path to inspect at the ref. If provided, shows that path's content at the ref instead of the diff.",
31
+ "description": "Optional single file path to inspect at the ref. Merged with `paths` when both are provided.",
32
32
  "type": "string"
33
+ },
34
+ "paths": {
35
+ "description": "Optional list of file paths to filter the shown diff/stat. Merged with `path` when both are provided.",
36
+ "type": "array",
37
+ "items": {
38
+ "type": "string"
39
+ }
40
+ },
41
+ "stat": {
42
+ "description": "When true, show --stat diffstat (files changed summary) instead of the full patch.",
43
+ "type": "boolean"
33
44
  }
34
45
  },
35
46
  "required": [
@@ -54,6 +54,21 @@
54
54
  "file": "git_stash_list.json",
55
55
  "description": "Parameter schema for the 'git_stash_list' MCP tool."
56
56
  },
57
+ {
58
+ "name": "git_blame",
59
+ "file": "git_blame.json",
60
+ "description": "Parameter schema for the 'git_blame' MCP tool."
61
+ },
62
+ {
63
+ "name": "git_branch_list",
64
+ "file": "git_branch_list.json",
65
+ "description": "Parameter schema for the 'git_branch_list' MCP tool."
66
+ },
67
+ {
68
+ "name": "git_reflog",
69
+ "file": "git_reflog.json",
70
+ "description": "Parameter schema for the 'git_reflog' MCP tool."
71
+ },
57
72
  {
58
73
  "name": "git_fetch",
59
74
  "file": "git_fetch.json",