@rethunk/mcp-multi-root-git 2.9.1 → 3.1.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/AGENTS.md +4 -6
- package/CHANGELOG.md +31 -0
- package/HUMANS.md +4 -4
- package/dist/server/batch-commit-tool.js +24 -15
- package/dist/server/error-codes.js +6 -9
- package/dist/server/git-blame-tool.js +60 -16
- package/dist/server/git-branch-list-tool.js +1 -7
- package/dist/server/git-cherry-pick-tool.js +1 -1
- package/dist/server/git-diff-summary-tool.js +3 -3
- package/dist/server/git-diff-tool.js +1 -4
- package/dist/server/git-fetch-tool.js +3 -12
- package/dist/server/git-inventory-tool.js +4 -4
- package/dist/server/git-log-tool.js +2 -2
- package/dist/server/git-merge-tool.js +1 -1
- package/dist/server/git-parity-tool.js +2 -2
- package/dist/server/git-push-tool.js +1 -1
- package/dist/server/git-reflog-tool.js +1 -7
- package/dist/server/git-reset-soft-tool.js +1 -1
- package/dist/server/git-show-tool.js +2 -11
- package/dist/server/git-stash-tool.js +3 -12
- package/dist/server/git-status-tool.js +2 -2
- package/dist/server/git-tag-tool.js +1 -7
- package/dist/server/git-worktree-tool.js +3 -7
- package/dist/server/list-presets-tool.js +2 -8
- package/dist/server/roots.js +41 -80
- package/dist/server/schemas.js +11 -19
- package/dist/server/tool-parameter-schemas.js +3 -3
- package/dist/server.js +1 -1
- package/docs/mcp-tools.md +117 -136
- package/package.json +4 -4
- package/schemas/batch_commit.json +3 -15
- package/schemas/git_blame.json +12 -10
- package/schemas/git_branch_list.json +1 -7
- package/schemas/git_cherry_pick.json +1 -13
- package/schemas/git_diff.json +1 -7
- package/schemas/git_diff_summary.json +1 -15
- package/schemas/git_fetch.json +1 -7
- package/schemas/git_inventory.json +18 -23
- package/schemas/git_log.json +18 -23
- package/schemas/git_merge.json +1 -13
- package/schemas/git_parity.json +18 -23
- package/schemas/git_push.json +1 -13
- package/schemas/git_reflog.json +1 -7
- package/schemas/git_reset_soft.json +1 -13
- package/schemas/git_show.json +1 -7
- package/schemas/git_stash_apply.json +2 -8
- package/schemas/git_stash_list.json +1 -7
- package/schemas/git_status.json +18 -23
- package/schemas/git_tag.json +1 -7
- package/schemas/git_worktree_add.json +1 -13
- package/schemas/git_worktree_list.json +1 -7
- package/schemas/git_worktree_remove.json +1 -13
- package/schemas/list_presets.json +18 -23
- package/tool-parameters.schema.json +122 -297
|
@@ -4,7 +4,7 @@ import { isStrictlyUnderGitTop } from "../repo-paths.js";
|
|
|
4
4
|
import { asyncPool, GIT_SUBPROCESS_PARALLELISM, gitStatusShortBranchAsync, gitTopLevel, hasGitMetadata, parseGitSubmodulePaths, } from "./git.js";
|
|
5
5
|
import { jsonRespond } from "./json.js";
|
|
6
6
|
import { requireGitAndRoots } from "./roots.js";
|
|
7
|
-
import {
|
|
7
|
+
import { RootPickSchema } from "./schemas.js";
|
|
8
8
|
export function registerGitStatusTool(server) {
|
|
9
9
|
server.addTool({
|
|
10
10
|
name: "git_status",
|
|
@@ -12,7 +12,7 @@ export function registerGitStatusTool(server) {
|
|
|
12
12
|
annotations: {
|
|
13
13
|
readOnlyHint: true,
|
|
14
14
|
},
|
|
15
|
-
parameters:
|
|
15
|
+
parameters: RootPickSchema.extend({
|
|
16
16
|
includeSubmodules: z.boolean().optional().default(true),
|
|
17
17
|
}),
|
|
18
18
|
execute: async (args) => {
|
|
@@ -44,13 +44,7 @@ export function registerGitTagTool(server) {
|
|
|
44
44
|
readOnlyHint: false,
|
|
45
45
|
destructiveHint: true,
|
|
46
46
|
},
|
|
47
|
-
parameters: WorkspacePickSchema.
|
|
48
|
-
.pick({
|
|
49
|
-
workspaceRoot: true,
|
|
50
|
-
rootIndex: true,
|
|
51
|
-
format: true,
|
|
52
|
-
})
|
|
53
|
-
.extend({
|
|
47
|
+
parameters: WorkspacePickSchema.extend({
|
|
54
48
|
tag: z.string().min(1).describe("Tag name (e.g. 'v1.2.3')."),
|
|
55
49
|
message: z
|
|
56
50
|
.string()
|
|
@@ -16,11 +16,7 @@ export function registerGitWorktreeListTool(server) {
|
|
|
16
16
|
annotations: {
|
|
17
17
|
readOnlyHint: true,
|
|
18
18
|
},
|
|
19
|
-
parameters: WorkspacePickSchema
|
|
20
|
-
workspaceRoot: true,
|
|
21
|
-
rootIndex: true,
|
|
22
|
-
format: true,
|
|
23
|
-
}),
|
|
19
|
+
parameters: WorkspacePickSchema,
|
|
24
20
|
execute: async (args) => {
|
|
25
21
|
const pre = requireSingleRepo(server, args);
|
|
26
22
|
if (!pre.ok)
|
|
@@ -55,7 +51,7 @@ export function registerGitWorktreeAddTool(server) {
|
|
|
55
51
|
destructiveHint: false,
|
|
56
52
|
idempotentHint: false,
|
|
57
53
|
},
|
|
58
|
-
parameters: WorkspacePickSchema.
|
|
54
|
+
parameters: WorkspacePickSchema.extend({
|
|
59
55
|
path: z
|
|
60
56
|
.string()
|
|
61
57
|
.min(1)
|
|
@@ -137,7 +133,7 @@ export function registerGitWorktreeRemoveTool(server) {
|
|
|
137
133
|
destructiveHint: true,
|
|
138
134
|
idempotentHint: false,
|
|
139
135
|
},
|
|
140
|
-
parameters: WorkspacePickSchema.
|
|
136
|
+
parameters: WorkspacePickSchema.extend({
|
|
141
137
|
path: z
|
|
142
138
|
.string()
|
|
143
139
|
.min(1)
|
|
@@ -4,7 +4,7 @@ import { gitTopLevel } from "./git.js";
|
|
|
4
4
|
import { jsonRespond, spreadDefined } from "./json.js";
|
|
5
5
|
import { loadPresetsFromGitTop, PRESET_FILE_PATH, presetLoadErrorPayload } from "./presets.js";
|
|
6
6
|
import { requireGitAndRoots } from "./roots.js";
|
|
7
|
-
import {
|
|
7
|
+
import { RootPickSchema } from "./schemas.js";
|
|
8
8
|
export function registerListPresetsTool(server) {
|
|
9
9
|
server.addTool({
|
|
10
10
|
name: "list_presets",
|
|
@@ -12,13 +12,7 @@ export function registerListPresetsTool(server) {
|
|
|
12
12
|
annotations: {
|
|
13
13
|
readOnlyHint: true,
|
|
14
14
|
},
|
|
15
|
-
parameters:
|
|
16
|
-
workspaceRoot: true,
|
|
17
|
-
rootIndex: true,
|
|
18
|
-
allWorkspaceRoots: true,
|
|
19
|
-
absoluteGitRoots: true,
|
|
20
|
-
format: true,
|
|
21
|
-
}),
|
|
15
|
+
parameters: RootPickSchema,
|
|
22
16
|
execute: async (args) => {
|
|
23
17
|
const pre = requireGitAndRoots(server, args, undefined);
|
|
24
18
|
if (!pre.ok) {
|
package/dist/server/roots.js
CHANGED
|
@@ -3,7 +3,7 @@ import { fileURLToPath } from "node:url";
|
|
|
3
3
|
import { ERROR_CODES } from "./error-codes.js";
|
|
4
4
|
import { gateGit, gitTopLevel } from "./git.js";
|
|
5
5
|
import { loadPresetsFromGitTop, presetLoadErrorPayload } from "./presets.js";
|
|
6
|
-
import {
|
|
6
|
+
import { MAX_ROOT_PATHS } from "./schemas.js";
|
|
7
7
|
function uriToPath(uri) {
|
|
8
8
|
if (!uri.startsWith("file://"))
|
|
9
9
|
return null;
|
|
@@ -45,25 +45,17 @@ function pathMatchesWorkspaceRootHint(rootPath, hint) {
|
|
|
45
45
|
return true;
|
|
46
46
|
return basename(rootPath) === h;
|
|
47
47
|
}
|
|
48
|
-
function hasExclusiveWorkspacePick(args) {
|
|
49
|
-
if (args.workspaceRoot?.trim())
|
|
50
|
-
return true;
|
|
51
|
-
if (args.rootIndex != null)
|
|
52
|
-
return true;
|
|
53
|
-
if (args.allWorkspaceRoots === true)
|
|
54
|
-
return true;
|
|
55
|
-
return false;
|
|
56
|
-
}
|
|
57
48
|
/**
|
|
58
|
-
* Resolve `
|
|
49
|
+
* Resolve an explicit `root` path array to unique git toplevels
|
|
50
|
+
* (stable order, first occurrence wins).
|
|
59
51
|
*/
|
|
60
|
-
export function
|
|
61
|
-
if (raw.length >
|
|
52
|
+
export function resolveRootPathList(raw) {
|
|
53
|
+
if (raw.length > MAX_ROOT_PATHS) {
|
|
62
54
|
return {
|
|
63
55
|
ok: false,
|
|
64
56
|
error: {
|
|
65
|
-
error: ERROR_CODES.
|
|
66
|
-
max:
|
|
57
|
+
error: ERROR_CODES.ROOT_LIST_TOO_MANY,
|
|
58
|
+
max: MAX_ROOT_PATHS,
|
|
67
59
|
count: raw.length,
|
|
68
60
|
},
|
|
69
61
|
};
|
|
@@ -73,12 +65,12 @@ export function resolveAbsoluteGitRootsList(raw) {
|
|
|
73
65
|
for (const item of raw) {
|
|
74
66
|
const trimmed = item.trim();
|
|
75
67
|
if (trimmed.length === 0) {
|
|
76
|
-
return { ok: false, error: { error: ERROR_CODES.
|
|
68
|
+
return { ok: false, error: { error: ERROR_CODES.INVALID_ROOT_PATH, path: item } };
|
|
77
69
|
}
|
|
78
70
|
const abs = resolve(trimmed);
|
|
79
71
|
const top = gitTopLevel(abs);
|
|
80
72
|
if (!top) {
|
|
81
|
-
return { ok: false, error: { error: ERROR_CODES.
|
|
73
|
+
return { ok: false, error: { error: ERROR_CODES.INVALID_ROOT_PATH, path: abs } };
|
|
82
74
|
}
|
|
83
75
|
if (seen.has(top))
|
|
84
76
|
continue;
|
|
@@ -86,47 +78,23 @@ export function resolveAbsoluteGitRootsList(raw) {
|
|
|
86
78
|
tops.push(top);
|
|
87
79
|
}
|
|
88
80
|
if (tops.length === 0) {
|
|
89
|
-
return { ok: false, error: { error: ERROR_CODES.
|
|
81
|
+
return { ok: false, error: { error: ERROR_CODES.ROOT_LIST_EMPTY } };
|
|
90
82
|
}
|
|
91
83
|
return { ok: true, roots: tops };
|
|
92
84
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
return { ok: true, roots: [resolve(args.workspaceRoot.trim())] };
|
|
96
|
-
}
|
|
97
|
-
const fileRoots = listFileRoots(server);
|
|
98
|
-
const fallback = { ok: true, roots: [process.cwd()] };
|
|
99
|
-
if (args.allWorkspaceRoots) {
|
|
100
|
-
return fileRoots.length === 0 ? fallback : { ok: true, roots: fileRoots };
|
|
101
|
-
}
|
|
102
|
-
if (args.rootIndex != null) {
|
|
103
|
-
const r = fileRoots[args.rootIndex];
|
|
104
|
-
if (!r) {
|
|
105
|
-
return {
|
|
106
|
-
ok: false,
|
|
107
|
-
error: {
|
|
108
|
-
error: ERROR_CODES.ROOT_INDEX_OUT_OF_RANGE,
|
|
109
|
-
rootIndex: args.rootIndex,
|
|
110
|
-
rootCount: fileRoots.length,
|
|
111
|
-
},
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
return { ok: true, roots: [r] };
|
|
115
|
-
}
|
|
85
|
+
/** Default when `root` is omitted: first MCP file root, else cwd. */
|
|
86
|
+
function defaultRoots(fileRoots) {
|
|
116
87
|
const primary = fileRoots[0];
|
|
117
|
-
return
|
|
88
|
+
return { ok: true, roots: [primary ?? process.cwd()] };
|
|
118
89
|
}
|
|
119
90
|
/**
|
|
120
91
|
* When a preset name is requested and multiple MCP roots exist, pick the first root
|
|
121
92
|
* whose git toplevel loads a preset file containing that name.
|
|
122
93
|
*/
|
|
123
|
-
function resolveRootsForPreset(server,
|
|
124
|
-
if (args.workspaceRoot?.trim() || args.allWorkspaceRoots || args.rootIndex != null) {
|
|
125
|
-
return resolveWorkspaceRoots(server, args);
|
|
126
|
-
}
|
|
94
|
+
function resolveRootsForPreset(server, presetName) {
|
|
127
95
|
const fileRoots = listFileRoots(server);
|
|
128
96
|
if (fileRoots.length <= 1) {
|
|
129
|
-
return
|
|
97
|
+
return defaultRoots(fileRoots);
|
|
130
98
|
}
|
|
131
99
|
const matches = [];
|
|
132
100
|
for (const r of fileRoots) {
|
|
@@ -153,53 +121,46 @@ function resolveRootsForPreset(server, args, presetName) {
|
|
|
153
121
|
if (pick !== undefined) {
|
|
154
122
|
return { ok: true, roots: [pick] };
|
|
155
123
|
}
|
|
156
|
-
return
|
|
124
|
+
return defaultRoots(fileRoots);
|
|
157
125
|
}
|
|
158
|
-
/** `gateGit` plus
|
|
126
|
+
/** `gateGit` plus `root` resolution; shared fan-out tool and resource prelude. */
|
|
159
127
|
export function requireGitAndRoots(server, args, presetName) {
|
|
160
128
|
const gg = gateGit();
|
|
161
129
|
if (!gg.ok) {
|
|
162
130
|
return { ok: false, error: gg.body };
|
|
163
131
|
}
|
|
164
|
-
const
|
|
165
|
-
if (
|
|
132
|
+
const root = args.root;
|
|
133
|
+
if (Array.isArray(root)) {
|
|
166
134
|
if (presetName) {
|
|
167
|
-
return { ok: false, error: { error: ERROR_CODES.
|
|
168
|
-
}
|
|
169
|
-
if (hasExclusiveWorkspacePick(args)) {
|
|
170
|
-
return { ok: false, error: { error: ERROR_CODES.ABSOLUTE_GIT_ROOTS_EXCLUSIVE } };
|
|
135
|
+
return { ok: false, error: { error: ERROR_CODES.ROOT_LIST_PRESET_CONFLICT } };
|
|
171
136
|
}
|
|
172
|
-
return
|
|
137
|
+
return resolveRootPathList(root);
|
|
173
138
|
}
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
return { ok: false, error: rootsRes.error };
|
|
139
|
+
const trimmed = root?.trim();
|
|
140
|
+
if (trimmed === "*") {
|
|
141
|
+
const fileRoots = listFileRoots(server);
|
|
142
|
+
return fileRoots.length === 0 ? defaultRoots(fileRoots) : { ok: true, roots: fileRoots };
|
|
179
143
|
}
|
|
180
|
-
|
|
144
|
+
if (trimmed) {
|
|
145
|
+
return { ok: true, roots: [resolve(trimmed)] };
|
|
146
|
+
}
|
|
147
|
+
if (presetName) {
|
|
148
|
+
return resolveRootsForPreset(server, presetName);
|
|
149
|
+
}
|
|
150
|
+
return defaultRoots(listFileRoots(server));
|
|
181
151
|
}
|
|
182
152
|
/**
|
|
183
|
-
*
|
|
184
|
-
* root, and resolve its git toplevel. Returns `{ ok: true, gitTop }`
|
|
185
|
-
* error payload ready for `jsonRespond`.
|
|
153
|
+
* Prelude for single-repo tools: gate git, resolve `workspaceRoot` (or the first
|
|
154
|
+
* MCP root / cwd), and resolve its git toplevel. Returns `{ ok: true, gitTop }`
|
|
155
|
+
* or a structured error payload ready for `jsonRespond`.
|
|
186
156
|
*/
|
|
187
|
-
export function requireSingleRepo(server, args
|
|
188
|
-
const
|
|
189
|
-
if (!
|
|
190
|
-
return
|
|
191
|
-
if (args.absoluteGitRoots != null && args.absoluteGitRoots.length > 0 && pre.roots.length !== 1) {
|
|
192
|
-
return {
|
|
193
|
-
ok: false,
|
|
194
|
-
error: {
|
|
195
|
-
error: ERROR_CODES.ABSOLUTE_GIT_ROOTS_SINGLE_REPO_ONLY,
|
|
196
|
-
rootCount: pre.roots.length,
|
|
197
|
-
},
|
|
198
|
-
};
|
|
157
|
+
export function requireSingleRepo(server, args) {
|
|
158
|
+
const gg = gateGit();
|
|
159
|
+
if (!gg.ok) {
|
|
160
|
+
return { ok: false, error: gg.body };
|
|
199
161
|
}
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
return { ok: false, error: { error: ERROR_CODES.NO_WORKSPACE_ROOT } };
|
|
162
|
+
const ws = args.workspaceRoot?.trim();
|
|
163
|
+
const rootInput = ws ? resolve(ws) : (listFileRoots(server)[0] ?? process.cwd());
|
|
203
164
|
const top = gitTopLevel(rootInput);
|
|
204
165
|
if (!top)
|
|
205
166
|
return { ok: false, error: { error: ERROR_CODES.NOT_A_GIT_REPOSITORY, path: rootInput } };
|
package/dist/server/schemas.js
CHANGED
|
@@ -1,27 +1,19 @@
|
|
|
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
|
|
5
|
-
export const
|
|
4
|
+
/** Max entries when `root` is an array (matches `git_inventory` `maxRoots` hard cap). */
|
|
5
|
+
export const MAX_ROOT_PATHS = 256;
|
|
6
|
+
/** Single-repo tools: one optional repo-path override plus output format. */
|
|
6
7
|
export const WorkspacePickSchema = z.object({
|
|
7
|
-
workspaceRoot: z.string().optional().describe("
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
.
|
|
14
|
-
allWorkspaceRoots: z
|
|
15
|
-
.boolean()
|
|
16
|
-
.optional()
|
|
17
|
-
.default(false)
|
|
18
|
-
.describe("Fan out across all MCP 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)
|
|
8
|
+
workspaceRoot: z.string().optional().describe("Repo path. Default: first MCP root / cwd."),
|
|
9
|
+
format: FormatSchema,
|
|
10
|
+
});
|
|
11
|
+
/** Fan-out tools: one polymorphic routing param plus output format. */
|
|
12
|
+
export const RootPickSchema = z.object({
|
|
13
|
+
root: z
|
|
14
|
+
.union([z.string(), z.array(z.string()).max(MAX_ROOT_PATHS), z.literal("*")])
|
|
23
15
|
.optional()
|
|
24
|
-
.describe(
|
|
16
|
+
.describe('Repo path, array of paths, or "*" for all MCP roots.'),
|
|
25
17
|
format: FormatSchema,
|
|
26
18
|
});
|
|
27
19
|
export { MAX_INVENTORY_ROOTS_DEFAULT };
|
|
@@ -19,15 +19,15 @@ import { registerGitStatusTool } from "./git-status-tool.js";
|
|
|
19
19
|
import { registerGitTagTool } from "./git-tag-tool.js";
|
|
20
20
|
import { registerGitWorktreeAddTool, registerGitWorktreeListTool, registerGitWorktreeRemoveTool, } from "./git-worktree-tool.js";
|
|
21
21
|
import { registerListPresetsTool } from "./list-presets-tool.js";
|
|
22
|
-
export const
|
|
22
|
+
export const FAN_OUT_ROOT_TOOLS = [
|
|
23
23
|
"git_status",
|
|
24
24
|
"git_inventory",
|
|
25
25
|
"git_parity",
|
|
26
26
|
"list_presets",
|
|
27
27
|
"git_log",
|
|
28
|
-
"git_diff_summary",
|
|
29
28
|
];
|
|
30
29
|
export const READ_ONLY_SINGLE_REPO_TOOLS = [
|
|
30
|
+
"git_diff_summary",
|
|
31
31
|
"git_diff",
|
|
32
32
|
"git_show",
|
|
33
33
|
"git_worktree_list",
|
|
@@ -49,7 +49,7 @@ export const MUTATING_TOOLS = [
|
|
|
49
49
|
"git_stash_apply",
|
|
50
50
|
];
|
|
51
51
|
export const ALL_PARAMETER_SCHEMA_TOOLS = [
|
|
52
|
-
...
|
|
52
|
+
...FAN_OUT_ROOT_TOOLS,
|
|
53
53
|
...READ_ONLY_SINGLE_REPO_TOOLS,
|
|
54
54
|
...MUTATING_TOOLS,
|
|
55
55
|
];
|
package/dist/server.js
CHANGED
|
@@ -7,7 +7,7 @@ import { registerRethunkGitTools } from "./server/tools.js";
|
|
|
7
7
|
* (renamed/nested/omitted fields). Surfaced via the FastMCP `instructions`
|
|
8
8
|
* field below, so it is discoverable in the MCP `initialize` response.
|
|
9
9
|
*/
|
|
10
|
-
export const MCP_JSON_FORMAT_VERSION = "
|
|
10
|
+
export const MCP_JSON_FORMAT_VERSION = "5";
|
|
11
11
|
const server = new FastMCP({
|
|
12
12
|
name: "rethunk-git",
|
|
13
13
|
version: readMcpServerVersion(),
|