@rethunk/mcp-multi-root-git 2.3.2 → 2.3.4

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/AGENTS.md CHANGED
@@ -17,9 +17,9 @@ IDEs injecting this as context: do not re-link from rules.
17
17
  | [`src/server.ts`](src/server.ts) | `FastMCP` + `roots: { enabled: true }`; `readMcpServerVersion()`; `registerRethunkGitTools` |
18
18
  | [`src/server/json.ts`](src/server/json.ts) | `jsonRespond()` (minified, no envelope), `spreadWhen`, `spreadDefined` |
19
19
  | [`src/server/git.ts`](src/server/git.ts) | `gateGit`, `spawnGitAsync`, `asyncPool`, `GIT_SUBPROCESS_PARALLELISM`, `gitTopLevel`, `gitRevParseGitDir`, `gitRevParseHead`, `parseGitSubmodulePaths`, `hasGitMetadata`, `gitStatusSnapshotAsync`, `gitStatusShortBranchAsync`, `fetchAheadBehind`, `isSafeGitUpstreamToken` |
20
- | [`src/server/roots.ts`](src/server/roots.ts) | `requireGitAndRoots`, `requireSingleRepo` — shared tool preludes; session root resolution |
20
+ | [`src/server/roots.ts`](src/server/roots.ts) | `requireGitAndRoots`, `requireSingleRepo`, `resolveAbsoluteGitRootsList`, `GitRootPickArgs` — shared tool preludes; session root resolution; optional `absoluteGitRoots` bulk pick |
21
21
  | [`src/server/presets.ts`](src/server/presets.ts) | `PRESET_FILE_PATH`, `loadPresetsFromGitTop`, `presetLoadErrorPayload`, `applyPresetNestedRoots`, `applyPresetParityPairs`; Zod schemas must match [`git-mcp-presets.schema.json`](git-mcp-presets.schema.json) |
22
- | [`src/server/schemas.ts`](src/server/schemas.ts) | `WorkspacePickSchema`, `MAX_INVENTORY_ROOTS_DEFAULT` |
22
+ | [`src/server/schemas.ts`](src/server/schemas.ts) | `WorkspacePickSchema`, `MAX_INVENTORY_ROOTS_DEFAULT`, **`MAX_ABSOLUTE_GIT_ROOTS`** (256), optional **`absoluteGitRoots`** on workspace pick |
23
23
  | [`src/server/inventory.ts`](src/server/inventory.ts) | `InventoryEntryJson`, `validateRepoPath`, `makeSkipEntry`, `buildInventorySectionMarkdown`, `collectInventoryEntry` |
24
24
  | [`src/server/git-refs.ts`](src/server/git-refs.ts) | `isProtectedBranch`, `isSafeGitRefToken`, `isSafeGitRangeToken`, `isSafeGitAncestorRef`; `getCurrentBranch`, `resolveRef`, `isWorkingTreeClean`, `isFullyMergedInto`, `commitListBetween`; `listWorktrees`, `worktreeForBranch`; `conflictPaths` |
25
25
  | [`src/server/tools.ts`](src/server/tools.ts) | `registerRethunkGitTools` — dispatches to `register*` below |
@@ -36,6 +36,7 @@ IDEs injecting this as context: do not re-link from rules.
36
36
  | [`src/server/git-cherry-pick-tool.ts`](src/server/git-cherry-pick-tool.ts) | `git_cherry_pick` — mutating |
37
37
  | [`src/server/git-reset-soft-tool.ts`](src/server/git-reset-soft-tool.ts) | `git_reset_soft` — soft-reset; mutating |
38
38
  | [`src/server/presets-resource.ts`](src/server/presets-resource.ts) | `rethunk-git://presets` resource |
39
+ | [`src/server/tool-parameter-schemas.ts`](src/server/tool-parameter-schemas.ts) | `buildToolParameterSchemaDocument`, `captureToolParameterSchemas`; backs `tool-parameters.schema.json` |
39
40
  | [`src/repo-paths.ts`](src/repo-paths.ts) | `resolvePathForRepo`, `assertRelativePathUnderTop`, `isStrictlyUnderGitTop` |
40
41
 
41
42
  ## Changing contracts
@@ -47,7 +48,7 @@ IDEs injecting this as context: do not re-link from rules.
47
48
 
48
49
  ## Validate + CI
49
50
 
50
- Local: `bun run build` | `bun run check` | `bun run test`. CI ([`ci.yml`](.github/workflows/ci.yml)) runs same on PRs + `main` after `bun install --frozen-lockfile`, uploads prerelease `npm pack` artifact. Tag `v*.*.*` matching `package.json` version → [`release.yml`](.github/workflows/release.yml) publishes to GitHub Packages as `@rethunk-ai/mcp-multi-root-git` + cuts GitHub Release. npmjs publish is manual (see [HUMANS.md](HUMANS.md)).
51
+ Local: `bun run build` | `bun run check` | `bun run schema:tools:check` | `bun run test`. CI ([`ci.yml`](.github/workflows/ci.yml)) runs same on PRs + `main` after `bun install --frozen-lockfile`, then `bun run test:coverage` + `bun run coverage:check`, and uploads prerelease `npm pack` artifact. Tag `v*.*.*` matching `package.json` version → [`release.yml`](.github/workflows/release.yml) publishes to GitHub Packages as `@rethunk-ai/mcp-multi-root-git` + cuts GitHub Release. npmjs publish is manual (see [HUMANS.md](HUMANS.md)).
51
52
 
52
53
  Optional [`.githooks/`](.githooks): `bun run setup-hooks` once per clone. pre-commit=`check`; pre-push=frozen install + build + check + test.
53
54
 
package/CHANGELOG.md CHANGED
@@ -2,6 +2,34 @@
2
2
 
3
3
  All notable changes to `@rethunk/mcp-multi-root-git` are documented here. Format loosely follows [Keep a Changelog](https://keepachangelog.com); the project uses [Semantic Versioning](https://semver.org).
4
4
 
5
+ ## [2.3.4] — 2026-04-26
6
+
7
+ Publication-prep patch for the `absoluteGitRoots` line.
8
+
9
+ ### Added
10
+
11
+ - **`tool-parameters.schema.json`** — generated JSON Schema snapshot for every registered tool parameter surface, plus `bun run schema:tools` / `bun run schema:tools:check`.
12
+ - **`git_parity` absolute-root regression coverage** — sibling clone batches are now covered directly.
13
+
14
+ ### Fixed
15
+
16
+ - **CI coverage gate** now checks `% Lines` from Bun's coverage table instead of accidentally reading `% Funcs`.
17
+
18
+ ### Documentation
19
+
20
+ - **`HUMANS.md`** — added sibling-clone `absoluteGitRoots` examples for `git_status` and `git_parity`.
21
+ - **`docs/mcp-tools.md`** — clarified direct `git_push` use for already-committed work and `git_parity` sibling-clone batches.
22
+
23
+ ## [2.3.3] — 2026-04-21
24
+
25
+ ### Added
26
+
27
+ - **`absoluteGitRoots`** on the workspace pick schema: pass absolute paths to many independent git clones in one MCP call for **`git_status`**, **`git_inventory`**, **`git_log`**, **`git_parity`**, **`git_diff_summary`** (single distinct toplevel only), and **`list_presets`**. Mutating tools omit this parameter from their Zod surface. See **`docs/mcp-tools.md`** (*Workspace root resolution*).
28
+
29
+ ### Changed
30
+
31
+ - **`requireGitAndRoots`** / **`requireSingleRepo`**: new prelude **`resolveAbsoluteGitRootsList`** with dedupe, fail-fast on invalid paths, and mutual exclusion with `workspaceRoot` / `rootIndex` / `allWorkspaceRoots` / `preset` (and `nestedRoots`+`preset` guarded in **`git_inventory`**).
32
+
5
33
  ## [2.3.2] — 2026-04-21
6
34
 
7
35
  ### CI
package/HUMANS.md CHANGED
@@ -53,6 +53,22 @@ With **multiple MCP file roots**, the server picks a root whose git toplevel def
53
53
 
54
54
  If you installed from **GitHub Packages**, use **`./node_modules/@rethunk-ai/mcp-multi-root-git/git-mcp-presets.schema.json`** in **`$schema`** instead (see [docs/install.md](docs/install.md#github-packages)).
55
55
 
56
+ ## Tool parameter schema artifact
57
+
58
+ The package ships **`tool-parameters.schema.json`**, a generated JSON Schema snapshot of every registered tool's parameter schema. Maintainers regenerate it with:
59
+
60
+ ```bash
61
+ bun run schema:tools
62
+ ```
63
+
64
+ CI and `prepublishOnly` use:
65
+
66
+ ```bash
67
+ bun run schema:tools:check
68
+ ```
69
+
70
+ This artifact is for inspection, drift checks, and clients that want an offline schema snapshot. Runtime MCP schema discovery remains the source of truth for connected clients.
71
+
56
72
  **Layouts:**
57
73
 
58
74
  1. **Wrapped (recommended):** `{ "schemaVersion": "1", "presets": { "<name>": { ... } } }`.
@@ -74,6 +90,37 @@ Invalid JSON or schema errors return **`preset_file_invalid`** (not a silent emp
74
90
 
75
91
  Relative preset paths must stay inside the git toplevel; escapes are rejected.
76
92
 
93
+ ## Sibling clone batches
94
+
95
+ Use **`absoluteGitRoots`** when you want one read-only call to inspect independent sibling clones that are not all exposed as MCP workspace roots. This is most useful from agent workflows rooted at a parent directory or when an MCP client exposes only one repo root.
96
+
97
+ Example `git_status` batch:
98
+
99
+ ```json
100
+ {
101
+ "format": "json",
102
+ "absoluteGitRoots": [
103
+ "/usr/local/src/com.github/Rethunk-AI/mcp-multi-root-git",
104
+ "/usr/local/src/com.github/Rethunk-AI/rethunk-github-mcp"
105
+ ]
106
+ }
107
+ ```
108
+
109
+ Example `git_parity` batch using the same pair in each sibling clone:
110
+
111
+ ```json
112
+ {
113
+ "format": "json",
114
+ "absoluteGitRoots": [
115
+ "/usr/local/src/com.github/Rethunk-AI/mcp-multi-root-git",
116
+ "/usr/local/src/com.github/Rethunk-AI/rethunk-github-mcp"
117
+ ],
118
+ "pairs": [{ "left": "packages/shared", "right": "apps/web/shared", "label": "shared" }]
119
+ }
120
+ ```
121
+
122
+ `absoluteGitRoots` is read-only by design. Mutating tools such as **`batch_commit`**, **`git_push`**, **`git_merge`**, and **`git_cherry_pick`** omit it from their schema; use `workspaceRoot` or MCP roots for writes. Full parameter rules and error codes live in **[docs/mcp-tools.md](docs/mcp-tools.md#absoluteGitRoots-sibling-clones)**.
123
+
77
124
  ## `git_not_found`
78
125
 
79
126
  If **`git`** is missing or not runnable, tools and the presets resource respond with **`git_not_found`**. Prerequisites and `PATH`: **[docs/install.md](docs/install.md)**.
@@ -97,6 +144,14 @@ Tag pushes run [`.github/workflows/release.yml`](.github/workflows/release.yml):
97
144
 
98
145
  Prerequisite: push a **semver git tag** `vX.Y.Z` that **exactly matches** `version` in `package.json` (e.g. `v1.2.3` and `"version": "1.2.3"`).
99
146
 
147
+ Before tagging, run the clean-tree preflight from the release commit:
148
+
149
+ ```bash
150
+ bun run publish:preflight
151
+ ```
152
+
153
+ It verifies the `package.json` version has a matching `CHANGELOG.md` section, generated tool schemas are current, build/check/coverage pass, and `npm pack --dry-run` includes the expected package files.
154
+
100
155
  ### npmjs (manual) — maintainers only
101
156
 
102
157
  npmjs no longer fits an unattended CI publish flow for this org; **do not** rely on automation to [npmjs](https://www.npmjs.com/package/@rethunk/mcp-multi-root-git). To publish the **same** package name consumers already use (**`@rethunk/mcp-multi-root-git`**):
@@ -53,7 +53,7 @@ export function registerBatchCommitTool(server) {
53
53
  destructiveHint: false,
54
54
  idempotentHint: false,
55
55
  },
56
- parameters: WorkspacePickSchema.extend({
56
+ parameters: WorkspacePickSchema.omit({ absoluteGitRoots: true }).extend({
57
57
  commits: z
58
58
  .array(CommitEntrySchema)
59
59
  .min(1)
@@ -0,0 +1,22 @@
1
+ export function parseAllFilesLineCoverage(output) {
2
+ const rows = output
3
+ .split(/\r?\n/)
4
+ .map((line) => line.trim())
5
+ .filter((line) => line.includes("|"));
6
+ const header = rows.find((line) => /^File\s*\|/i.test(line));
7
+ if (!header)
8
+ return null;
9
+ const columns = header.split("|").map((column) => column.trim().toLowerCase());
10
+ const lineIndex = columns.indexOf("% lines");
11
+ if (lineIndex === -1)
12
+ return null;
13
+ const allFiles = rows.find((line) => /^All files\s*\|/i.test(line));
14
+ if (!allFiles)
15
+ return null;
16
+ const cells = allFiles.split("|").map((cell) => cell.trim());
17
+ const raw = cells[lineIndex];
18
+ if (!raw)
19
+ return null;
20
+ const value = Number.parseFloat(raw);
21
+ return Number.isFinite(value) ? value : null;
22
+ }
@@ -103,7 +103,7 @@ export function registerGitCherryPickTool(server) {
103
103
  destructiveHint: false,
104
104
  idempotentHint: false,
105
105
  },
106
- parameters: WorkspacePickSchema.extend({
106
+ parameters: WorkspacePickSchema.omit({ absoluteGitRoots: true }).extend({
107
107
  sources: z
108
108
  .array(z.string().min(1))
109
109
  .min(1)
@@ -25,6 +25,11 @@ export function registerGitInventoryTool(server) {
25
25
  maxRoots: z.number().int().min(1).max(256).optional().default(MAX_INVENTORY_ROOTS_DEFAULT),
26
26
  }),
27
27
  execute: async (args) => {
28
+ if (args.absoluteGitRoots != null && args.absoluteGitRoots.length > 0) {
29
+ if (args.preset || (args.nestedRoots?.length ?? 0) > 0) {
30
+ return jsonRespond({ error: "absolute_git_roots_nested_or_preset_conflict" });
31
+ }
32
+ }
28
33
  const pre = requireGitAndRoots(server, args, args.preset);
29
34
  if (!pre.ok) {
30
35
  return jsonRespond(pre.error);
@@ -217,7 +217,7 @@ export function registerGitMergeTool(server) {
217
217
  destructiveHint: false,
218
218
  idempotentHint: false,
219
219
  },
220
- parameters: WorkspacePickSchema.extend({
220
+ parameters: WorkspacePickSchema.omit({ absoluteGitRoots: true }).extend({
221
221
  sources: z
222
222
  .array(z.string().min(1))
223
223
  .min(1)
@@ -15,7 +15,7 @@ export function registerGitPushTool(server) {
15
15
  destructiveHint: false,
16
16
  idempotentHint: false,
17
17
  },
18
- parameters: WorkspacePickSchema.extend({
18
+ parameters: WorkspacePickSchema.omit({ absoluteGitRoots: true }).extend({
19
19
  remote: z
20
20
  .string()
21
21
  .optional()
@@ -16,7 +16,7 @@ export function registerGitResetSoftTool(server) {
16
16
  destructiveHint: false,
17
17
  idempotentHint: false,
18
18
  },
19
- parameters: WorkspacePickSchema.extend({
19
+ parameters: WorkspacePickSchema.omit({ absoluteGitRoots: true }).extend({
20
20
  ref: z
21
21
  .string()
22
22
  .min(1)
@@ -55,7 +55,7 @@ export function registerGitWorktreeAddTool(server) {
55
55
  destructiveHint: false,
56
56
  idempotentHint: false,
57
57
  },
58
- parameters: WorkspacePickSchema.extend({
58
+ parameters: WorkspacePickSchema.omit({ absoluteGitRoots: true }).extend({
59
59
  path: z
60
60
  .string()
61
61
  .min(1)
@@ -139,7 +139,7 @@ export function registerGitWorktreeRemoveTool(server) {
139
139
  destructiveHint: true,
140
140
  idempotentHint: false,
141
141
  },
142
- parameters: WorkspacePickSchema.extend({
142
+ parameters: WorkspacePickSchema.omit({ absoluteGitRoots: true }).extend({
143
143
  path: z
144
144
  .string()
145
145
  .min(1)
@@ -15,6 +15,7 @@ export function registerListPresetsTool(server) {
15
15
  workspaceRoot: true,
16
16
  rootIndex: true,
17
17
  allWorkspaceRoots: true,
18
+ absoluteGitRoots: true,
18
19
  format: true,
19
20
  }),
20
21
  execute: async (args) => {
@@ -2,6 +2,7 @@ import { basename, resolve } from "node:path";
2
2
  import { fileURLToPath } from "node:url";
3
3
  import { gateGit, gitTopLevel } from "./git.js";
4
4
  import { loadPresetsFromGitTop, presetLoadErrorPayload } from "./presets.js";
5
+ import { MAX_ABSOLUTE_GIT_ROOTS } from "./schemas.js";
5
6
  function uriToPath(uri) {
6
7
  if (!uri.startsWith("file://"))
7
8
  return null;
@@ -39,6 +40,51 @@ function pathMatchesWorkspaceRootHint(rootPath, hint) {
39
40
  return true;
40
41
  return basename(rootPath) === h;
41
42
  }
43
+ function hasExclusiveWorkspacePick(args) {
44
+ if (args.workspaceRoot?.trim())
45
+ return true;
46
+ if (args.rootIndex != null)
47
+ return true;
48
+ if (args.allWorkspaceRoots === true)
49
+ return true;
50
+ return false;
51
+ }
52
+ /**
53
+ * Resolve `absoluteGitRoots` to unique git toplevels (stable order, first occurrence wins).
54
+ */
55
+ export function resolveAbsoluteGitRootsList(raw) {
56
+ if (raw.length > MAX_ABSOLUTE_GIT_ROOTS) {
57
+ return {
58
+ ok: false,
59
+ error: {
60
+ error: "absolute_git_roots_too_many",
61
+ max: MAX_ABSOLUTE_GIT_ROOTS,
62
+ count: raw.length,
63
+ },
64
+ };
65
+ }
66
+ const seen = new Set();
67
+ const tops = [];
68
+ for (const item of raw) {
69
+ const trimmed = item.trim();
70
+ if (trimmed.length === 0) {
71
+ return { ok: false, error: { error: "invalid_absolute_git_root", path: item } };
72
+ }
73
+ const abs = resolve(trimmed);
74
+ const top = gitTopLevel(abs);
75
+ if (!top) {
76
+ return { ok: false, error: { error: "invalid_absolute_git_root", path: abs } };
77
+ }
78
+ if (seen.has(top))
79
+ continue;
80
+ seen.add(top);
81
+ tops.push(top);
82
+ }
83
+ if (tops.length === 0) {
84
+ return { ok: false, error: { error: "absolute_git_roots_empty" } };
85
+ }
86
+ return { ok: true, roots: tops };
87
+ }
42
88
  function resolveWorkspaceRoots(server, args) {
43
89
  if (args.workspaceRoot?.trim()) {
44
90
  return { ok: true, roots: [resolve(args.workspaceRoot.trim())] };
@@ -110,6 +156,16 @@ export function requireGitAndRoots(server, args, presetName) {
110
156
  if (!gg.ok) {
111
157
  return { ok: false, error: gg.body };
112
158
  }
159
+ const abs = args.absoluteGitRoots;
160
+ if (abs != null && abs.length > 0) {
161
+ if (presetName) {
162
+ return { ok: false, error: { error: "absolute_git_roots_preset_conflict" } };
163
+ }
164
+ if (hasExclusiveWorkspacePick(args)) {
165
+ return { ok: false, error: { error: "absolute_git_roots_exclusive" } };
166
+ }
167
+ return resolveAbsoluteGitRootsList(abs);
168
+ }
113
169
  const rootsRes = presetName
114
170
  ? resolveRootsForPreset(server, args, presetName)
115
171
  : resolveWorkspaceRoots(server, args);
@@ -127,6 +183,15 @@ export function requireSingleRepo(server, args, presetName = undefined) {
127
183
  const pre = requireGitAndRoots(server, args, presetName);
128
184
  if (!pre.ok)
129
185
  return pre;
186
+ if (args.absoluteGitRoots != null && args.absoluteGitRoots.length > 0 && pre.roots.length !== 1) {
187
+ return {
188
+ ok: false,
189
+ error: {
190
+ error: "absolute_git_roots_single_repo_only",
191
+ rootCount: pre.roots.length,
192
+ },
193
+ };
194
+ }
130
195
  const rootInput = pre.roots[0];
131
196
  if (!rootInput)
132
197
  return { ok: false, error: { error: "no_workspace_root" } };
@@ -1,6 +1,8 @@
1
1
  import { z } from "zod";
2
2
  import { MAX_INVENTORY_ROOTS_DEFAULT } from "./inventory.js";
3
3
  const FormatSchema = z.enum(["markdown", "json"]).optional().default("markdown");
4
+ /** Max paths in `absoluteGitRoots` (matches `git_inventory` `maxRoots` hard cap). */
5
+ export const MAX_ABSOLUTE_GIT_ROOTS = 256;
4
6
  export const WorkspacePickSchema = z.object({
5
7
  workspaceRoot: z.string().optional().describe("Highest-priority override."),
6
8
  rootIndex: z
@@ -14,6 +16,12 @@ export const WorkspacePickSchema = z.object({
14
16
  .optional()
15
17
  .default(false)
16
18
  .describe("Fan out across all MCP file roots."),
19
+ /** Independent git worktrees (sibling clones). Mutually exclusive with workspaceRoot, rootIndex, allWorkspaceRoots, and (git_inventory) preset/nestedRoots. */
20
+ absoluteGitRoots: z
21
+ .array(z.string())
22
+ .max(MAX_ABSOLUTE_GIT_ROOTS)
23
+ .optional()
24
+ .describe("Absolute paths to git repo roots. Use for many sibling clones under a non-git parent directory."),
17
25
  format: FormatSchema,
18
26
  });
19
27
  export { MAX_INVENTORY_ROOTS_DEFAULT };
@@ -62,7 +62,10 @@ function makeFakeServer() {
62
62
  const server = {
63
63
  sessions: [],
64
64
  addTool(tool) {
65
- tools.push({ name: tool.name, execute: tool.execute });
65
+ tools.push({ name: tool.name, parameters: tool.parameters, execute: tool.execute });
66
+ },
67
+ addResource() {
68
+ // Resource tests do not need transport behavior; tool-surface tests only need registration to complete.
66
69
  },
67
70
  };
68
71
  return { server, tools };
@@ -89,3 +92,8 @@ export function captureTool(register, toolName) {
89
92
  return JSON.stringify(result);
90
93
  };
91
94
  }
95
+ export function captureToolDefinitions(register) {
96
+ const { server, tools } = makeFakeServer();
97
+ register(server);
98
+ return tools;
99
+ }
@@ -0,0 +1,74 @@
1
+ import { z } from "zod";
2
+ import { registerBatchCommitTool } from "./batch-commit-tool.js";
3
+ import { registerGitCherryPickTool } from "./git-cherry-pick-tool.js";
4
+ import { registerGitDiffSummaryTool } from "./git-diff-summary-tool.js";
5
+ import { registerGitInventoryTool } from "./git-inventory-tool.js";
6
+ import { registerGitLogTool } from "./git-log-tool.js";
7
+ import { registerGitMergeTool } from "./git-merge-tool.js";
8
+ import { registerGitParityTool } from "./git-parity-tool.js";
9
+ import { registerGitPushTool } from "./git-push-tool.js";
10
+ import { registerGitResetSoftTool } from "./git-reset-soft-tool.js";
11
+ import { registerGitStatusTool } from "./git-status-tool.js";
12
+ import { registerGitWorktreeAddTool, registerGitWorktreeListTool, registerGitWorktreeRemoveTool, } from "./git-worktree-tool.js";
13
+ import { registerListPresetsTool } from "./list-presets-tool.js";
14
+ export const READ_ONLY_ABSOLUTE_ROOT_TOOLS = [
15
+ "git_status",
16
+ "git_inventory",
17
+ "git_parity",
18
+ "list_presets",
19
+ "git_log",
20
+ "git_diff_summary",
21
+ ];
22
+ export const MUTATING_TOOLS = [
23
+ "batch_commit",
24
+ "git_push",
25
+ "git_merge",
26
+ "git_cherry_pick",
27
+ "git_reset_soft",
28
+ "git_worktree_add",
29
+ "git_worktree_remove",
30
+ ];
31
+ export const ALL_PARAMETER_SCHEMA_TOOLS = [
32
+ ...READ_ONLY_ABSOLUTE_ROOT_TOOLS,
33
+ "git_worktree_list",
34
+ ...MUTATING_TOOLS,
35
+ ];
36
+ function captureParameterTools(register) {
37
+ const tools = [];
38
+ const server = {
39
+ sessions: [],
40
+ addTool(tool) {
41
+ tools.push({ name: tool.name, parameters: tool.parameters });
42
+ },
43
+ };
44
+ register(server);
45
+ return tools;
46
+ }
47
+ export function captureToolParameterSchemas() {
48
+ const tools = captureParameterTools((server) => {
49
+ registerGitStatusTool(server);
50
+ registerGitInventoryTool(server);
51
+ registerGitParityTool(server);
52
+ registerListPresetsTool(server);
53
+ registerGitLogTool(server);
54
+ registerGitDiffSummaryTool(server);
55
+ registerGitWorktreeListTool(server);
56
+ registerBatchCommitTool(server);
57
+ registerGitPushTool(server);
58
+ registerGitMergeTool(server);
59
+ registerGitCherryPickTool(server);
60
+ registerGitResetSoftTool(server);
61
+ registerGitWorktreeAddTool(server);
62
+ registerGitWorktreeRemoveTool(server);
63
+ });
64
+ return Object.fromEntries(tools.map((tool) => [tool.name, z.toJSONSchema(tool.parameters)]));
65
+ }
66
+ export function buildToolParameterSchemaDocument() {
67
+ return {
68
+ $schema: "https://json-schema.org/draft/2020-12/schema",
69
+ title: "@rethunk/mcp-multi-root-git tool parameter schemas",
70
+ description: "JSON Schema snapshots generated from registered FastMCP tool parameter schemas.",
71
+ generatedBy: "scripts/generate-tool-parameters-schema.ts",
72
+ tools: captureToolParameterSchemas(),
73
+ };
74
+ }
package/docs/mcp-tools.md CHANGED
@@ -11,12 +11,12 @@ MCP clients expose tools as `{serverName}_{toolName}`. With the server registere
11
11
 
12
12
  | Short id | Client id (server `rethunk-git`) | Purpose |
13
13
  |----------|-----------------------------------|---------|
14
- | `git_status` | `rethunk-git_git_status` | `git status --short -b` per MCP root and optional submodules (`includeSubmodules`); parallel submodule status. Args include `allWorkspaceRoots`, `rootIndex`, `workspaceRoot`, `format`. **Read-only.** |
15
- | `git_inventory` | `rethunk-git_git_inventory` | Status + ahead/behind per path; default upstream each repo’s `@{u}`; pass **both** `remote` and `branch` for fixed tracking. `nestedRoots`, `preset`, `presetMerge`, `maxRoots`, `format`, plus workspace pick args. **Read-only.** |
16
- | `git_parity` | `rethunk-git_git_parity` | Compare `git rev-parse HEAD` for path pairs. `pairs`, `preset`, `presetMerge`, `format`, plus workspace pick args. **Read-only.** |
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. **Read-only.** |
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`. **Read-only.** |
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.** |
14
+ | `git_status` | `rethunk-git_git_status` | `git status --short -b` per MCP root and optional submodules (`includeSubmodules`); parallel submodule status. Args include `absoluteGitRoots`, `allWorkspaceRoots`, `rootIndex`, `workspaceRoot`, `format`. **Read-only.** |
15
+ | `git_inventory` | `rethunk-git_git_inventory` | Status + ahead/behind per path; default upstream each repo’s `@{u}`; pass **both** `remote` and `branch` for fixed tracking. `nestedRoots`, `preset`, `presetMerge`, `maxRoots`, `format`, plus workspace pick args (`absoluteGitRoots` cannot combine with `preset`/`nestedRoots`). **Read-only.** |
16
+ | `git_parity` | `rethunk-git_git_parity` | Compare `git rev-parse HEAD` for path pairs. `pairs`, `preset`, `presetMerge`, `format`, plus workspace pick args (`absoluteGitRoots` for sibling clone batches). **Read-only.** |
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
+ | `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`. **Read-only.** |
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
20
  | `git_worktree_list` | `rethunk-git_git_worktree_list` | List all worktrees (`git worktree list --porcelain`). Workspace pick + `format`. **Read-only.** |
21
21
  | `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.** |
22
22
  | `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.** |
@@ -32,6 +32,8 @@ Pass **`format: "json"`** on any tool for structured JSON instead of markdown (d
32
32
 
33
33
  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`**.
34
34
 
35
+ The package also ships **`tool-parameters.schema.json`**, generated from the registered Zod parameter schemas via `bun run schema:tools`. Connected MCP clients should still prefer live schema discovery from `initialize` / tool listing; the artifact is for offline inspection and drift checks.
36
+
35
37
  ### v2/v3 field omission (consumer contract)
36
38
 
37
39
  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`.
@@ -106,6 +108,29 @@ v2 field-omission rules still apply: `filesChanged`, `insertions`, `deletions` o
106
108
  | `invalid_paths` | One of the `paths` entries contains shell metacharacters and was rejected. |
107
109
  | `git_log_failed` | `git log` exited non-zero (e.g. unknown branch ref). |
108
110
  | `root_index_out_of_range` | `rootIndex` exceeds the number of MCP file roots. |
111
+ | `absolute_git_roots_exclusive` | `absoluteGitRoots` was combined with `workspaceRoot`, `rootIndex`, or `allWorkspaceRoots: true`. |
112
+ | `absolute_git_roots_preset_conflict` | `absoluteGitRoots` was combined with a `preset` argument (root resolution). |
113
+ | `invalid_absolute_git_root` | An `absoluteGitRoots` entry is empty, not inside a git worktree, or not a directory git recognizes. |
114
+ | `absolute_git_roots_too_many` | More than 256 entries in `absoluteGitRoots`. |
115
+ | `absolute_git_roots_empty` | `absoluteGitRoots` produced zero git toplevels after resolution. |
116
+ | `absolute_git_roots_single_repo_only` | A single-repo tool received `absoluteGitRoots` resolving to more than one distinct git toplevel. |
117
+
118
+ ### `git_parity` — `absoluteGitRoots` example
119
+
120
+ Use **`absoluteGitRoots`** when the same parity pair should be checked across sibling clones that are not all MCP workspace roots:
121
+
122
+ ```json
123
+ {
124
+ "format": "json",
125
+ "absoluteGitRoots": [
126
+ "/usr/local/src/com.github/Rethunk-AI/mcp-multi-root-git",
127
+ "/usr/local/src/com.github/Rethunk-AI/rethunk-github-mcp"
128
+ ],
129
+ "pairs": [{ "left": "packages/shared", "right": "apps/web/shared", "label": "shared" }]
130
+ }
131
+ ```
132
+
133
+ The response contains one **`parity[]`** entry per resolved git toplevel. `absoluteGitRoots` cannot be combined with `preset`; pass inline `pairs` for sibling-clone batches.
109
134
 
110
135
  ### `git_diff_summary` — parameters
111
136
 
@@ -346,6 +371,8 @@ Repo state is cleaned (`git cherry-pick --abort`) before returning — no partia
346
371
 
347
372
  ### `git_push` — parameters
348
373
 
374
+ For already-committed work, call **`git_push`** directly instead of creating an empty commit or falling back to shell. Continue to prefer **`batch_commit`** with **`push: "after"`** when commits and push happen in the same MCP call.
375
+
349
376
  | Parameter | Type | Notes |
350
377
  |-----------|------|-------|
351
378
  | `remote` | string | Remote to push to. Defaults to the remote inferred from the upstream tracking ref, or `origin` when `setUpstream` is true. |
@@ -456,6 +483,32 @@ Repo state is cleaned (`git cherry-pick --abort`) before returning — no partia
456
483
 
457
484
  ## Workspace root resolution
458
485
 
486
+ ### `absoluteGitRoots` (sibling clones)
487
+
488
+ When **`absoluteGitRoots`** is a **non-empty** string array, it **replaces** the normal workspace pick for that tool call:
489
+
490
+ - Each entry is passed through `path.resolve`, then resolved to a **git toplevel** via the same logic as `workspaceRoot`. Duplicate toplevels are dropped (stable order, first wins).
491
+ - **Maximum** **256** paths (same cap as `git_inventory` `maxRoots` upper bound).
492
+ - **Mutually exclusive** with **`workspaceRoot`**, **`rootIndex`**, and **`allWorkspaceRoots: true`**. Combining them returns `{ "error": "absolute_git_roots_exclusive" }`.
493
+ - **Mutually exclusive** with a **`preset`** argument on root resolution (`absolute_git_roots_preset_conflict`).
494
+ - **`git_inventory` only:** also mutually exclusive with **`nestedRoots`** or **`preset`** on the same call (`absolute_git_roots_nested_or_preset_conflict`).
495
+ - **Mutating** tools (`batch_commit`, `git_push`, `git_merge`, …) **omit** this parameter from their schema; callers must use `workspaceRoot` / MCP roots for writes.
496
+ - **Read tools** that use **`requireSingleRepo`** (`git_diff_summary`, …) accept at most **one** distinct toplevel from `absoluteGitRoots`; more than one returns `absolute_git_roots_single_repo_only`.
497
+
498
+ Example — two sibling repos in one `git_status` call:
499
+
500
+ ```json
501
+ {
502
+ "format": "json",
503
+ "absoluteGitRoots": [
504
+ "/usr/local/src/com.github/Rethunk-AI/mcp-multi-root-git",
505
+ "/usr/local/src/com.github/Rethunk-AI/rethunk-github-mcp"
506
+ ]
507
+ }
508
+ ```
509
+
510
+ ### Default order (when `absoluteGitRoots` is absent or empty)
511
+
459
512
  Order applied when resolving which directory(ies) tools run against:
460
513
 
461
514
  1. Explicit **`workspaceRoot`** on the tool call (highest priority).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rethunk/mcp-multi-root-git",
3
- "version": "2.3.2",
3
+ "version": "2.3.4",
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,
@@ -11,6 +11,7 @@
11
11
  "files": [
12
12
  "dist",
13
13
  "git-mcp-presets.schema.json",
14
+ "tool-parameters.schema.json",
14
15
  "docs/install.md",
15
16
  "docs/mcp-tools.md",
16
17
  "AGENTS.md",
@@ -31,9 +32,13 @@
31
32
  "build": "rimraf dist && tsc",
32
33
  "check": "biome check .",
33
34
  "check:fix": "biome check --write .",
35
+ "coverage:check": "bun scripts/check-coverage.ts",
36
+ "schema:tools": "bun scripts/generate-tool-parameters-schema.ts",
37
+ "schema:tools:check": "bun scripts/generate-tool-parameters-schema.ts --check",
38
+ "publish:preflight": "bun scripts/publish-preflight.ts",
34
39
  "test": "bun test src/",
35
40
  "test:coverage": "bun test src/ --coverage",
36
- "prepublishOnly": "bun run build && bun run check && bun run test",
41
+ "prepublishOnly": "bun run schema:tools:check && bun run build && bun run check && bun run test",
37
42
  "setup-hooks": "git config core.hooksPath .githooks"
38
43
  },
39
44
  "keywords": [
@@ -57,13 +62,13 @@
57
62
  "access": "public"
58
63
  },
59
64
  "dependencies": {
60
- "fastmcp": "^3.35.0",
65
+ "fastmcp": "^4.0.1",
61
66
  "zod": "^4.3.6"
62
67
  },
63
68
  "devDependencies": {
64
- "@biomejs/biome": "^2.4.11",
65
- "@types/node": "^22.19.17",
69
+ "@biomejs/biome": "^2.4.13",
70
+ "@types/node": "^25.6.0",
66
71
  "rimraf": "^6.1.3",
67
- "typescript": "^6.0.2"
72
+ "typescript": "^6.0.3"
68
73
  }
69
74
  }