@rethunk/mcp-multi-root-git 2.9.1 → 4.0.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 +23 -19
- package/CHANGELOG.md +145 -39
- package/HUMANS.md +23 -42
- package/README.md +30 -13
- package/dist/repo-paths.js +57 -13
- package/dist/server/batch-commit-tool.js +203 -53
- package/dist/server/error-codes.js +31 -16
- package/dist/server/git-blame-tool.js +66 -19
- package/dist/server/git-branch-tool.js +151 -0
- package/dist/server/git-cherry-pick-tool.js +221 -12
- package/dist/server/git-conflicts-tool.js +230 -0
- package/dist/server/git-diff-summary-tool.js +39 -28
- package/dist/server/git-diff-tool.js +56 -31
- package/dist/server/git-grep-tool.js +188 -0
- package/dist/server/git-inventory-tool.js +30 -10
- package/dist/server/git-log-tool.js +37 -6
- package/dist/server/git-merge-tool.js +71 -13
- package/dist/server/git-parity-tool.js +15 -6
- package/dist/server/git-push-tool.js +4 -2
- package/dist/server/git-refs.js +48 -17
- package/dist/server/git-reset-soft-tool.js +1 -1
- package/dist/server/git-revert-tool.js +160 -0
- package/dist/server/git-show-tool.js +5 -10
- package/dist/server/git-stash-tool.js +112 -78
- package/dist/server/git-status-tool.js +2 -2
- package/dist/server/git-tag-tool.js +8 -13
- package/dist/server/git-worktree-tool.js +67 -59
- package/dist/server/git.js +116 -24
- package/dist/server/inventory.js +90 -32
- package/dist/server/list-presets-tool.js +2 -8
- package/dist/server/presets-resource.js +37 -20
- package/dist/server/presets.js +87 -15
- package/dist/server/roots.js +52 -79
- package/dist/server/schemas.js +18 -19
- package/dist/server/test-harness.js +11 -4
- package/dist/server/tool-parameter-schemas.js +47 -58
- package/dist/server/tools.js +36 -17
- package/dist/server.js +1 -1
- package/docs/install.md +52 -5
- package/docs/mcp-tools.md +472 -284
- package/package.json +6 -6
- package/schemas/batch_commit.json +5 -17
- package/schemas/git_blame.json +13 -11
- package/schemas/git_branch.json +54 -0
- package/schemas/git_cherry_pick.json +13 -15
- package/schemas/git_cherry_pick_continue.json +34 -0
- package/schemas/git_conflicts.json +38 -0
- package/schemas/git_diff.json +12 -10
- package/schemas/git_diff_summary.json +1 -15
- package/schemas/git_grep.json +85 -0
- package/schemas/git_inventory.json +32 -23
- package/schemas/git_log.json +20 -24
- package/schemas/git_merge.json +3 -15
- package/schemas/git_parity.json +13 -23
- package/schemas/git_push.json +1 -13
- package/schemas/git_reset_soft.json +1 -13
- package/schemas/git_revert.json +47 -0
- package/schemas/git_show.json +2 -8
- package/schemas/git_stash_apply.json +2 -8
- package/schemas/git_stash_push.json +47 -0
- package/schemas/git_status.json +13 -23
- package/schemas/git_tag.json +1 -7
- package/schemas/git_worktree_add.json +2 -14
- package/schemas/git_worktree_remove.json +2 -14
- package/schemas/index.json +28 -23
- package/schemas/list_presets.json +13 -23
- package/tool-parameters.schema.json +407 -423
- package/dist/server/git-branch-list-tool.js +0 -138
- package/dist/server/git-fetch-tool.js +0 -266
- package/dist/server/git-reflog-tool.js +0 -126
- package/schemas/git_branch_list.json +0 -36
- package/schemas/git_fetch.json +0 -52
- package/schemas/git_reflog.json +0 -44
- package/schemas/git_stash_list.json +0 -30
- package/schemas/git_worktree_list.json +0 -30
|
@@ -10,39 +10,56 @@ export function registerPresetsResource(server) {
|
|
|
10
10
|
name: "git-mcp-presets",
|
|
11
11
|
mimeType: "application/json",
|
|
12
12
|
async load() {
|
|
13
|
-
const pre = requireGitAndRoots(server, {}, undefined);
|
|
13
|
+
const pre = requireGitAndRoots(server, { root: "*" }, undefined);
|
|
14
14
|
if (!pre.ok) {
|
|
15
15
|
return { text: jsonRespond(pre.error) };
|
|
16
16
|
}
|
|
17
|
-
|
|
18
|
-
if (!ws) {
|
|
17
|
+
if (pre.roots.length === 0) {
|
|
19
18
|
return { text: jsonRespond({ error: ERROR_CODES.NO_WORKSPACE_ROOT }) };
|
|
20
19
|
}
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const loaded = loadPresetsFromGitTop(top);
|
|
26
|
-
if (!loaded.ok) {
|
|
27
|
-
if (loaded.reason === "missing") {
|
|
20
|
+
const roots = pre.roots.map((ws) => {
|
|
21
|
+
const top = gitTopLevel(ws);
|
|
22
|
+
const presetFile = top ? join(top, PRESET_FILE_PATH) : join(ws, PRESET_FILE_PATH);
|
|
23
|
+
if (!top) {
|
|
28
24
|
return {
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
workspaceRoot: ws,
|
|
26
|
+
gitTop: null,
|
|
27
|
+
presetFile,
|
|
28
|
+
fileExists: false,
|
|
29
|
+
presets: {},
|
|
30
|
+
error: { error: ERROR_CODES.NOT_A_GIT_REPOSITORY, path: ws },
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
const loaded = loadPresetsFromGitTop(top);
|
|
34
|
+
if (!loaded.ok) {
|
|
35
|
+
if (loaded.reason === "missing") {
|
|
36
|
+
return {
|
|
37
|
+
workspaceRoot: ws,
|
|
38
|
+
gitTop: top,
|
|
39
|
+
presetFile,
|
|
31
40
|
fileExists: false,
|
|
32
41
|
presets: {},
|
|
33
|
-
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
workspaceRoot: ws,
|
|
46
|
+
gitTop: top,
|
|
47
|
+
presetFile,
|
|
48
|
+
fileExists: true,
|
|
49
|
+
presets: {},
|
|
50
|
+
error: presetLoadErrorPayload(top, loaded),
|
|
34
51
|
};
|
|
35
52
|
}
|
|
36
|
-
return {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
presetFile: join(top, PRESET_FILE_PATH),
|
|
53
|
+
return {
|
|
54
|
+
workspaceRoot: ws,
|
|
55
|
+
gitTop: top,
|
|
56
|
+
presetFile,
|
|
41
57
|
fileExists: true,
|
|
42
58
|
...spreadDefined("presetSchemaVersion", loaded.schemaVersion),
|
|
43
59
|
presets: loaded.data,
|
|
44
|
-
}
|
|
45
|
-
};
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
return { text: jsonRespond({ roots }) };
|
|
46
63
|
},
|
|
47
64
|
});
|
|
48
65
|
}
|
package/dist/server/presets.js
CHANGED
|
@@ -5,21 +5,56 @@ import { ERROR_CODES } from "./error-codes.js";
|
|
|
5
5
|
/**
|
|
6
6
|
* Schema for `.rethunk/git-mcp-presets.json` at the workspace root.
|
|
7
7
|
* Each named entry defines roots for `git_inventory` and/or pairs for `git_parity`.
|
|
8
|
+
* Must stay aligned with `git-mcp-presets.schema.json`.
|
|
8
9
|
*/
|
|
9
|
-
const
|
|
10
|
+
const ParityPairSchema = z
|
|
11
|
+
.object({
|
|
12
|
+
left: z.string(),
|
|
13
|
+
right: z.string(),
|
|
14
|
+
label: z.string().optional(),
|
|
15
|
+
})
|
|
16
|
+
.strict();
|
|
17
|
+
const PresetEntrySchema = z
|
|
18
|
+
.object({
|
|
10
19
|
nestedRoots: z.array(z.string()).optional(),
|
|
11
|
-
parityPairs: z
|
|
12
|
-
.array(z.object({
|
|
13
|
-
left: z.string(),
|
|
14
|
-
right: z.string(),
|
|
15
|
-
label: z.string().optional(),
|
|
16
|
-
}))
|
|
17
|
-
.optional(),
|
|
20
|
+
parityPairs: z.array(ParityPairSchema).optional(),
|
|
18
21
|
/** When multiple MCP file roots exist, prefer one whose path basename or suffix matches this hint. */
|
|
19
22
|
workspaceRootHint: z.string().optional(),
|
|
20
|
-
})
|
|
23
|
+
})
|
|
24
|
+
.strict();
|
|
21
25
|
const PresetFileSchema = z.record(z.string(), PresetEntrySchema);
|
|
22
26
|
export const PRESET_FILE_PATH = ".rethunk/git-mcp-presets.json";
|
|
27
|
+
const PRESET_ENTRY_FIELD_NAMES = new Set(["nestedRoots", "parityPairs", "workspaceRootHint"]);
|
|
28
|
+
const WRAPPED_META_KEYS = new Set(["$schema", "schemaVersion", "presets"]);
|
|
29
|
+
const PRESET_SCHEMA_VERSION = "1";
|
|
30
|
+
function looksLikePresetEntryObject(obj) {
|
|
31
|
+
const keys = Object.keys(obj);
|
|
32
|
+
if (keys.length === 0)
|
|
33
|
+
return true;
|
|
34
|
+
return keys.every((k) => PRESET_ENTRY_FIELD_NAMES.has(k));
|
|
35
|
+
}
|
|
36
|
+
function isWrappedLayout(o) {
|
|
37
|
+
if (!("presets" in o))
|
|
38
|
+
return false;
|
|
39
|
+
const inner = o.presets;
|
|
40
|
+
if (inner === null || typeof inner !== "object" || Array.isArray(inner)) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
const innerObj = inner;
|
|
44
|
+
// Non-empty inner with only PresetEntry field names → legacy preset named "presets".
|
|
45
|
+
if (looksLikePresetEntryObject(innerObj) && Object.keys(innerObj).length > 0) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
// Empty inner: wrapped when top level has only metadata + presets.
|
|
49
|
+
if (Object.keys(innerObj).length === 0) {
|
|
50
|
+
const topKeys = Object.keys(o);
|
|
51
|
+
return topKeys.every((k) => WRAPPED_META_KEYS.has(k));
|
|
52
|
+
}
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
function schemaIssue(message, path = []) {
|
|
56
|
+
return { code: "custom", path, message };
|
|
57
|
+
}
|
|
23
58
|
/**
|
|
24
59
|
* Supports:
|
|
25
60
|
* - Wrapped: `{ "schemaVersion": "1", "presets": { "name": { ... } } }`
|
|
@@ -30,10 +65,12 @@ function splitPresetFileRaw(raw) {
|
|
|
30
65
|
throw new Error("invalid_root");
|
|
31
66
|
}
|
|
32
67
|
const o = raw;
|
|
33
|
-
if (
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
68
|
+
if (isWrappedLayout(o)) {
|
|
69
|
+
for (const key of Object.keys(o)) {
|
|
70
|
+
if (!WRAPPED_META_KEYS.has(key)) {
|
|
71
|
+
throw new Error("wrapped_extra_keys");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
37
74
|
const sv = o.schemaVersion;
|
|
38
75
|
return {
|
|
39
76
|
mapRaw: o.presets,
|
|
@@ -69,13 +106,39 @@ export function loadPresetsFromGitTop(gitTop) {
|
|
|
69
106
|
mapRaw = s.mapRaw;
|
|
70
107
|
schemaVersion = s.schemaVersion;
|
|
71
108
|
}
|
|
72
|
-
catch {
|
|
109
|
+
catch (e) {
|
|
110
|
+
if (e instanceof Error && e.message === "wrapped_extra_keys") {
|
|
111
|
+
return {
|
|
112
|
+
ok: false,
|
|
113
|
+
reason: "schema",
|
|
114
|
+
issues: [
|
|
115
|
+
schemaIssue("Wrapped preset files allow only $schema, schemaVersion, and presets"),
|
|
116
|
+
],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
73
119
|
return {
|
|
74
120
|
ok: false,
|
|
75
121
|
reason: "invalid_json",
|
|
76
122
|
message: "Preset file root must be a JSON object",
|
|
77
123
|
};
|
|
78
124
|
}
|
|
125
|
+
if (schemaVersion !== undefined && schemaVersion !== PRESET_SCHEMA_VERSION) {
|
|
126
|
+
return {
|
|
127
|
+
ok: false,
|
|
128
|
+
reason: "schema",
|
|
129
|
+
issues: [schemaIssue(`schemaVersion must be "${PRESET_SCHEMA_VERSION}"`, ["schemaVersion"])],
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
if (mapRaw === null ||
|
|
133
|
+
typeof mapRaw !== "object" ||
|
|
134
|
+
Array.isArray(mapRaw) ||
|
|
135
|
+
Object.keys(mapRaw).length === 0) {
|
|
136
|
+
return {
|
|
137
|
+
ok: false,
|
|
138
|
+
reason: "schema",
|
|
139
|
+
issues: [schemaIssue("Preset file must contain at least one preset")],
|
|
140
|
+
};
|
|
141
|
+
}
|
|
79
142
|
const parsed = PresetFileSchema.safeParse(mapRaw);
|
|
80
143
|
if (!parsed.success) {
|
|
81
144
|
return { ok: false, reason: "schema", issues: parsed.error.issues };
|
|
@@ -150,7 +213,16 @@ function mergePairs(preset, inline) {
|
|
|
150
213
|
const b = inline ?? [];
|
|
151
214
|
if (a.length === 0 && b.length === 0)
|
|
152
215
|
return undefined;
|
|
153
|
-
|
|
216
|
+
const seen = new Set();
|
|
217
|
+
const out = [];
|
|
218
|
+
for (const pair of [...a, ...b]) {
|
|
219
|
+
const key = `${pair.left}\0${pair.right}`;
|
|
220
|
+
if (!seen.has(key)) {
|
|
221
|
+
seen.add(key);
|
|
222
|
+
out.push(pair);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return out;
|
|
154
226
|
}
|
|
155
227
|
export function applyPresetNestedRoots(gitTop, presetName, presetMerge, inlineNestedRoots) {
|
|
156
228
|
const got = getPresetEntry(gitTop, presetName);
|
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,58 @@ 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.
|
|
135
|
+
return { ok: false, error: { error: ERROR_CODES.ROOT_LIST_PRESET_CONFLICT } };
|
|
168
136
|
}
|
|
169
|
-
|
|
170
|
-
|
|
137
|
+
return resolveRootPathList(root);
|
|
138
|
+
}
|
|
139
|
+
const trimmed = root?.trim();
|
|
140
|
+
if (trimmed === "*") {
|
|
141
|
+
const fileRoots = listFileRoots(server);
|
|
142
|
+
if (fileRoots.length === 0)
|
|
143
|
+
return defaultRoots(fileRoots);
|
|
144
|
+
if (fileRoots.length > MAX_ROOT_PATHS) {
|
|
145
|
+
return {
|
|
146
|
+
ok: false,
|
|
147
|
+
error: {
|
|
148
|
+
error: ERROR_CODES.ROOT_LIST_TOO_MANY,
|
|
149
|
+
max: MAX_ROOT_PATHS,
|
|
150
|
+
count: fileRoots.length,
|
|
151
|
+
},
|
|
152
|
+
};
|
|
171
153
|
}
|
|
172
|
-
return
|
|
154
|
+
return { ok: true, roots: fileRoots };
|
|
155
|
+
}
|
|
156
|
+
if (trimmed) {
|
|
157
|
+
return { ok: true, roots: [resolve(trimmed)] };
|
|
173
158
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
: resolveWorkspaceRoots(server, args);
|
|
177
|
-
if (!rootsRes.ok) {
|
|
178
|
-
return { ok: false, error: rootsRes.error };
|
|
159
|
+
if (presetName) {
|
|
160
|
+
return resolveRootsForPreset(server, presetName);
|
|
179
161
|
}
|
|
180
|
-
return
|
|
162
|
+
return defaultRoots(listFileRoots(server));
|
|
181
163
|
}
|
|
182
164
|
/**
|
|
183
|
-
*
|
|
184
|
-
* root, and resolve its git toplevel. Returns `{ ok: true, gitTop }`
|
|
185
|
-
* error payload ready for `jsonRespond`.
|
|
165
|
+
* Prelude for single-repo tools: gate git, resolve `workspaceRoot` (or the first
|
|
166
|
+
* MCP root / cwd), and resolve its git toplevel. Returns `{ ok: true, gitTop }`
|
|
167
|
+
* or a structured error payload ready for `jsonRespond`.
|
|
186
168
|
*/
|
|
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
|
-
};
|
|
169
|
+
export function requireSingleRepo(server, args) {
|
|
170
|
+
const gg = gateGit();
|
|
171
|
+
if (!gg.ok) {
|
|
172
|
+
return { ok: false, error: gg.body };
|
|
199
173
|
}
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
return { ok: false, error: { error: ERROR_CODES.NO_WORKSPACE_ROOT } };
|
|
174
|
+
const ws = args.workspaceRoot?.trim();
|
|
175
|
+
const rootInput = ws ? resolve(ws) : (listFileRoots(server)[0] ?? process.cwd());
|
|
203
176
|
const top = gitTopLevel(rootInput);
|
|
204
177
|
if (!top)
|
|
205
178
|
return { ok: false, error: { error: ERROR_CODES.NOT_A_GIT_REPOSITORY, path: rootInput } };
|
package/dist/server/schemas.js
CHANGED
|
@@ -1,27 +1,26 @@
|
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
+
/**
|
|
12
|
+
* Fan-out tools: one polymorphic routing param plus output format.
|
|
13
|
+
*
|
|
14
|
+
* Array length is intentionally uncapped here so `resolveRootPathList` can
|
|
15
|
+
* return the structured `{ error: root_list_too_many, max, count }` JSON
|
|
16
|
+
* payload. Zod `.max(MAX_ROOT_PATHS)` would reject with `too_big` before execute.
|
|
17
|
+
* The `"*"` sentinel is a plain string (no redundant `z.literal("*")`).
|
|
18
|
+
*/
|
|
19
|
+
export const RootPickSchema = z.object({
|
|
20
|
+
root: z
|
|
21
|
+
.union([z.string(), z.array(z.string())])
|
|
23
22
|
.optional()
|
|
24
|
-
.describe(
|
|
23
|
+
.describe('Repo path, array of paths, or "*" for all MCP roots.'),
|
|
25
24
|
format: FormatSchema,
|
|
26
25
|
});
|
|
27
26
|
export { MAX_INVENTORY_ROOTS_DEFAULT };
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* const result = await tool({ workspaceRoot: dir, commits: [...] });
|
|
17
17
|
* // result is string (markdown) or JSON-parseable string
|
|
18
18
|
*/
|
|
19
|
+
import { afterEach } from "bun:test";
|
|
19
20
|
import { execFileSync } from "node:child_process";
|
|
20
21
|
import { appendFileSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
21
22
|
import { tmpdir } from "node:os";
|
|
@@ -33,12 +34,18 @@ const STUB_CONTEXT = {
|
|
|
33
34
|
};
|
|
34
35
|
// ---------------------------------------------------------------------------
|
|
35
36
|
// Tmp-dir lifecycle — prevents accumulating thousands of leaked test dirs.
|
|
36
|
-
// Each test file
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
37
|
+
// Each test file that calls mkTmpDir / makeRepo / trackTmpPath must register
|
|
38
|
+
// cleanup once at module scope:
|
|
39
|
+
// registerTmpCleanup();
|
|
40
|
+
// (equivalent to afterEach(cleanupTmpPaths) but documents the contract.)
|
|
41
|
+
// Module-scope afterEach(...) in this file would only register once
|
|
42
|
+
// (first-importer wins) because the module is cached across test files.
|
|
40
43
|
// ---------------------------------------------------------------------------
|
|
41
44
|
const tmpPaths = [];
|
|
45
|
+
/** Register afterEach cleanup for dirs created via mkTmpDir / makeRepo. Call once per test file. */
|
|
46
|
+
export function registerTmpCleanup() {
|
|
47
|
+
afterEach(cleanupTmpPaths);
|
|
48
|
+
}
|
|
42
49
|
export function mkTmpDir(prefix) {
|
|
43
50
|
const dir = mkdtempSync(join(tmpdir(), prefix));
|
|
44
51
|
tmpPaths.push(dir);
|
|
@@ -1,95 +1,84 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
import { registerGitInventoryTool } from "./git-inventory-tool.js";
|
|
10
|
-
import { registerGitLogTool } from "./git-log-tool.js";
|
|
11
|
-
import { registerGitMergeTool } from "./git-merge-tool.js";
|
|
12
|
-
import { registerGitParityTool } from "./git-parity-tool.js";
|
|
13
|
-
import { registerGitPushTool } from "./git-push-tool.js";
|
|
14
|
-
import { registerGitReflogTool } from "./git-reflog-tool.js";
|
|
15
|
-
import { registerGitResetSoftTool } from "./git-reset-soft-tool.js";
|
|
16
|
-
import { registerGitShowTool } from "./git-show-tool.js";
|
|
17
|
-
import { registerGitStashApplyTool, registerGitStashListTool } from "./git-stash-tool.js";
|
|
18
|
-
import { registerGitStatusTool } from "./git-status-tool.js";
|
|
19
|
-
import { registerGitTagTool } from "./git-tag-tool.js";
|
|
20
|
-
import { registerGitWorktreeAddTool, registerGitWorktreeListTool, registerGitWorktreeRemoveTool, } from "./git-worktree-tool.js";
|
|
21
|
-
import { registerListPresetsTool } from "./list-presets-tool.js";
|
|
22
|
-
export const READ_ONLY_ABSOLUTE_ROOT_TOOLS = [
|
|
2
|
+
import { registerRethunkGitTools } from "./tools.js";
|
|
3
|
+
/**
|
|
4
|
+
* Fan-out read tools: polymorphic `root` routing (string | string[] | "*").
|
|
5
|
+
* Must stay in sync with the tools that use RootPickSchema — asserted against
|
|
6
|
+
* live `registerRethunkGitTools` capture in tests.
|
|
7
|
+
*/
|
|
8
|
+
export const FAN_OUT_ROOT_TOOLS = [
|
|
23
9
|
"git_status",
|
|
24
10
|
"git_inventory",
|
|
25
11
|
"git_parity",
|
|
26
12
|
"list_presets",
|
|
27
13
|
"git_log",
|
|
28
|
-
"
|
|
14
|
+
"git_grep",
|
|
29
15
|
];
|
|
16
|
+
/**
|
|
17
|
+
* Read-only single-repo tools: `workspaceRoot` routing only.
|
|
18
|
+
*/
|
|
30
19
|
export const READ_ONLY_SINGLE_REPO_TOOLS = [
|
|
20
|
+
"git_diff_summary",
|
|
31
21
|
"git_diff",
|
|
32
22
|
"git_show",
|
|
33
|
-
"
|
|
34
|
-
"git_stash_list",
|
|
23
|
+
"git_conflicts",
|
|
35
24
|
"git_blame",
|
|
36
|
-
"git_branch_list",
|
|
37
|
-
"git_reflog",
|
|
38
25
|
];
|
|
26
|
+
/**
|
|
27
|
+
* Mutating tools: `workspaceRoot` routing only.
|
|
28
|
+
*/
|
|
39
29
|
export const MUTATING_TOOLS = [
|
|
40
|
-
"git_fetch",
|
|
41
30
|
"batch_commit",
|
|
42
31
|
"git_push",
|
|
43
32
|
"git_merge",
|
|
44
33
|
"git_cherry_pick",
|
|
34
|
+
"git_cherry_pick_continue",
|
|
45
35
|
"git_reset_soft",
|
|
36
|
+
"git_revert",
|
|
46
37
|
"git_tag",
|
|
38
|
+
"git_branch",
|
|
47
39
|
"git_worktree_add",
|
|
48
40
|
"git_worktree_remove",
|
|
49
41
|
"git_stash_apply",
|
|
42
|
+
"git_stash_push",
|
|
50
43
|
];
|
|
44
|
+
/** Category union used for routing assertions; must equal live registrar names. */
|
|
51
45
|
export const ALL_PARAMETER_SCHEMA_TOOLS = [
|
|
52
|
-
...
|
|
46
|
+
...FAN_OUT_ROOT_TOOLS,
|
|
53
47
|
...READ_ONLY_SINGLE_REPO_TOOLS,
|
|
54
48
|
...MUTATING_TOOLS,
|
|
55
49
|
];
|
|
56
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Capture parameter Zod schemas by driving the same registrar path as the live
|
|
52
|
+
* server (`registerRethunkGitTools`), so adding a tool only in `tools.ts` is
|
|
53
|
+
* enough for schema capture — no parallel register* list here.
|
|
54
|
+
*
|
|
55
|
+
* Clears `RETHUNK_GIT_TOOLS` for the duration so allowlist filtering cannot
|
|
56
|
+
* silently omit tools from published artifacts.
|
|
57
|
+
*/
|
|
58
|
+
export function captureToolParameterSchemas() {
|
|
57
59
|
const tools = [];
|
|
58
60
|
const server = {
|
|
59
61
|
sessions: [],
|
|
60
62
|
addTool(tool) {
|
|
61
63
|
tools.push({ name: tool.name, parameters: tool.parameters });
|
|
62
64
|
},
|
|
65
|
+
addResource() {
|
|
66
|
+
// Presets resource is always registered; capture only needs tools.
|
|
67
|
+
},
|
|
63
68
|
};
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
registerGitWorktreeListTool(server);
|
|
78
|
-
registerGitStashListTool(server);
|
|
79
|
-
registerGitFetchTool(server);
|
|
80
|
-
registerGitBlameTool(server);
|
|
81
|
-
registerGitBranchListTool(server);
|
|
82
|
-
registerGitReflogTool(server);
|
|
83
|
-
registerBatchCommitTool(server);
|
|
84
|
-
registerGitPushTool(server);
|
|
85
|
-
registerGitMergeTool(server);
|
|
86
|
-
registerGitCherryPickTool(server);
|
|
87
|
-
registerGitResetSoftTool(server);
|
|
88
|
-
registerGitTagTool(server);
|
|
89
|
-
registerGitWorktreeAddTool(server);
|
|
90
|
-
registerGitWorktreeRemoveTool(server);
|
|
91
|
-
registerGitStashApplyTool(server);
|
|
92
|
-
});
|
|
69
|
+
const prev = process.env.RETHUNK_GIT_TOOLS;
|
|
70
|
+
delete process.env.RETHUNK_GIT_TOOLS;
|
|
71
|
+
try {
|
|
72
|
+
registerRethunkGitTools(server);
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
if (prev === undefined) {
|
|
76
|
+
delete process.env.RETHUNK_GIT_TOOLS;
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
process.env.RETHUNK_GIT_TOOLS = prev;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
93
82
|
return Object.fromEntries(tools.map((tool) => [tool.name, z.toJSONSchema(tool.parameters)]));
|
|
94
83
|
}
|
|
95
84
|
export function buildToolParameterSchemaDocument() {
|