@rethunk/mcp-multi-root-git 2.5.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.
- package/AGENTS.md +4 -4
- package/CHANGELOG.md +45 -1
- package/README.md +1 -1
- package/dist/server/batch-commit-tool.js +32 -10
- package/dist/server/error-codes.js +95 -0
- package/dist/server/git-blame-tool.js +227 -0
- package/dist/server/git-branch-list-tool.js +140 -0
- package/dist/server/git-cherry-pick-tool.js +11 -10
- package/dist/server/git-diff-summary-tool.js +10 -3
- package/dist/server/git-diff-tool.js +60 -13
- package/dist/server/git-fetch-tool.js +148 -13
- package/dist/server/git-inventory-tool.js +7 -6
- package/dist/server/git-log-tool.js +14 -4
- package/dist/server/git-merge-tool.js +15 -14
- package/dist/server/git-parity-tool.js +5 -4
- package/dist/server/git-push-tool.js +9 -5
- package/dist/server/git-reflog-tool.js +126 -0
- package/dist/server/git-refs.js +12 -5
- package/dist/server/git-reset-soft-tool.js +4 -3
- package/dist/server/git-show-tool.js +96 -22
- package/dist/server/git-stash-tool.js +17 -15
- package/dist/server/git-tag-tool.js +8 -7
- package/dist/server/git-worktree-tool.js +8 -7
- package/dist/server/git.js +86 -7
- package/dist/server/list-presets-tool.js +2 -1
- package/dist/server/presets-resource.js +3 -2
- package/dist/server/presets.js +11 -5
- package/dist/server/roots.js +11 -10
- package/dist/server/tool-parameter-schemas.js +9 -0
- package/dist/server/tools.js +6 -0
- package/dist/server.js +7 -0
- package/docs/mcp-tools.md +134 -12
- package/package.json +4 -3
- package/schemas/git_blame.json +52 -0
- package/schemas/git_branch_list.json +36 -0
- package/schemas/git_diff.json +14 -15
- package/schemas/git_diff_summary.json +0 -6
- package/schemas/git_reflog.json +44 -0
- package/schemas/git_show.json +13 -1
- package/schemas/index.json +15 -0
- package/tool-parameters.schema.json +153 -22
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { ERROR_CODES } from "./error-codes.js";
|
|
2
3
|
import { spawnGitAsync } from "./git.js";
|
|
3
4
|
import { jsonRespond, spreadDefined } from "./json.js";
|
|
4
5
|
import { requireSingleRepo } from "./roots.js";
|
|
@@ -30,7 +31,7 @@ export function registerGitStashListTool(server) {
|
|
|
30
31
|
// If there are no stashes, git still returns ok=true with empty output
|
|
31
32
|
// Only treat as error if git itself failed
|
|
32
33
|
return jsonRespond({
|
|
33
|
-
error:
|
|
34
|
+
error: ERROR_CODES.STASH_LIST_FAILED,
|
|
34
35
|
detail: (r.stderr || r.stdout).trim(),
|
|
35
36
|
});
|
|
36
37
|
}
|
|
@@ -39,21 +40,22 @@ export function registerGitStashListTool(server) {
|
|
|
39
40
|
.split("\n")
|
|
40
41
|
.map((l) => l.trim())
|
|
41
42
|
.filter((l) => l.length > 0);
|
|
42
|
-
for (
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
message,
|
|
53
|
-
sha,
|
|
54
|
-
});
|
|
55
|
-
}
|
|
43
|
+
for (const line of lines) {
|
|
44
|
+
const parts = line.split("|");
|
|
45
|
+
// parts[0] = stash@{N}, last part = short SHA, middle = message (may contain "|")
|
|
46
|
+
const sha = parts[parts.length - 1];
|
|
47
|
+
const message = parts.slice(1, -1).join("|");
|
|
48
|
+
// Parse the real stash index from the canonical stash@{N} ref in parts[0].
|
|
49
|
+
const indexMatch = parts[0] ? /stash@\{(\d+)\}/.exec(parts[0]) : null;
|
|
50
|
+
if (!indexMatch || parts.length < 3 || !message || !sha) {
|
|
51
|
+
// Malformed line — skip without affecting index tracking.
|
|
52
|
+
continue;
|
|
56
53
|
}
|
|
54
|
+
stashes.push({
|
|
55
|
+
index: Number(indexMatch[1]),
|
|
56
|
+
message,
|
|
57
|
+
sha,
|
|
58
|
+
});
|
|
57
59
|
}
|
|
58
60
|
if (args.format === "json") {
|
|
59
61
|
return jsonRespond({ stashes });
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { ERROR_CODES } from "./error-codes.js";
|
|
2
3
|
import { isSafeGitUpstreamToken, spawnGitAsync } from "./git.js";
|
|
3
4
|
import { jsonRespond } from "./json.js";
|
|
4
5
|
import { requireSingleRepo } from "./roots.js";
|
|
@@ -72,18 +73,18 @@ export function registerGitTagTool(server) {
|
|
|
72
73
|
const gitTop = pre.gitTop;
|
|
73
74
|
const tag = args.tag.trim();
|
|
74
75
|
if (!tag) {
|
|
75
|
-
return jsonRespond({ error:
|
|
76
|
+
return jsonRespond({ error: ERROR_CODES.EMPTY_TAG_NAME });
|
|
76
77
|
}
|
|
77
78
|
// Validate tag name: no shell metacharacters
|
|
78
79
|
if (!isSafeGitUpstreamToken(tag)) {
|
|
79
|
-
return jsonRespond({ error:
|
|
80
|
+
return jsonRespond({ error: ERROR_CODES.UNSAFE_TAG_TOKEN, tag });
|
|
80
81
|
}
|
|
81
82
|
// Handle deletion
|
|
82
83
|
if (args.delete === true) {
|
|
83
84
|
const delResult = await spawnGitAsync(gitTop, ["tag", "-d", tag]);
|
|
84
85
|
if (!delResult.ok) {
|
|
85
86
|
return jsonRespond({
|
|
86
|
-
error:
|
|
87
|
+
error: ERROR_CODES.TAG_DELETE_FAILED,
|
|
87
88
|
detail: (delResult.stderr || delResult.stdout).trim(),
|
|
88
89
|
});
|
|
89
90
|
}
|
|
@@ -99,13 +100,13 @@ export function registerGitTagTool(server) {
|
|
|
99
100
|
// Determine the ref to tag (default HEAD)
|
|
100
101
|
const ref = (args.ref ?? "HEAD").trim();
|
|
101
102
|
if (!isSafeGitUpstreamToken(ref)) {
|
|
102
|
-
return jsonRespond({ error:
|
|
103
|
+
return jsonRespond({ error: ERROR_CODES.UNSAFE_REF_TOKEN, ref });
|
|
103
104
|
}
|
|
104
105
|
// Get the SHA of the ref to tag
|
|
105
106
|
const sha = await getRefSha(gitTop, ref);
|
|
106
107
|
if (!sha) {
|
|
107
108
|
return jsonRespond({
|
|
108
|
-
error:
|
|
109
|
+
error: ERROR_CODES.REF_NOT_FOUND,
|
|
109
110
|
ref,
|
|
110
111
|
});
|
|
111
112
|
}
|
|
@@ -122,7 +123,7 @@ export function registerGitTagTool(server) {
|
|
|
122
123
|
const createResult = await spawnGitAsync(gitTop, tagArgs);
|
|
123
124
|
if (!createResult.ok) {
|
|
124
125
|
return jsonRespond({
|
|
125
|
-
error:
|
|
126
|
+
error: ERROR_CODES.TAG_CREATE_FAILED,
|
|
126
127
|
detail: (createResult.stderr || createResult.stdout).trim(),
|
|
127
128
|
});
|
|
128
129
|
}
|
|
@@ -130,7 +131,7 @@ export function registerGitTagTool(server) {
|
|
|
130
131
|
const tagType = await getTagType(gitTop, tag);
|
|
131
132
|
if (!tagType) {
|
|
132
133
|
return jsonRespond({
|
|
133
|
-
error:
|
|
134
|
+
error: ERROR_CODES.TAG_VERIFICATION_FAILED,
|
|
134
135
|
tag,
|
|
135
136
|
});
|
|
136
137
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { ERROR_CODES } from "./error-codes.js";
|
|
3
4
|
import { spawnGitAsync } from "./git.js";
|
|
4
5
|
import { isProtectedBranch, isSafeGitRefToken, listWorktrees, } from "./git-refs.js";
|
|
5
6
|
import { jsonRespond, spreadDefined, spreadWhen } from "./json.js";
|
|
@@ -76,13 +77,13 @@ export function registerGitWorktreeAddTool(server) {
|
|
|
76
77
|
const { gitTop } = pre;
|
|
77
78
|
// Validate branch
|
|
78
79
|
if (!isSafeGitRefToken(args.branch)) {
|
|
79
|
-
return jsonRespond({ error:
|
|
80
|
+
return jsonRespond({ error: ERROR_CODES.UNSAFE_REF_TOKEN, ref: args.branch });
|
|
80
81
|
}
|
|
81
82
|
if (isProtectedBranch(args.branch)) {
|
|
82
|
-
return jsonRespond({ error:
|
|
83
|
+
return jsonRespond({ error: ERROR_CODES.PROTECTED_BRANCH, branch: args.branch });
|
|
83
84
|
}
|
|
84
85
|
if (args.baseRef !== undefined && !isSafeGitRefToken(args.baseRef)) {
|
|
85
|
-
return jsonRespond({ error:
|
|
86
|
+
return jsonRespond({ error: ERROR_CODES.UNSAFE_REF_TOKEN, ref: args.baseRef });
|
|
86
87
|
}
|
|
87
88
|
// Resolve path
|
|
88
89
|
const wtPath = resolve(gitTop, args.path);
|
|
@@ -107,7 +108,7 @@ export function registerGitWorktreeAddTool(server) {
|
|
|
107
108
|
if (!r.ok) {
|
|
108
109
|
return jsonRespond({
|
|
109
110
|
ok: false,
|
|
110
|
-
error:
|
|
111
|
+
error: ERROR_CODES.WORKTREE_ADD_FAILED,
|
|
111
112
|
detail: (r.stderr || r.stdout).trim(),
|
|
112
113
|
});
|
|
113
114
|
}
|
|
@@ -158,13 +159,13 @@ export function registerGitWorktreeRemoveTool(server) {
|
|
|
158
159
|
// Refuse to remove the main worktree
|
|
159
160
|
const wtPath = resolve(gitTop, args.path);
|
|
160
161
|
if (wtPath === gitTop) {
|
|
161
|
-
return jsonRespond({ error:
|
|
162
|
+
return jsonRespond({ error: ERROR_CODES.CANNOT_REMOVE_MAIN_WORKTREE, path: wtPath });
|
|
162
163
|
}
|
|
163
164
|
// Verify it exists in the worktree list
|
|
164
165
|
const trees = await listWorktrees(gitTop);
|
|
165
166
|
const isRegistered = trees.some((t) => t.path === wtPath || t.path === args.path);
|
|
166
167
|
if (!isRegistered) {
|
|
167
|
-
return jsonRespond({ error:
|
|
168
|
+
return jsonRespond({ error: ERROR_CODES.WORKTREE_NOT_FOUND, path: args.path });
|
|
168
169
|
}
|
|
169
170
|
const removeArgs = ["worktree", "remove", wtPath];
|
|
170
171
|
if (args.force)
|
|
@@ -173,7 +174,7 @@ export function registerGitWorktreeRemoveTool(server) {
|
|
|
173
174
|
if (!r.ok) {
|
|
174
175
|
return jsonRespond({
|
|
175
176
|
ok: false,
|
|
176
|
-
error:
|
|
177
|
+
error: ERROR_CODES.WORKTREE_REMOVE_FAILED,
|
|
177
178
|
detail: (r.stderr || r.stdout).trim(),
|
|
178
179
|
...spreadWhen((r.stderr || r.stdout).includes("contains modified") ||
|
|
179
180
|
(r.stderr || r.stdout).includes("is not empty"), { hint: "Pass force: true to remove a worktree with uncommitted changes." }),
|
package/dist/server/git.js
CHANGED
|
@@ -2,6 +2,7 @@ import { spawn, spawnSync } from "node:child_process";
|
|
|
2
2
|
import { existsSync, lstatSync, readFileSync } from "node:fs";
|
|
3
3
|
import { cpus } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
+
import { ERROR_CODES } from "./error-codes.js";
|
|
5
6
|
/**
|
|
6
7
|
* Parallel git subprocesses for inventory rows and git_status submodule rows.
|
|
7
8
|
* Reads from GIT_SUBPROCESS_PARALLELISM env var (default 4), clamped to [1, 2×CPU_COUNT].
|
|
@@ -19,9 +20,28 @@ function resolveGitSubprocessParallelism() {
|
|
|
19
20
|
return 4;
|
|
20
21
|
}
|
|
21
22
|
export const GIT_SUBPROCESS_PARALLELISM = resolveGitSubprocessParallelism();
|
|
23
|
+
/**
|
|
24
|
+
* Default timeout for git subprocesses spawned by spawnGitAsync.
|
|
25
|
+
* Reads from GIT_SUBPROCESS_TIMEOUT_MS env var (default 120000 ms = 2 min).
|
|
26
|
+
* A value of 0 (or negative/NaN) disables the timeout — use for operations
|
|
27
|
+
* like large clones where unbounded wait is intentional.
|
|
28
|
+
*/
|
|
29
|
+
function resolveGitSubprocessTimeoutMs() {
|
|
30
|
+
const env = process.env.GIT_SUBPROCESS_TIMEOUT_MS;
|
|
31
|
+
if (env) {
|
|
32
|
+
const n = Number.parseInt(env, 10);
|
|
33
|
+
if (!Number.isNaN(n) && n > 0)
|
|
34
|
+
return n;
|
|
35
|
+
// 0 or negative → disabled
|
|
36
|
+
if (!Number.isNaN(n))
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
return 120_000;
|
|
40
|
+
}
|
|
41
|
+
export const GIT_SUBPROCESS_TIMEOUT_MS = resolveGitSubprocessTimeoutMs();
|
|
22
42
|
let gitPathState = "unknown";
|
|
23
43
|
const GIT_NOT_FOUND_BODY = {
|
|
24
|
-
error:
|
|
44
|
+
error: ERROR_CODES.GIT_NOT_FOUND,
|
|
25
45
|
};
|
|
26
46
|
export function gateGit() {
|
|
27
47
|
if (gitPathState === "ok") {
|
|
@@ -94,10 +114,23 @@ export function parseGitSubmodulePaths(gitRoot) {
|
|
|
94
114
|
return [];
|
|
95
115
|
}
|
|
96
116
|
const paths = [];
|
|
97
|
-
|
|
98
|
-
|
|
117
|
+
let inSubmoduleSection = false;
|
|
118
|
+
for (const rawLine of text.split("\n")) {
|
|
119
|
+
// Strip inline and whole-line comments (; and #)
|
|
120
|
+
const commentIdx = rawLine.search(/\s*[;#]/);
|
|
121
|
+
const line = commentIdx >= 0 ? rawLine.slice(0, commentIdx) : rawLine;
|
|
122
|
+
// Track INI section header
|
|
123
|
+
const sectionMatch = /^\s*\[(.+)\]\s*$/.exec(line);
|
|
124
|
+
if (sectionMatch) {
|
|
125
|
+
inSubmoduleSection = /^submodule\s+"/.test(sectionMatch[1] ?? "");
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
// Only collect path = lines inside a [submodule "..."] section
|
|
129
|
+
if (!inSubmoduleSection)
|
|
130
|
+
continue;
|
|
131
|
+
const m = /^\s*path\s*=\s*(.+?)\s*$/.exec(line);
|
|
99
132
|
if (m?.[1])
|
|
100
|
-
paths.push(m[1]
|
|
133
|
+
paths.push(m[1]);
|
|
101
134
|
}
|
|
102
135
|
return paths;
|
|
103
136
|
}
|
|
@@ -134,11 +167,12 @@ export async function asyncPool(items, concurrency, fn) {
|
|
|
134
167
|
await Promise.all(Array.from({ length: n }, () => worker()));
|
|
135
168
|
return results;
|
|
136
169
|
}
|
|
137
|
-
export function spawnGitAsync(cwd, args) {
|
|
170
|
+
export function spawnGitAsync(cwd, args, opts) {
|
|
138
171
|
return new Promise((resolveP) => {
|
|
139
172
|
const child = spawn("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
140
173
|
let stdout = "";
|
|
141
174
|
let stderr = "";
|
|
175
|
+
let settled = false;
|
|
142
176
|
child.stdout?.setEncoding("utf8");
|
|
143
177
|
child.stderr?.setEncoding("utf8");
|
|
144
178
|
child.stdout?.on("data", (c) => {
|
|
@@ -147,8 +181,53 @@ export function spawnGitAsync(cwd, args) {
|
|
|
147
181
|
child.stderr?.on("data", (c) => {
|
|
148
182
|
stderr += c;
|
|
149
183
|
});
|
|
150
|
-
|
|
151
|
-
|
|
184
|
+
const effectiveTimeout = opts?.timeoutMs ?? GIT_SUBPROCESS_TIMEOUT_MS;
|
|
185
|
+
let timer;
|
|
186
|
+
let abortListener;
|
|
187
|
+
function cleanup() {
|
|
188
|
+
if (timer !== undefined) {
|
|
189
|
+
clearTimeout(timer);
|
|
190
|
+
timer = undefined;
|
|
191
|
+
}
|
|
192
|
+
if (abortListener !== undefined && opts?.signal) {
|
|
193
|
+
opts.signal.removeEventListener("abort", abortListener);
|
|
194
|
+
abortListener = undefined;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function settle(result) {
|
|
198
|
+
if (settled)
|
|
199
|
+
return;
|
|
200
|
+
settled = true;
|
|
201
|
+
cleanup();
|
|
202
|
+
resolveP(result);
|
|
203
|
+
}
|
|
204
|
+
// AbortSignal: kill immediately if already aborted, else listen
|
|
205
|
+
if (opts?.signal) {
|
|
206
|
+
if (opts.signal.aborted) {
|
|
207
|
+
child.kill("SIGTERM");
|
|
208
|
+
settle({ ok: false, stdout, stderr, aborted: true });
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
abortListener = () => {
|
|
212
|
+
child.kill("SIGTERM");
|
|
213
|
+
settle({ ok: false, stdout, stderr, aborted: true });
|
|
214
|
+
};
|
|
215
|
+
opts.signal.addEventListener("abort", abortListener, { once: true });
|
|
216
|
+
}
|
|
217
|
+
// Timeout: set timer if effectiveTimeout > 0
|
|
218
|
+
if (effectiveTimeout > 0) {
|
|
219
|
+
timer = setTimeout(() => {
|
|
220
|
+
child.kill("SIGTERM");
|
|
221
|
+
settle({
|
|
222
|
+
ok: false,
|
|
223
|
+
stdout,
|
|
224
|
+
stderr: `${stderr}\n<git timed out after ${effectiveTimeout}ms>`,
|
|
225
|
+
timedOut: true,
|
|
226
|
+
});
|
|
227
|
+
}, effectiveTimeout);
|
|
228
|
+
}
|
|
229
|
+
child.on("error", () => settle({ ok: false, stdout, stderr }));
|
|
230
|
+
child.on("close", (code) => settle({ ok: code === 0, stdout, stderr }));
|
|
152
231
|
});
|
|
153
232
|
}
|
|
154
233
|
function gitStatusFailText(r) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
+
import { ERROR_CODES } from "./error-codes.js";
|
|
2
3
|
import { gitTopLevel } from "./git.js";
|
|
3
4
|
import { jsonRespond, spreadDefined } from "./json.js";
|
|
4
5
|
import { loadPresetsFromGitTop, PRESET_FILE_PATH, presetLoadErrorPayload } from "./presets.js";
|
|
@@ -34,7 +35,7 @@ export function registerListPresetsTool(server) {
|
|
|
34
35
|
presetFile,
|
|
35
36
|
fileExists: false,
|
|
36
37
|
presets: [],
|
|
37
|
-
error: { error:
|
|
38
|
+
error: { error: ERROR_CODES.NOT_A_GIT_REPOSITORY, path: ws },
|
|
38
39
|
});
|
|
39
40
|
continue;
|
|
40
41
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
+
import { ERROR_CODES } from "./error-codes.js";
|
|
2
3
|
import { gitTopLevel } from "./git.js";
|
|
3
4
|
import { jsonRespond, spreadDefined } from "./json.js";
|
|
4
5
|
import { loadPresetsFromGitTop, PRESET_FILE_PATH, presetLoadErrorPayload } from "./presets.js";
|
|
@@ -15,11 +16,11 @@ export function registerPresetsResource(server) {
|
|
|
15
16
|
}
|
|
16
17
|
const ws = pre.roots[0];
|
|
17
18
|
if (!ws) {
|
|
18
|
-
return { text: jsonRespond({ error:
|
|
19
|
+
return { text: jsonRespond({ error: ERROR_CODES.NO_WORKSPACE_ROOT }) };
|
|
19
20
|
}
|
|
20
21
|
const top = gitTopLevel(ws);
|
|
21
22
|
if (!top) {
|
|
22
|
-
return { text: jsonRespond({ error:
|
|
23
|
+
return { text: jsonRespond({ error: ERROR_CODES.NOT_A_GIT_REPOSITORY, path: ws }) };
|
|
23
24
|
}
|
|
24
25
|
const loaded = loadPresetsFromGitTop(top);
|
|
25
26
|
if (!loaded.ok) {
|
package/dist/server/presets.js
CHANGED
|
@@ -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:
|
|
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 {
|
|
96
|
+
return {
|
|
97
|
+
error: ERROR_CODES.PRESET_FILE_INVALID,
|
|
98
|
+
kind: "schema",
|
|
99
|
+
presetFile,
|
|
100
|
+
issues: fail.issues,
|
|
101
|
+
};
|
|
96
102
|
}
|
|
97
|
-
return { error:
|
|
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:
|
|
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:
|
|
125
|
+
error: ERROR_CODES.PRESET_NOT_FOUND,
|
|
120
126
|
preset: presetName,
|
|
121
127
|
presetFile: join(gitTop, PRESET_FILE_PATH),
|
|
122
128
|
},
|
package/dist/server/roots.js
CHANGED
|
@@ -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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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);
|
package/dist/server/tools.js
CHANGED
|
@@ -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/dist/server.js
CHANGED
|
@@ -2,9 +2,16 @@
|
|
|
2
2
|
import { FastMCP } from "fastmcp";
|
|
3
3
|
import { readMcpServerVersion } from "./server/json.js";
|
|
4
4
|
import { registerRethunkGitTools } from "./server/tools.js";
|
|
5
|
+
/**
|
|
6
|
+
* JSON payload contract version. Bump on incompatible JSON output changes
|
|
7
|
+
* (renamed/nested/omitted fields). Surfaced via the FastMCP `instructions`
|
|
8
|
+
* field below, so it is discoverable in the MCP `initialize` response.
|
|
9
|
+
*/
|
|
10
|
+
export const MCP_JSON_FORMAT_VERSION = "3";
|
|
5
11
|
const server = new FastMCP({
|
|
6
12
|
name: "rethunk-git",
|
|
7
13
|
version: readMcpServerVersion(),
|
|
14
|
+
instructions: `rethunk-git MCP server. JSON payload contract: format version ${MCP_JSON_FORMAT_VERSION} (minified, no envelope; optional fields omitted when empty/null/false).`,
|
|
8
15
|
roots: { enabled: true },
|
|
9
16
|
});
|
|
10
17
|
registerRethunkGitTools(server);
|