@selfagency/git-mcp 0.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/README.md +228 -0
- package/index.d.ts +1 -0
- package/index.js +2950 -0
- package/index.js.map +1 -0
- package/package.json +45 -0
package/index.js
ADDED
|
@@ -0,0 +1,2950 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
import { z as z11 } from "zod";
|
|
7
|
+
|
|
8
|
+
// src/config.ts
|
|
9
|
+
import path from "path";
|
|
10
|
+
function parseCliRepoPath() {
|
|
11
|
+
const args = process.argv.slice(2);
|
|
12
|
+
for (let i = 0; i < args.length; i++) {
|
|
13
|
+
const arg = args[i] ?? "";
|
|
14
|
+
if ((arg === "--repo" || arg === "--repo-path") && i + 1 < args.length) {
|
|
15
|
+
return args[i + 1];
|
|
16
|
+
}
|
|
17
|
+
const match = /^--repo(?:-path)?=(.+)$/.exec(arg);
|
|
18
|
+
if (match?.[1]) return match[1];
|
|
19
|
+
}
|
|
20
|
+
return void 0;
|
|
21
|
+
}
|
|
22
|
+
var configured = process.env["GIT_REPO_PATH"] ?? parseCliRepoPath();
|
|
23
|
+
var DEFAULT_REPO_PATH = configured ? path.resolve(configured) : void 0;
|
|
24
|
+
function resolveRepoPath(repoPath) {
|
|
25
|
+
const resolved = repoPath ?? DEFAULT_REPO_PATH;
|
|
26
|
+
if (!resolved) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
"No repository path provided. Pass repo_path in the tool request, or configure a server default via the GIT_REPO_PATH environment variable or the --repo / --repo-path CLI argument."
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
return resolved;
|
|
32
|
+
}
|
|
33
|
+
var ALLOW_NO_VERIFY = process.env["GIT_ALLOW_NO_VERIFY"] === "true";
|
|
34
|
+
var ALLOW_FORCE_PUSH = process.env["GIT_ALLOW_FORCE_PUSH"] === "true";
|
|
35
|
+
var DEFAULT_SIGNING_KEY = process.env["GIT_SIGNING_KEY"] || void 0;
|
|
36
|
+
var DEFAULT_SIGNING_FORMAT = process.env["GIT_SIGNING_FORMAT"] || void 0;
|
|
37
|
+
var AUTO_SIGN_COMMITS = process.env["GIT_AUTO_SIGN_COMMITS"] === "true";
|
|
38
|
+
var AUTO_SIGN_TAGS = process.env["GIT_AUTO_SIGN_TAGS"] === "true";
|
|
39
|
+
|
|
40
|
+
// src/constants.ts
|
|
41
|
+
var SERVER_NAME = "git-mcp-server";
|
|
42
|
+
var SERVER_VERSION = "0.1.0";
|
|
43
|
+
var CHARACTER_LIMIT = 25e3;
|
|
44
|
+
var EXCLUDED_DIFF_DIRECTORIES = ["node_modules/", ".yarn/", ".astro/", "dist/"];
|
|
45
|
+
var EXCLUDED_DIFF_EXTENSIONS = [
|
|
46
|
+
"png",
|
|
47
|
+
"jpg",
|
|
48
|
+
"jpeg",
|
|
49
|
+
"gif",
|
|
50
|
+
"svg",
|
|
51
|
+
"ico",
|
|
52
|
+
"webp",
|
|
53
|
+
"bmp",
|
|
54
|
+
"tiff",
|
|
55
|
+
"mp4",
|
|
56
|
+
"mp3",
|
|
57
|
+
"wav",
|
|
58
|
+
"ogg",
|
|
59
|
+
"pdf",
|
|
60
|
+
"woff",
|
|
61
|
+
"woff2",
|
|
62
|
+
"ttf",
|
|
63
|
+
"eot",
|
|
64
|
+
"zip",
|
|
65
|
+
"tar",
|
|
66
|
+
"gz"
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
// src/resources/git.resources.ts
|
|
70
|
+
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
71
|
+
|
|
72
|
+
// src/git/client.ts
|
|
73
|
+
import { existsSync, statSync } from "fs";
|
|
74
|
+
import path2 from "path";
|
|
75
|
+
import { simpleGit } from "simple-git";
|
|
76
|
+
var GIT_NOT_FOUND_PATTERN = /(not found|is not recognized|ENOENT)/i;
|
|
77
|
+
var PERMISSION_PATTERN = /(permission denied|EACCES|EPERM)/i;
|
|
78
|
+
var CONFLICT_PATTERN = /(CONFLICT|merge conflict|rebase in progress|cherry-pick in progress)/i;
|
|
79
|
+
var NETWORK_PATTERN = /(network|timed out|unable to access|could not resolve host|proxy)/i;
|
|
80
|
+
function classifyError(message) {
|
|
81
|
+
if (GIT_NOT_FOUND_PATTERN.test(message)) {
|
|
82
|
+
return "missing_git";
|
|
83
|
+
}
|
|
84
|
+
if (PERMISSION_PATTERN.test(message)) {
|
|
85
|
+
return "permission";
|
|
86
|
+
}
|
|
87
|
+
if (CONFLICT_PATTERN.test(message)) {
|
|
88
|
+
return "git_conflict";
|
|
89
|
+
}
|
|
90
|
+
if (NETWORK_PATTERN.test(message)) {
|
|
91
|
+
return "network";
|
|
92
|
+
}
|
|
93
|
+
return "unknown";
|
|
94
|
+
}
|
|
95
|
+
function toGitError(error) {
|
|
96
|
+
if (error instanceof Error) {
|
|
97
|
+
return {
|
|
98
|
+
kind: classifyError(error.message),
|
|
99
|
+
message: error.message
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
kind: "unknown",
|
|
104
|
+
message: String(error)
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function validateRepoPath(repoPath) {
|
|
108
|
+
const resolved = path2.resolve(repoPath);
|
|
109
|
+
if (!existsSync(resolved)) {
|
|
110
|
+
throw new Error(`Repository path does not exist: ${repoPath}`);
|
|
111
|
+
}
|
|
112
|
+
if (!statSync(resolved).isDirectory()) {
|
|
113
|
+
throw new Error(`Repository path is not a directory: ${repoPath}`);
|
|
114
|
+
}
|
|
115
|
+
return resolved;
|
|
116
|
+
}
|
|
117
|
+
function getGit(repoPath) {
|
|
118
|
+
const safePath = validateRepoPath(repoPath);
|
|
119
|
+
return simpleGit({ baseDir: safePath, binary: "git", maxConcurrentProcesses: 6 });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// src/services/branch.service.ts
|
|
123
|
+
async function listBranches(repoPath, all) {
|
|
124
|
+
const git = getGit(repoPath);
|
|
125
|
+
const summary = await git.branch(all ? ["-a"] : []);
|
|
126
|
+
return summary.all.map((name) => {
|
|
127
|
+
const details = Object.hasOwn(summary.branches, name) ? summary.branches[name] : void 0;
|
|
128
|
+
return {
|
|
129
|
+
name,
|
|
130
|
+
isCurrent: summary.current === name,
|
|
131
|
+
commit: details?.commit,
|
|
132
|
+
upstream: details?.label
|
|
133
|
+
};
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
async function createBranch(repoPath, options) {
|
|
137
|
+
const git = getGit(repoPath);
|
|
138
|
+
if (options.fromRef) {
|
|
139
|
+
if (options.checkout) {
|
|
140
|
+
await git.checkoutBranch(options.name, options.fromRef);
|
|
141
|
+
} else {
|
|
142
|
+
await git.raw(["branch", options.name, options.fromRef]);
|
|
143
|
+
}
|
|
144
|
+
return options.checkout ? `Created and checked out ${options.name} from ${options.fromRef}.` : `Created branch ${options.name} from ${options.fromRef}.`;
|
|
145
|
+
}
|
|
146
|
+
await git.branch([options.name]);
|
|
147
|
+
if (options.checkout) {
|
|
148
|
+
await git.checkout(options.name);
|
|
149
|
+
}
|
|
150
|
+
return options.checkout ? `Created and checked out ${options.name}.` : `Created branch ${options.name}.`;
|
|
151
|
+
}
|
|
152
|
+
async function deleteBranch(repoPath, options) {
|
|
153
|
+
const git = getGit(repoPath);
|
|
154
|
+
await git.deleteLocalBranch(options.name, options.force);
|
|
155
|
+
return `Deleted branch ${options.name}.`;
|
|
156
|
+
}
|
|
157
|
+
async function renameBranch(repoPath, oldName, newName) {
|
|
158
|
+
const git = getGit(repoPath);
|
|
159
|
+
await git.branch(["-m", oldName, newName]);
|
|
160
|
+
return `Renamed branch ${oldName} to ${newName}.`;
|
|
161
|
+
}
|
|
162
|
+
async function checkoutRef(repoPath, ref, create) {
|
|
163
|
+
const git = getGit(repoPath);
|
|
164
|
+
if (create) {
|
|
165
|
+
await git.checkoutLocalBranch(ref);
|
|
166
|
+
return `Created and checked out ${ref}.`;
|
|
167
|
+
}
|
|
168
|
+
await git.checkout(ref);
|
|
169
|
+
return `Checked out ${ref}.`;
|
|
170
|
+
}
|
|
171
|
+
async function setUpstream(repoPath, branch, upstream) {
|
|
172
|
+
const git = getGit(repoPath);
|
|
173
|
+
await git.raw(["branch", "--set-upstream-to", upstream, branch]);
|
|
174
|
+
return `Set upstream of ${branch} to ${upstream}.`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// src/services/inspect.service.ts
|
|
178
|
+
import path3 from "path";
|
|
179
|
+
function truncate(text) {
|
|
180
|
+
if (text.length <= CHARACTER_LIMIT) {
|
|
181
|
+
return text;
|
|
182
|
+
}
|
|
183
|
+
return `${text.slice(0, CHARACTER_LIMIT)}
|
|
184
|
+
|
|
185
|
+
[truncated to ${CHARACTER_LIMIT} characters]`;
|
|
186
|
+
}
|
|
187
|
+
function parseLogLine(line) {
|
|
188
|
+
const [hash, authorName, authorEmail, dateIso, ...subjectParts] = line.split(" ");
|
|
189
|
+
if (!hash || !authorName || !authorEmail || !dateIso) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
hash,
|
|
194
|
+
authorName,
|
|
195
|
+
authorEmail,
|
|
196
|
+
dateIso,
|
|
197
|
+
subject: subjectParts.join(" ")
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function shouldExcludeFile(filePath) {
|
|
201
|
+
if (EXCLUDED_DIFF_DIRECTORIES.some((prefix) => filePath.startsWith(prefix))) {
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
const extension = path3.extname(filePath).replace(/^\./, "").toLowerCase();
|
|
205
|
+
if (!extension) {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
return EXCLUDED_DIFF_EXTENSIONS.includes(extension);
|
|
209
|
+
}
|
|
210
|
+
function buildDiffBaseArgs(options) {
|
|
211
|
+
if (options.mode === "staged") {
|
|
212
|
+
return ["diff", "--staged"];
|
|
213
|
+
}
|
|
214
|
+
if (options.mode === "refs") {
|
|
215
|
+
if (!options.fromRef || !options.toRef) {
|
|
216
|
+
throw new Error("from_ref and to_ref are required when mode='refs'");
|
|
217
|
+
}
|
|
218
|
+
return ["diff", `${options.fromRef}..${options.toRef}`];
|
|
219
|
+
}
|
|
220
|
+
return ["diff"];
|
|
221
|
+
}
|
|
222
|
+
async function getStatus(repoPath) {
|
|
223
|
+
const git = getGit(repoPath);
|
|
224
|
+
const status = await git.status();
|
|
225
|
+
const files = status.files.map((file) => ({
|
|
226
|
+
path: file.path,
|
|
227
|
+
index: file.index,
|
|
228
|
+
workingTree: file.working_dir
|
|
229
|
+
}));
|
|
230
|
+
return {
|
|
231
|
+
branch: status.current ?? "",
|
|
232
|
+
current: status.current ?? "",
|
|
233
|
+
tracking: status.tracking ?? "",
|
|
234
|
+
ahead: status.ahead,
|
|
235
|
+
behind: status.behind,
|
|
236
|
+
files,
|
|
237
|
+
isClean: status.isClean()
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
async function getLog(repoPath, options) {
|
|
241
|
+
const git = getGit(repoPath);
|
|
242
|
+
const args = [
|
|
243
|
+
"log",
|
|
244
|
+
"--date=iso-strict",
|
|
245
|
+
`--skip=${options.offset}`,
|
|
246
|
+
"-n",
|
|
247
|
+
String(options.limit),
|
|
248
|
+
"--pretty=format:%H%x09%an%x09%ae%x09%aI%x09%s"
|
|
249
|
+
];
|
|
250
|
+
if (options.author) {
|
|
251
|
+
args.push(`--author=${options.author}`);
|
|
252
|
+
}
|
|
253
|
+
if (options.grep) {
|
|
254
|
+
args.push(`--grep=${options.grep}`);
|
|
255
|
+
}
|
|
256
|
+
if (options.since) {
|
|
257
|
+
args.push(`--since=${options.since}`);
|
|
258
|
+
}
|
|
259
|
+
if (options.until) {
|
|
260
|
+
args.push(`--until=${options.until}`);
|
|
261
|
+
}
|
|
262
|
+
if (options.filePath) {
|
|
263
|
+
args.push("--", options.filePath);
|
|
264
|
+
}
|
|
265
|
+
const output = await git.raw(args);
|
|
266
|
+
return output.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).map(parseLogLine).filter((item) => item !== null);
|
|
267
|
+
}
|
|
268
|
+
async function showRef(repoPath, ref) {
|
|
269
|
+
const git = getGit(repoPath);
|
|
270
|
+
const output = await git.raw(["show", "--stat", "--patch", ref]);
|
|
271
|
+
return truncate(output);
|
|
272
|
+
}
|
|
273
|
+
async function getDiffSummary(repoPath, options) {
|
|
274
|
+
const git = getGit(repoPath);
|
|
275
|
+
const baseArgs = buildDiffBaseArgs(options).slice(1);
|
|
276
|
+
const summary = await git.diffSummary(baseArgs);
|
|
277
|
+
return {
|
|
278
|
+
filesChanged: summary.files.length,
|
|
279
|
+
insertions: summary.insertions,
|
|
280
|
+
deletions: summary.deletions
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
async function getDiff(repoPath, options) {
|
|
284
|
+
const git = getGit(repoPath);
|
|
285
|
+
const baseArgs = buildDiffBaseArgs(options);
|
|
286
|
+
if (!options.filtered) {
|
|
287
|
+
const output = await git.raw(baseArgs);
|
|
288
|
+
return truncate(output);
|
|
289
|
+
}
|
|
290
|
+
const namesOutput = await git.raw([...baseArgs, "--name-only"]);
|
|
291
|
+
const files = namesOutput.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).filter((line) => !shouldExcludeFile(line));
|
|
292
|
+
if (files.length === 0) {
|
|
293
|
+
return "No changed files after filtering.";
|
|
294
|
+
}
|
|
295
|
+
const chunks = [];
|
|
296
|
+
for (const filePath of files) {
|
|
297
|
+
const diff = await git.raw([...baseArgs, "--", filePath]);
|
|
298
|
+
chunks.push(`=== ${filePath} ===
|
|
299
|
+
${diff.trim()}`);
|
|
300
|
+
}
|
|
301
|
+
return truncate(chunks.join("\n\n"));
|
|
302
|
+
}
|
|
303
|
+
async function blameFile(repoPath, filePath, ref) {
|
|
304
|
+
const git = getGit(repoPath);
|
|
305
|
+
const args = ["blame"];
|
|
306
|
+
if (ref) {
|
|
307
|
+
args.push(ref);
|
|
308
|
+
}
|
|
309
|
+
args.push("--", filePath);
|
|
310
|
+
const output = await git.raw(args);
|
|
311
|
+
return truncate(output);
|
|
312
|
+
}
|
|
313
|
+
async function getReflog(repoPath, limit) {
|
|
314
|
+
const git = getGit(repoPath);
|
|
315
|
+
const output = await git.raw(["reflog", "--date=iso", "-n", String(limit)]);
|
|
316
|
+
return truncate(output);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// src/resources/git.resources.ts
|
|
320
|
+
function decodeRepoPath(value) {
|
|
321
|
+
return decodeURIComponent(value);
|
|
322
|
+
}
|
|
323
|
+
function stringify(data) {
|
|
324
|
+
return JSON.stringify(data, null, 2);
|
|
325
|
+
}
|
|
326
|
+
function registerGitResources(server2) {
|
|
327
|
+
const templateConfig = { list: void 0 };
|
|
328
|
+
server2.registerResource(
|
|
329
|
+
"git_repo_status",
|
|
330
|
+
new ResourceTemplate("git+repo://status/{repo_path}", templateConfig),
|
|
331
|
+
{
|
|
332
|
+
title: "Git Repository Status",
|
|
333
|
+
description: "Read-only repository status snapshot.",
|
|
334
|
+
mimeType: "application/json"
|
|
335
|
+
},
|
|
336
|
+
async (uri, variables) => {
|
|
337
|
+
const repoPath = decodeRepoPath(String(variables.repo_path ?? ""));
|
|
338
|
+
const status = await getStatus(repoPath);
|
|
339
|
+
return {
|
|
340
|
+
contents: [{ uri: uri.toString(), mimeType: "application/json", text: stringify(status) }]
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
);
|
|
344
|
+
server2.registerResource(
|
|
345
|
+
"git_repo_log",
|
|
346
|
+
new ResourceTemplate("git+repo://log/{repo_path}", templateConfig),
|
|
347
|
+
{
|
|
348
|
+
title: "Git Repository Log",
|
|
349
|
+
description: "Read-only recent commit log.",
|
|
350
|
+
mimeType: "application/json"
|
|
351
|
+
},
|
|
352
|
+
async (uri, variables) => {
|
|
353
|
+
const repoPath = decodeRepoPath(String(variables.repo_path ?? ""));
|
|
354
|
+
const commits = await getLog(repoPath, { limit: 20, offset: 0 });
|
|
355
|
+
return {
|
|
356
|
+
contents: [{ uri: uri.toString(), mimeType: "application/json", text: stringify({ commits }) }]
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
);
|
|
360
|
+
server2.registerResource(
|
|
361
|
+
"git_repo_branches",
|
|
362
|
+
new ResourceTemplate("git+repo://branches/{repo_path}", templateConfig),
|
|
363
|
+
{
|
|
364
|
+
title: "Git Repository Branches",
|
|
365
|
+
description: "Read-only branch list.",
|
|
366
|
+
mimeType: "application/json"
|
|
367
|
+
},
|
|
368
|
+
async (uri, variables) => {
|
|
369
|
+
const repoPath = decodeRepoPath(String(variables.repo_path ?? ""));
|
|
370
|
+
const branches = await listBranches(repoPath, true);
|
|
371
|
+
return {
|
|
372
|
+
contents: [{ uri: uri.toString(), mimeType: "application/json", text: stringify({ branches }) }]
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
);
|
|
376
|
+
server2.registerResource(
|
|
377
|
+
"git_repo_diff",
|
|
378
|
+
new ResourceTemplate("git+repo://diff/{repo_path}", templateConfig),
|
|
379
|
+
{
|
|
380
|
+
title: "Git Repository Diff",
|
|
381
|
+
description: "Read-only unstaged + staged diff views.",
|
|
382
|
+
mimeType: "application/json"
|
|
383
|
+
},
|
|
384
|
+
async (uri, variables) => {
|
|
385
|
+
const repoPath = decodeRepoPath(String(variables.repo_path ?? ""));
|
|
386
|
+
const [unstaged, staged] = await Promise.all([
|
|
387
|
+
getDiff(repoPath, { mode: "unstaged", filtered: false }),
|
|
388
|
+
getDiff(repoPath, { mode: "staged", filtered: false })
|
|
389
|
+
]);
|
|
390
|
+
return {
|
|
391
|
+
contents: [
|
|
392
|
+
{
|
|
393
|
+
uri: uri.toString(),
|
|
394
|
+
mimeType: "application/json",
|
|
395
|
+
text: stringify({ unstaged, staged })
|
|
396
|
+
}
|
|
397
|
+
]
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// src/tools/advanced.tools.ts
|
|
404
|
+
import { z as z2 } from "zod";
|
|
405
|
+
|
|
406
|
+
// src/schemas/index.ts
|
|
407
|
+
import { z } from "zod";
|
|
408
|
+
var RepoPathSchema = z.string().min(1).optional().describe(
|
|
409
|
+
"Absolute path to the local Git repository. If omitted, falls back to the server default set via the GIT_REPO_PATH environment variable or --repo-path CLI argument."
|
|
410
|
+
);
|
|
411
|
+
var RefSchema = z.string().min(1, "ref is required").describe("Git reference: branch, tag, commit SHA, or HEAD expression.");
|
|
412
|
+
var PaginationSchema = z.object({
|
|
413
|
+
limit: z.number().int().min(1).max(200).default(50),
|
|
414
|
+
offset: z.number().int().min(0).default(0)
|
|
415
|
+
}).strict();
|
|
416
|
+
var ConfirmSchema = z.boolean().default(false);
|
|
417
|
+
var ResponseFormatSchema = z.enum(["markdown", "json"]).default("markdown").describe("Output format for the response.");
|
|
418
|
+
|
|
419
|
+
// src/services/advanced.service.ts
|
|
420
|
+
async function runStashAction(repoPath, options) {
|
|
421
|
+
const git = getGit(repoPath);
|
|
422
|
+
if (options.action === "save") {
|
|
423
|
+
const args = ["stash", "push"];
|
|
424
|
+
if (options.includeUntracked) {
|
|
425
|
+
args.push("--include-untracked");
|
|
426
|
+
}
|
|
427
|
+
if (options.message) {
|
|
428
|
+
args.push("-m", options.message);
|
|
429
|
+
}
|
|
430
|
+
const output2 = await git.raw(args);
|
|
431
|
+
return output2.trim() || "Stash saved.";
|
|
432
|
+
}
|
|
433
|
+
if (options.action === "list") {
|
|
434
|
+
const output2 = await git.raw(["stash", "list"]);
|
|
435
|
+
return output2.trim() || "No stashes.";
|
|
436
|
+
}
|
|
437
|
+
const index = options.index ?? 0;
|
|
438
|
+
if (options.action === "apply") {
|
|
439
|
+
const output2 = await git.raw(["stash", "apply", `stash@{${index}}`]);
|
|
440
|
+
return output2.trim() || `Applied stash@{${index}}.`;
|
|
441
|
+
}
|
|
442
|
+
if (options.action === "pop") {
|
|
443
|
+
const output2 = await git.raw(["stash", "pop", `stash@{${index}}`]);
|
|
444
|
+
return output2.trim() || `Popped stash@{${index}}.`;
|
|
445
|
+
}
|
|
446
|
+
const output = await git.raw(["stash", "drop", `stash@{${index}}`]);
|
|
447
|
+
return output.trim() || `Dropped stash@{${index}}.`;
|
|
448
|
+
}
|
|
449
|
+
async function runRebaseAction(repoPath, options) {
|
|
450
|
+
const git = getGit(repoPath);
|
|
451
|
+
if (options.action === "continue") {
|
|
452
|
+
const output2 = await git.raw(["rebase", "--continue"]);
|
|
453
|
+
return output2.trim() || "Rebase continued.";
|
|
454
|
+
}
|
|
455
|
+
if (options.action === "abort") {
|
|
456
|
+
const output2 = await git.raw(["rebase", "--abort"]);
|
|
457
|
+
return output2.trim() || "Rebase aborted.";
|
|
458
|
+
}
|
|
459
|
+
if (options.action === "skip") {
|
|
460
|
+
const output2 = await git.raw(["rebase", "--skip"]);
|
|
461
|
+
return output2.trim() || "Rebase skipped current commit.";
|
|
462
|
+
}
|
|
463
|
+
if (!options.onto) {
|
|
464
|
+
throw new Error("onto is required for rebase start.");
|
|
465
|
+
}
|
|
466
|
+
const output = await git.raw(["rebase", options.onto]);
|
|
467
|
+
return output.trim() || `Rebase started onto ${options.onto}.`;
|
|
468
|
+
}
|
|
469
|
+
async function runCherryPickAction(repoPath, options) {
|
|
470
|
+
const git = getGit(repoPath);
|
|
471
|
+
if (options.action === "continue") {
|
|
472
|
+
const output2 = await git.raw(["cherry-pick", "--continue"]);
|
|
473
|
+
return output2.trim() || "Cherry-pick continued.";
|
|
474
|
+
}
|
|
475
|
+
if (options.action === "abort") {
|
|
476
|
+
const output2 = await git.raw(["cherry-pick", "--abort"]);
|
|
477
|
+
return output2.trim() || "Cherry-pick aborted.";
|
|
478
|
+
}
|
|
479
|
+
if (!options.ref) {
|
|
480
|
+
throw new Error("ref is required when cherry-pick action is start.");
|
|
481
|
+
}
|
|
482
|
+
const output = await git.raw(["cherry-pick", options.ref]);
|
|
483
|
+
return output.trim() || `Cherry-picked ${options.ref}.`;
|
|
484
|
+
}
|
|
485
|
+
async function runBisectAction(repoPath, options) {
|
|
486
|
+
const git = getGit(repoPath);
|
|
487
|
+
switch (options.action) {
|
|
488
|
+
case "start": {
|
|
489
|
+
if (!options.badRef || !options.goodRef) {
|
|
490
|
+
throw new Error("goodRef and badRef are required for bisect start.");
|
|
491
|
+
}
|
|
492
|
+
await git.raw(["bisect", "start"]);
|
|
493
|
+
await git.raw(["bisect", "bad", options.badRef]);
|
|
494
|
+
await git.raw(["bisect", "good", options.goodRef]);
|
|
495
|
+
return `Bisect started between good=${options.goodRef} and bad=${options.badRef}.`;
|
|
496
|
+
}
|
|
497
|
+
case "good": {
|
|
498
|
+
const output = await git.raw(["bisect", "good", ...options.ref ? [options.ref] : []]);
|
|
499
|
+
return output.trim() || "Marked current commit as good.";
|
|
500
|
+
}
|
|
501
|
+
case "bad": {
|
|
502
|
+
const output = await git.raw(["bisect", "bad", ...options.ref ? [options.ref] : []]);
|
|
503
|
+
return output.trim() || "Marked current commit as bad.";
|
|
504
|
+
}
|
|
505
|
+
case "skip": {
|
|
506
|
+
const output = await git.raw(["bisect", "skip", ...options.ref ? [options.ref] : []]);
|
|
507
|
+
return output.trim() || "Skipped current bisect commit.";
|
|
508
|
+
}
|
|
509
|
+
case "run": {
|
|
510
|
+
if (!options.command) {
|
|
511
|
+
throw new Error("command is required for bisect run.");
|
|
512
|
+
}
|
|
513
|
+
const output = await git.raw(["bisect", "run", "sh", "-lc", options.command]);
|
|
514
|
+
return output.trim() || "Bisect run completed.";
|
|
515
|
+
}
|
|
516
|
+
case "reset": {
|
|
517
|
+
const output = await git.raw(["bisect", "reset"]);
|
|
518
|
+
return output.trim() || "Bisect reset.";
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
async function runTagAction(repoPath, options) {
|
|
523
|
+
const git = getGit(repoPath);
|
|
524
|
+
if (options.action === "list") {
|
|
525
|
+
const output = await git.tags();
|
|
526
|
+
return output.all.join("\n") || "No tags.";
|
|
527
|
+
}
|
|
528
|
+
if (options.action === "delete") {
|
|
529
|
+
if (!options.name) {
|
|
530
|
+
throw new Error("name is required for delete action.");
|
|
531
|
+
}
|
|
532
|
+
await git.tag(["-d", options.name]);
|
|
533
|
+
return `Deleted tag ${options.name}.`;
|
|
534
|
+
}
|
|
535
|
+
if (!options.name) {
|
|
536
|
+
throw new Error("name is required for create action.");
|
|
537
|
+
}
|
|
538
|
+
const shouldSign = options.sign ?? AUTO_SIGN_TAGS;
|
|
539
|
+
if (shouldSign) {
|
|
540
|
+
const key = options.signingKey ?? DEFAULT_SIGNING_KEY;
|
|
541
|
+
const signFlag = key ? ["-u", key] : ["-s"];
|
|
542
|
+
const msgFlag = options.message ? ["-m", options.message] : ["-m", options.name];
|
|
543
|
+
const targetArg = options.target ? [options.target] : [];
|
|
544
|
+
await git.raw(["tag", ...signFlag, ...msgFlag, options.name, ...targetArg]);
|
|
545
|
+
return `Created signed tag ${options.name}.`;
|
|
546
|
+
}
|
|
547
|
+
if (options.message) {
|
|
548
|
+
await git.addAnnotatedTag(options.name, options.message);
|
|
549
|
+
return `Created annotated tag ${options.name}.`;
|
|
550
|
+
}
|
|
551
|
+
await git.addTag(options.name);
|
|
552
|
+
return `Created tag ${options.name}.`;
|
|
553
|
+
}
|
|
554
|
+
async function runWorktreeAction(repoPath, options) {
|
|
555
|
+
const git = getGit(repoPath);
|
|
556
|
+
if (options.action === "list") {
|
|
557
|
+
const output = await git.raw(["worktree", "list", "--porcelain"]);
|
|
558
|
+
return output.trim();
|
|
559
|
+
}
|
|
560
|
+
if (options.action === "remove") {
|
|
561
|
+
if (!options.path) {
|
|
562
|
+
throw new Error("path is required for worktree remove.");
|
|
563
|
+
}
|
|
564
|
+
await git.raw(["worktree", "remove", options.path]);
|
|
565
|
+
return `Removed worktree ${options.path}.`;
|
|
566
|
+
}
|
|
567
|
+
if (!options.path || !options.branch) {
|
|
568
|
+
throw new Error("path and branch are required for worktree add.");
|
|
569
|
+
}
|
|
570
|
+
await git.raw(["worktree", "add", options.path, options.branch]);
|
|
571
|
+
return `Added worktree at ${options.path} for ${options.branch}.`;
|
|
572
|
+
}
|
|
573
|
+
async function runSubmoduleAction(repoPath, options) {
|
|
574
|
+
const git = getGit(repoPath);
|
|
575
|
+
if (options.action === "list") {
|
|
576
|
+
const output = await git.raw(["submodule", "status"]);
|
|
577
|
+
return output.trim() || "No submodules.";
|
|
578
|
+
}
|
|
579
|
+
if (options.action === "sync") {
|
|
580
|
+
const args = ["submodule", "sync"];
|
|
581
|
+
if (options.recursive) {
|
|
582
|
+
args.push("--recursive");
|
|
583
|
+
}
|
|
584
|
+
const output = await git.raw(args);
|
|
585
|
+
return output.trim() || "Submodule sync complete.";
|
|
586
|
+
}
|
|
587
|
+
if (options.action === "update") {
|
|
588
|
+
const args = ["submodule", "update", "--init"];
|
|
589
|
+
if (options.recursive) {
|
|
590
|
+
args.push("--recursive");
|
|
591
|
+
}
|
|
592
|
+
const output = await git.raw(args);
|
|
593
|
+
return output.trim() || "Submodule update complete.";
|
|
594
|
+
}
|
|
595
|
+
if (!options.url || !options.path) {
|
|
596
|
+
throw new Error("url and path are required for submodule add.");
|
|
597
|
+
}
|
|
598
|
+
await git.raw(["submodule", "add", options.url, options.path]);
|
|
599
|
+
return `Added submodule ${options.path}.`;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// src/tools/advanced.tools.ts
|
|
603
|
+
function render(content, format) {
|
|
604
|
+
if (typeof content === "string" && format === "markdown") {
|
|
605
|
+
return content;
|
|
606
|
+
}
|
|
607
|
+
return JSON.stringify(content, null, 2);
|
|
608
|
+
}
|
|
609
|
+
function buildError(error) {
|
|
610
|
+
const gitError = toGitError(error);
|
|
611
|
+
return { content: [{ type: "text", text: `Error (${gitError.kind}): ${gitError.message}` }] };
|
|
612
|
+
}
|
|
613
|
+
function registerAdvancedTools(server2) {
|
|
614
|
+
server2.registerTool(
|
|
615
|
+
"git_stash",
|
|
616
|
+
{
|
|
617
|
+
title: "Git Stash Actions",
|
|
618
|
+
description: "Save, list, apply, pop, or drop stash entries.",
|
|
619
|
+
inputSchema: {
|
|
620
|
+
repo_path: RepoPathSchema,
|
|
621
|
+
action: z2.enum(["save", "list", "apply", "pop", "drop"]),
|
|
622
|
+
message: z2.string().optional(),
|
|
623
|
+
index: z2.number().int().min(0).optional(),
|
|
624
|
+
include_untracked: z2.boolean().default(false),
|
|
625
|
+
response_format: ResponseFormatSchema
|
|
626
|
+
},
|
|
627
|
+
annotations: {
|
|
628
|
+
readOnlyHint: false,
|
|
629
|
+
idempotentHint: false,
|
|
630
|
+
destructiveHint: true,
|
|
631
|
+
openWorldHint: false
|
|
632
|
+
}
|
|
633
|
+
},
|
|
634
|
+
async ({
|
|
635
|
+
repo_path,
|
|
636
|
+
action,
|
|
637
|
+
message,
|
|
638
|
+
index,
|
|
639
|
+
include_untracked,
|
|
640
|
+
response_format
|
|
641
|
+
}) => {
|
|
642
|
+
try {
|
|
643
|
+
const repoPath = resolveRepoPath(repo_path);
|
|
644
|
+
const output = await runStashAction(repoPath, {
|
|
645
|
+
action,
|
|
646
|
+
message,
|
|
647
|
+
index,
|
|
648
|
+
includeUntracked: include_untracked
|
|
649
|
+
});
|
|
650
|
+
return {
|
|
651
|
+
content: [{ type: "text", text: render({ output }, response_format) }],
|
|
652
|
+
structuredContent: { output }
|
|
653
|
+
};
|
|
654
|
+
} catch (error) {
|
|
655
|
+
return buildError(error);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
);
|
|
659
|
+
server2.registerTool(
|
|
660
|
+
"git_rebase",
|
|
661
|
+
{
|
|
662
|
+
title: "Git Rebase Actions",
|
|
663
|
+
description: "Start, continue, skip, or abort rebase operations.",
|
|
664
|
+
inputSchema: {
|
|
665
|
+
repo_path: RepoPathSchema,
|
|
666
|
+
action: z2.enum(["start", "continue", "abort", "skip"]),
|
|
667
|
+
onto: z2.string().optional(),
|
|
668
|
+
response_format: ResponseFormatSchema
|
|
669
|
+
},
|
|
670
|
+
annotations: {
|
|
671
|
+
readOnlyHint: false,
|
|
672
|
+
idempotentHint: false,
|
|
673
|
+
destructiveHint: true,
|
|
674
|
+
openWorldHint: false
|
|
675
|
+
}
|
|
676
|
+
},
|
|
677
|
+
async ({
|
|
678
|
+
repo_path,
|
|
679
|
+
action,
|
|
680
|
+
onto,
|
|
681
|
+
response_format
|
|
682
|
+
}) => {
|
|
683
|
+
try {
|
|
684
|
+
const repoPath = resolveRepoPath(repo_path);
|
|
685
|
+
const output = await runRebaseAction(repoPath, { action, onto });
|
|
686
|
+
return {
|
|
687
|
+
content: [{ type: "text", text: render({ output }, response_format) }],
|
|
688
|
+
structuredContent: { output }
|
|
689
|
+
};
|
|
690
|
+
} catch (error) {
|
|
691
|
+
return buildError(error);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
);
|
|
695
|
+
server2.registerTool(
|
|
696
|
+
"git_cherry_pick",
|
|
697
|
+
{
|
|
698
|
+
title: "Git Cherry-pick Actions",
|
|
699
|
+
description: "Start, continue, or abort cherry-pick.",
|
|
700
|
+
inputSchema: {
|
|
701
|
+
repo_path: RepoPathSchema,
|
|
702
|
+
action: z2.enum(["start", "continue", "abort"]),
|
|
703
|
+
ref: z2.string().optional(),
|
|
704
|
+
response_format: ResponseFormatSchema
|
|
705
|
+
},
|
|
706
|
+
annotations: {
|
|
707
|
+
readOnlyHint: false,
|
|
708
|
+
idempotentHint: false,
|
|
709
|
+
destructiveHint: true,
|
|
710
|
+
openWorldHint: false
|
|
711
|
+
}
|
|
712
|
+
},
|
|
713
|
+
async ({
|
|
714
|
+
repo_path,
|
|
715
|
+
action,
|
|
716
|
+
ref,
|
|
717
|
+
response_format
|
|
718
|
+
}) => {
|
|
719
|
+
try {
|
|
720
|
+
const repoPath = resolveRepoPath(repo_path);
|
|
721
|
+
const output = await runCherryPickAction(repoPath, { action, ref });
|
|
722
|
+
return {
|
|
723
|
+
content: [{ type: "text", text: render({ output }, response_format) }],
|
|
724
|
+
structuredContent: { output }
|
|
725
|
+
};
|
|
726
|
+
} catch (error) {
|
|
727
|
+
return buildError(error);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
);
|
|
731
|
+
server2.registerTool(
|
|
732
|
+
"git_bisect",
|
|
733
|
+
{
|
|
734
|
+
title: "Git Bisect Actions",
|
|
735
|
+
description: "Run bisect workflows for bug isolation.",
|
|
736
|
+
inputSchema: {
|
|
737
|
+
repo_path: RepoPathSchema,
|
|
738
|
+
action: z2.enum(["start", "good", "bad", "skip", "run", "reset"]),
|
|
739
|
+
ref: z2.string().optional(),
|
|
740
|
+
good_ref: z2.string().optional(),
|
|
741
|
+
bad_ref: z2.string().optional(),
|
|
742
|
+
command: z2.string().optional(),
|
|
743
|
+
response_format: ResponseFormatSchema
|
|
744
|
+
},
|
|
745
|
+
annotations: {
|
|
746
|
+
readOnlyHint: false,
|
|
747
|
+
idempotentHint: false,
|
|
748
|
+
destructiveHint: true,
|
|
749
|
+
openWorldHint: false
|
|
750
|
+
}
|
|
751
|
+
},
|
|
752
|
+
async ({
|
|
753
|
+
repo_path,
|
|
754
|
+
action,
|
|
755
|
+
ref,
|
|
756
|
+
good_ref,
|
|
757
|
+
bad_ref,
|
|
758
|
+
command,
|
|
759
|
+
response_format
|
|
760
|
+
}) => {
|
|
761
|
+
try {
|
|
762
|
+
const repoPath = resolveRepoPath(repo_path);
|
|
763
|
+
const output = await runBisectAction(repoPath, {
|
|
764
|
+
action,
|
|
765
|
+
ref,
|
|
766
|
+
goodRef: good_ref,
|
|
767
|
+
badRef: bad_ref,
|
|
768
|
+
command
|
|
769
|
+
});
|
|
770
|
+
return {
|
|
771
|
+
content: [{ type: "text", text: render({ output }, response_format) }],
|
|
772
|
+
structuredContent: { output }
|
|
773
|
+
};
|
|
774
|
+
} catch (error) {
|
|
775
|
+
return buildError(error);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
);
|
|
779
|
+
server2.registerTool(
|
|
780
|
+
"git_tag",
|
|
781
|
+
{
|
|
782
|
+
title: "Git Tag Actions",
|
|
783
|
+
description: "List, create, or delete tags. Supports GPG/SSH signed tags.",
|
|
784
|
+
inputSchema: {
|
|
785
|
+
repo_path: RepoPathSchema,
|
|
786
|
+
action: z2.enum(["list", "create", "delete"]),
|
|
787
|
+
name: z2.string().optional(),
|
|
788
|
+
target: z2.string().optional(),
|
|
789
|
+
message: z2.string().optional(),
|
|
790
|
+
sign: z2.boolean().default(false).describe("Create a signed tag (-s/-u). Defaults to server AUTO_SIGN_TAGS setting."),
|
|
791
|
+
signing_key: z2.string().optional().describe("Specific signing key ID or path. Falls back to GIT_SIGNING_KEY env var."),
|
|
792
|
+
response_format: ResponseFormatSchema
|
|
793
|
+
},
|
|
794
|
+
annotations: {
|
|
795
|
+
readOnlyHint: false,
|
|
796
|
+
idempotentHint: false,
|
|
797
|
+
destructiveHint: true,
|
|
798
|
+
openWorldHint: false
|
|
799
|
+
}
|
|
800
|
+
},
|
|
801
|
+
async ({
|
|
802
|
+
repo_path,
|
|
803
|
+
action,
|
|
804
|
+
name,
|
|
805
|
+
target,
|
|
806
|
+
message,
|
|
807
|
+
sign,
|
|
808
|
+
signing_key,
|
|
809
|
+
response_format
|
|
810
|
+
}) => {
|
|
811
|
+
try {
|
|
812
|
+
const repoPath = resolveRepoPath(repo_path);
|
|
813
|
+
const output = await runTagAction(repoPath, {
|
|
814
|
+
action,
|
|
815
|
+
name,
|
|
816
|
+
target,
|
|
817
|
+
message,
|
|
818
|
+
sign,
|
|
819
|
+
signingKey: signing_key
|
|
820
|
+
});
|
|
821
|
+
return {
|
|
822
|
+
content: [{ type: "text", text: render({ output }, response_format) }],
|
|
823
|
+
structuredContent: { output }
|
|
824
|
+
};
|
|
825
|
+
} catch (error) {
|
|
826
|
+
return buildError(error);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
);
|
|
830
|
+
server2.registerTool(
|
|
831
|
+
"git_worktree",
|
|
832
|
+
{
|
|
833
|
+
title: "Git Worktree Actions",
|
|
834
|
+
description: "Add, list, or remove worktrees.",
|
|
835
|
+
inputSchema: {
|
|
836
|
+
repo_path: RepoPathSchema,
|
|
837
|
+
action: z2.enum(["add", "list", "remove"]),
|
|
838
|
+
path: z2.string().optional(),
|
|
839
|
+
branch: z2.string().optional(),
|
|
840
|
+
response_format: ResponseFormatSchema
|
|
841
|
+
},
|
|
842
|
+
annotations: {
|
|
843
|
+
readOnlyHint: false,
|
|
844
|
+
idempotentHint: false,
|
|
845
|
+
destructiveHint: true,
|
|
846
|
+
openWorldHint: false
|
|
847
|
+
}
|
|
848
|
+
},
|
|
849
|
+
async ({
|
|
850
|
+
repo_path,
|
|
851
|
+
action,
|
|
852
|
+
path: path5,
|
|
853
|
+
branch,
|
|
854
|
+
response_format
|
|
855
|
+
}) => {
|
|
856
|
+
try {
|
|
857
|
+
const repoPath = resolveRepoPath(repo_path);
|
|
858
|
+
const output = await runWorktreeAction(repoPath, { action, path: path5, branch });
|
|
859
|
+
return {
|
|
860
|
+
content: [{ type: "text", text: render({ output }, response_format) }],
|
|
861
|
+
structuredContent: { output }
|
|
862
|
+
};
|
|
863
|
+
} catch (error) {
|
|
864
|
+
return buildError(error);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
);
|
|
868
|
+
server2.registerTool(
|
|
869
|
+
"git_submodule",
|
|
870
|
+
{
|
|
871
|
+
title: "Git Submodule Actions",
|
|
872
|
+
description: "Add, list, update, or sync submodules.",
|
|
873
|
+
inputSchema: {
|
|
874
|
+
repo_path: RepoPathSchema,
|
|
875
|
+
action: z2.enum(["add", "list", "update", "sync"]),
|
|
876
|
+
url: z2.string().optional(),
|
|
877
|
+
path: z2.string().optional(),
|
|
878
|
+
recursive: z2.boolean().default(true),
|
|
879
|
+
response_format: ResponseFormatSchema
|
|
880
|
+
},
|
|
881
|
+
annotations: {
|
|
882
|
+
readOnlyHint: false,
|
|
883
|
+
idempotentHint: false,
|
|
884
|
+
destructiveHint: true,
|
|
885
|
+
openWorldHint: false
|
|
886
|
+
}
|
|
887
|
+
},
|
|
888
|
+
async ({
|
|
889
|
+
repo_path,
|
|
890
|
+
action,
|
|
891
|
+
url,
|
|
892
|
+
path: path5,
|
|
893
|
+
recursive,
|
|
894
|
+
response_format
|
|
895
|
+
}) => {
|
|
896
|
+
try {
|
|
897
|
+
const repoPath = resolveRepoPath(repo_path);
|
|
898
|
+
const output = await runSubmoduleAction(repoPath, {
|
|
899
|
+
action,
|
|
900
|
+
url,
|
|
901
|
+
path: path5,
|
|
902
|
+
recursive
|
|
903
|
+
});
|
|
904
|
+
return {
|
|
905
|
+
content: [{ type: "text", text: render({ output }, response_format) }],
|
|
906
|
+
structuredContent: { output }
|
|
907
|
+
};
|
|
908
|
+
} catch (error) {
|
|
909
|
+
return buildError(error);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// src/tools/branch.tools.ts
|
|
916
|
+
import { z as z3 } from "zod";
|
|
917
|
+
function render2(content, format) {
|
|
918
|
+
if (typeof content === "string" && format === "markdown") {
|
|
919
|
+
return content;
|
|
920
|
+
}
|
|
921
|
+
return JSON.stringify(content, null, 2);
|
|
922
|
+
}
|
|
923
|
+
function registerBranchTools(server2) {
|
|
924
|
+
server2.registerTool(
|
|
925
|
+
"git_list_branches",
|
|
926
|
+
{
|
|
927
|
+
title: "List Git Branches",
|
|
928
|
+
description: "List local branches, or all branches including remotes.",
|
|
929
|
+
inputSchema: {
|
|
930
|
+
repo_path: RepoPathSchema,
|
|
931
|
+
all: z3.boolean().default(false),
|
|
932
|
+
response_format: ResponseFormatSchema
|
|
933
|
+
},
|
|
934
|
+
annotations: {
|
|
935
|
+
readOnlyHint: true,
|
|
936
|
+
idempotentHint: true,
|
|
937
|
+
destructiveHint: false,
|
|
938
|
+
openWorldHint: false
|
|
939
|
+
}
|
|
940
|
+
},
|
|
941
|
+
async ({
|
|
942
|
+
repo_path,
|
|
943
|
+
all,
|
|
944
|
+
response_format
|
|
945
|
+
}) => {
|
|
946
|
+
try {
|
|
947
|
+
const repoPath = resolveRepoPath(repo_path);
|
|
948
|
+
const branches = await listBranches(repoPath, all);
|
|
949
|
+
return {
|
|
950
|
+
content: [{ type: "text", text: render2({ branches }, response_format) }],
|
|
951
|
+
structuredContent: { branches }
|
|
952
|
+
};
|
|
953
|
+
} catch (error) {
|
|
954
|
+
const gitError = toGitError(error);
|
|
955
|
+
return { content: [{ type: "text", text: `Error (${gitError.kind}): ${gitError.message}` }] };
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
);
|
|
959
|
+
server2.registerTool(
|
|
960
|
+
"git_create_branch",
|
|
961
|
+
{
|
|
962
|
+
title: "Create Git Branch",
|
|
963
|
+
description: "Create a branch from current HEAD or an explicit ref.",
|
|
964
|
+
inputSchema: {
|
|
965
|
+
repo_path: RepoPathSchema,
|
|
966
|
+
name: z3.string().min(1),
|
|
967
|
+
from_ref: z3.string().optional(),
|
|
968
|
+
checkout: z3.boolean().default(false),
|
|
969
|
+
response_format: ResponseFormatSchema
|
|
970
|
+
},
|
|
971
|
+
annotations: {
|
|
972
|
+
readOnlyHint: false,
|
|
973
|
+
idempotentHint: false,
|
|
974
|
+
destructiveHint: false,
|
|
975
|
+
openWorldHint: false
|
|
976
|
+
}
|
|
977
|
+
},
|
|
978
|
+
async ({
|
|
979
|
+
repo_path,
|
|
980
|
+
name,
|
|
981
|
+
from_ref,
|
|
982
|
+
checkout,
|
|
983
|
+
response_format
|
|
984
|
+
}) => {
|
|
985
|
+
try {
|
|
986
|
+
const repoPath = resolveRepoPath(repo_path);
|
|
987
|
+
const message = await createBranch(repoPath, {
|
|
988
|
+
name,
|
|
989
|
+
fromRef: from_ref,
|
|
990
|
+
checkout
|
|
991
|
+
});
|
|
992
|
+
return {
|
|
993
|
+
content: [{ type: "text", text: render2({ message }, response_format) }],
|
|
994
|
+
structuredContent: { message }
|
|
995
|
+
};
|
|
996
|
+
} catch (error) {
|
|
997
|
+
const gitError = toGitError(error);
|
|
998
|
+
return { content: [{ type: "text", text: `Error (${gitError.kind}): ${gitError.message}` }] };
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
);
|
|
1002
|
+
server2.registerTool(
|
|
1003
|
+
"git_delete_branch",
|
|
1004
|
+
{
|
|
1005
|
+
title: "Delete Git Branch",
|
|
1006
|
+
description: "Delete a local branch.",
|
|
1007
|
+
inputSchema: {
|
|
1008
|
+
repo_path: RepoPathSchema,
|
|
1009
|
+
name: z3.string().min(1),
|
|
1010
|
+
force: z3.boolean().default(false),
|
|
1011
|
+
response_format: ResponseFormatSchema
|
|
1012
|
+
},
|
|
1013
|
+
annotations: {
|
|
1014
|
+
readOnlyHint: false,
|
|
1015
|
+
idempotentHint: false,
|
|
1016
|
+
destructiveHint: true,
|
|
1017
|
+
openWorldHint: false
|
|
1018
|
+
}
|
|
1019
|
+
},
|
|
1020
|
+
async ({
|
|
1021
|
+
repo_path,
|
|
1022
|
+
name,
|
|
1023
|
+
force,
|
|
1024
|
+
response_format
|
|
1025
|
+
}) => {
|
|
1026
|
+
try {
|
|
1027
|
+
const repoPath = resolveRepoPath(repo_path);
|
|
1028
|
+
const message = await deleteBranch(repoPath, { name, force });
|
|
1029
|
+
return {
|
|
1030
|
+
content: [{ type: "text", text: render2({ message }, response_format) }],
|
|
1031
|
+
structuredContent: { message }
|
|
1032
|
+
};
|
|
1033
|
+
} catch (error) {
|
|
1034
|
+
const gitError = toGitError(error);
|
|
1035
|
+
return { content: [{ type: "text", text: `Error (${gitError.kind}): ${gitError.message}` }] };
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
);
|
|
1039
|
+
server2.registerTool(
|
|
1040
|
+
"git_rename_branch",
|
|
1041
|
+
{
|
|
1042
|
+
title: "Rename Git Branch",
|
|
1043
|
+
description: "Rename an existing branch.",
|
|
1044
|
+
inputSchema: {
|
|
1045
|
+
repo_path: RepoPathSchema,
|
|
1046
|
+
old_name: z3.string().min(1),
|
|
1047
|
+
new_name: z3.string().min(1),
|
|
1048
|
+
response_format: ResponseFormatSchema
|
|
1049
|
+
},
|
|
1050
|
+
annotations: {
|
|
1051
|
+
readOnlyHint: false,
|
|
1052
|
+
idempotentHint: false,
|
|
1053
|
+
destructiveHint: false,
|
|
1054
|
+
openWorldHint: false
|
|
1055
|
+
}
|
|
1056
|
+
},
|
|
1057
|
+
async ({
|
|
1058
|
+
repo_path,
|
|
1059
|
+
old_name,
|
|
1060
|
+
new_name,
|
|
1061
|
+
response_format
|
|
1062
|
+
}) => {
|
|
1063
|
+
try {
|
|
1064
|
+
const repoPath = resolveRepoPath(repo_path);
|
|
1065
|
+
const message = await renameBranch(repoPath, old_name, new_name);
|
|
1066
|
+
return {
|
|
1067
|
+
content: [{ type: "text", text: render2({ message }, response_format) }],
|
|
1068
|
+
structuredContent: { message }
|
|
1069
|
+
};
|
|
1070
|
+
} catch (error) {
|
|
1071
|
+
const gitError = toGitError(error);
|
|
1072
|
+
return { content: [{ type: "text", text: `Error (${gitError.kind}): ${gitError.message}` }] };
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
);
|
|
1076
|
+
server2.registerTool(
|
|
1077
|
+
"git_checkout",
|
|
1078
|
+
{
|
|
1079
|
+
title: "Git Checkout",
|
|
1080
|
+
description: "Checkout a branch, tag, or commit; optionally create a new local branch.",
|
|
1081
|
+
inputSchema: {
|
|
1082
|
+
repo_path: RepoPathSchema,
|
|
1083
|
+
ref: z3.string().min(1),
|
|
1084
|
+
create: z3.boolean().default(false),
|
|
1085
|
+
response_format: ResponseFormatSchema
|
|
1086
|
+
},
|
|
1087
|
+
annotations: {
|
|
1088
|
+
readOnlyHint: false,
|
|
1089
|
+
idempotentHint: false,
|
|
1090
|
+
destructiveHint: false,
|
|
1091
|
+
openWorldHint: false
|
|
1092
|
+
}
|
|
1093
|
+
},
|
|
1094
|
+
async ({
|
|
1095
|
+
repo_path,
|
|
1096
|
+
ref,
|
|
1097
|
+
create,
|
|
1098
|
+
response_format
|
|
1099
|
+
}) => {
|
|
1100
|
+
try {
|
|
1101
|
+
const repoPath = resolveRepoPath(repo_path);
|
|
1102
|
+
const message = await checkoutRef(repoPath, ref, create);
|
|
1103
|
+
return {
|
|
1104
|
+
content: [{ type: "text", text: render2({ message }, response_format) }],
|
|
1105
|
+
structuredContent: { message }
|
|
1106
|
+
};
|
|
1107
|
+
} catch (error) {
|
|
1108
|
+
const gitError = toGitError(error);
|
|
1109
|
+
return { content: [{ type: "text", text: `Error (${gitError.kind}): ${gitError.message}` }] };
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
);
|
|
1113
|
+
server2.registerTool(
|
|
1114
|
+
"git_set_upstream",
|
|
1115
|
+
{
|
|
1116
|
+
title: "Set Branch Upstream",
|
|
1117
|
+
description: "Set the upstream tracking branch for a local branch.",
|
|
1118
|
+
inputSchema: {
|
|
1119
|
+
repo_path: RepoPathSchema,
|
|
1120
|
+
branch: z3.string().min(1),
|
|
1121
|
+
upstream: z3.string().min(1),
|
|
1122
|
+
response_format: ResponseFormatSchema
|
|
1123
|
+
},
|
|
1124
|
+
annotations: {
|
|
1125
|
+
readOnlyHint: false,
|
|
1126
|
+
idempotentHint: false,
|
|
1127
|
+
destructiveHint: false,
|
|
1128
|
+
openWorldHint: false
|
|
1129
|
+
}
|
|
1130
|
+
},
|
|
1131
|
+
async ({
|
|
1132
|
+
repo_path,
|
|
1133
|
+
branch,
|
|
1134
|
+
upstream,
|
|
1135
|
+
response_format
|
|
1136
|
+
}) => {
|
|
1137
|
+
try {
|
|
1138
|
+
const repoPath = resolveRepoPath(repo_path);
|
|
1139
|
+
const message = await setUpstream(repoPath, branch, upstream);
|
|
1140
|
+
return {
|
|
1141
|
+
content: [{ type: "text", text: render2({ message }, response_format) }],
|
|
1142
|
+
structuredContent: { message }
|
|
1143
|
+
};
|
|
1144
|
+
} catch (error) {
|
|
1145
|
+
const gitError = toGitError(error);
|
|
1146
|
+
return { content: [{ type: "text", text: `Error (${gitError.kind}): ${gitError.message}` }] };
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
);
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
// src/tools/context.tools.ts
|
|
1153
|
+
import { z as z4 } from "zod";
|
|
1154
|
+
|
|
1155
|
+
// src/services/context.service.ts
|
|
1156
|
+
import { existsSync as existsSync2 } from "fs";
|
|
1157
|
+
import path4 from "path";
|
|
1158
|
+
async function getContextSummary(repoPath) {
|
|
1159
|
+
const git = getGit(repoPath);
|
|
1160
|
+
const [status, commits, remotesRaw, gitDir] = await Promise.all([
|
|
1161
|
+
getStatus(repoPath),
|
|
1162
|
+
getLog(repoPath, { limit: 5, offset: 0 }),
|
|
1163
|
+
git.getRemotes(false),
|
|
1164
|
+
git.raw(["rev-parse", "--absolute-git-dir"]).then((s) => s.trim())
|
|
1165
|
+
]);
|
|
1166
|
+
const rebasing = existsSync2(path4.join(gitDir, "rebase-merge")) || existsSync2(path4.join(gitDir, "rebase-apply"));
|
|
1167
|
+
const merging = existsSync2(path4.join(gitDir, "MERGE_HEAD"));
|
|
1168
|
+
const cherryPicking = existsSync2(path4.join(gitDir, "CHERRY_PICK_HEAD"));
|
|
1169
|
+
const bisecting = existsSync2(path4.join(gitDir, "BISECT_LOG"));
|
|
1170
|
+
return {
|
|
1171
|
+
branch: status.current,
|
|
1172
|
+
ahead: status.ahead,
|
|
1173
|
+
behind: status.behind,
|
|
1174
|
+
isClean: status.isClean,
|
|
1175
|
+
changedFiles: status.files.length,
|
|
1176
|
+
recentCommits: commits.map((commit) => ({
|
|
1177
|
+
hash: commit.hash,
|
|
1178
|
+
subject: commit.subject,
|
|
1179
|
+
dateIso: commit.dateIso
|
|
1180
|
+
})),
|
|
1181
|
+
remotes: remotesRaw.map((remote) => remote.name),
|
|
1182
|
+
inProgress: {
|
|
1183
|
+
rebasing,
|
|
1184
|
+
merging,
|
|
1185
|
+
cherryPicking,
|
|
1186
|
+
bisecting
|
|
1187
|
+
}
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
async function searchHistory(repoPath, query, limit) {
|
|
1191
|
+
const git = getGit(repoPath);
|
|
1192
|
+
const [pickaxe, grep] = await Promise.all([
|
|
1193
|
+
git.raw(["log", "-S", query, "--oneline", "-n", String(limit)]),
|
|
1194
|
+
git.raw(["grep", "-n", "-m", String(limit), "--", query]).catch(() => "")
|
|
1195
|
+
]);
|
|
1196
|
+
const sections = [
|
|
1197
|
+
"## Pickaxe (-S)",
|
|
1198
|
+
pickaxe.trim() || "No history matches.",
|
|
1199
|
+
"",
|
|
1200
|
+
"## grep",
|
|
1201
|
+
grep.trim() || "No working-tree matches."
|
|
1202
|
+
];
|
|
1203
|
+
const combined = sections.join("\n");
|
|
1204
|
+
return combined.length > CHARACTER_LIMIT ? `${combined.slice(0, CHARACTER_LIMIT)}
|
|
1205
|
+
|
|
1206
|
+
[Output truncated at ${CHARACTER_LIMIT} characters]` : combined;
|
|
1207
|
+
}
|
|
1208
|
+
var BLOCKED_CONFIG_KEY_PATTERNS = [/^credential\./i, /^url\./i];
|
|
1209
|
+
var SENSITIVE_KEY_PATTERNS = [/(password|token|secret|auth|passphrase)/i];
|
|
1210
|
+
function isBlockedConfigKey(key) {
|
|
1211
|
+
return BLOCKED_CONFIG_KEY_PATTERNS.some((p) => p.test(key));
|
|
1212
|
+
}
|
|
1213
|
+
function redactConfigValue(key, value) {
|
|
1214
|
+
if (SENSITIVE_KEY_PATTERNS.some((p) => p.test(key))) {
|
|
1215
|
+
return "***";
|
|
1216
|
+
}
|
|
1217
|
+
const stripped = value.replace(/(https?:\/\/)[^@\s]+@/g, "$1***@");
|
|
1218
|
+
if (/\b[0-9a-f]{40,}\b/i.test(stripped)) {
|
|
1219
|
+
return "***";
|
|
1220
|
+
}
|
|
1221
|
+
return stripped;
|
|
1222
|
+
}
|
|
1223
|
+
async function getConfig(repoPath, key) {
|
|
1224
|
+
const git = getGit(repoPath);
|
|
1225
|
+
if (key) {
|
|
1226
|
+
if (isBlockedConfigKey(key)) {
|
|
1227
|
+
throw new Error(`Access to git config key '${key}' is not permitted.`);
|
|
1228
|
+
}
|
|
1229
|
+
const value = await git.raw(["config", "--get", key]);
|
|
1230
|
+
return redactConfigValue(key, value.trim());
|
|
1231
|
+
}
|
|
1232
|
+
const output = await git.raw(["config", "--list"]);
|
|
1233
|
+
const lines = output.split("\n").map((line) => line.trim()).filter(Boolean).flatMap((line) => {
|
|
1234
|
+
const eq = line.indexOf("=");
|
|
1235
|
+
if (eq === -1) return [];
|
|
1236
|
+
const k = line.slice(0, eq);
|
|
1237
|
+
const v = line.slice(eq + 1);
|
|
1238
|
+
if (isBlockedConfigKey(k)) return [];
|
|
1239
|
+
return [`${k}=${redactConfigValue(k, v)}`];
|
|
1240
|
+
});
|
|
1241
|
+
return lines.join("\n").trim();
|
|
1242
|
+
}
|
|
1243
|
+
async function setConfig(repoPath, key, value) {
|
|
1244
|
+
const git = getGit(repoPath);
|
|
1245
|
+
await git.raw(["config", key, value]);
|
|
1246
|
+
return `Set ${key}.`;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
// src/tools/context.tools.ts
|
|
1250
|
+
function render3(content, format) {
|
|
1251
|
+
if (typeof content === "string" && format === "markdown") {
|
|
1252
|
+
return content;
|
|
1253
|
+
}
|
|
1254
|
+
return JSON.stringify(content, null, 2);
|
|
1255
|
+
}
|
|
1256
|
+
function buildError2(error) {
|
|
1257
|
+
const gitError = toGitError(error);
|
|
1258
|
+
return { content: [{ type: "text", text: `Error (${gitError.kind}): ${gitError.message}` }] };
|
|
1259
|
+
}
|
|
1260
|
+
function registerContextTools(server2) {
|
|
1261
|
+
server2.registerTool(
|
|
1262
|
+
"git_context_summary",
|
|
1263
|
+
{
|
|
1264
|
+
title: "Git Context Summary",
|
|
1265
|
+
description: "High-signal repository context for LLM-assisted workflows.",
|
|
1266
|
+
inputSchema: {
|
|
1267
|
+
repo_path: RepoPathSchema,
|
|
1268
|
+
response_format: ResponseFormatSchema
|
|
1269
|
+
},
|
|
1270
|
+
annotations: {
|
|
1271
|
+
readOnlyHint: true,
|
|
1272
|
+
idempotentHint: true,
|
|
1273
|
+
destructiveHint: false,
|
|
1274
|
+
openWorldHint: false
|
|
1275
|
+
}
|
|
1276
|
+
},
|
|
1277
|
+
async ({ repo_path, response_format }) => {
|
|
1278
|
+
try {
|
|
1279
|
+
const repoPath = resolveRepoPath(repo_path);
|
|
1280
|
+
const summary = await getContextSummary(repoPath);
|
|
1281
|
+
return {
|
|
1282
|
+
content: [{ type: "text", text: render3(summary, response_format) }],
|
|
1283
|
+
structuredContent: { summary }
|
|
1284
|
+
};
|
|
1285
|
+
} catch (error) {
|
|
1286
|
+
return buildError2(error);
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
);
|
|
1290
|
+
server2.registerTool(
|
|
1291
|
+
"git_search",
|
|
1292
|
+
{
|
|
1293
|
+
title: "Git Search",
|
|
1294
|
+
description: "Search code and history using pickaxe and grep.",
|
|
1295
|
+
inputSchema: {
|
|
1296
|
+
repo_path: RepoPathSchema,
|
|
1297
|
+
query: z4.string().min(1),
|
|
1298
|
+
limit: z4.number().int().min(1).max(200).default(20),
|
|
1299
|
+
response_format: ResponseFormatSchema
|
|
1300
|
+
},
|
|
1301
|
+
annotations: {
|
|
1302
|
+
readOnlyHint: true,
|
|
1303
|
+
idempotentHint: true,
|
|
1304
|
+
destructiveHint: false,
|
|
1305
|
+
openWorldHint: false
|
|
1306
|
+
}
|
|
1307
|
+
},
|
|
1308
|
+
async ({
|
|
1309
|
+
repo_path,
|
|
1310
|
+
query,
|
|
1311
|
+
limit,
|
|
1312
|
+
response_format
|
|
1313
|
+
}) => {
|
|
1314
|
+
try {
|
|
1315
|
+
const repoPath = resolveRepoPath(repo_path);
|
|
1316
|
+
const output = await searchHistory(repoPath, query, limit);
|
|
1317
|
+
return {
|
|
1318
|
+
content: [{ type: "text", text: render3({ output }, response_format) }],
|
|
1319
|
+
structuredContent: { output }
|
|
1320
|
+
};
|
|
1321
|
+
} catch (error) {
|
|
1322
|
+
return buildError2(error);
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
);
|
|
1326
|
+
server2.registerTool(
|
|
1327
|
+
"git_get_config",
|
|
1328
|
+
{
|
|
1329
|
+
title: "Get Git Config",
|
|
1330
|
+
description: "Read repository/local git configuration values.",
|
|
1331
|
+
inputSchema: {
|
|
1332
|
+
repo_path: RepoPathSchema,
|
|
1333
|
+
key: z4.string().optional(),
|
|
1334
|
+
response_format: ResponseFormatSchema
|
|
1335
|
+
},
|
|
1336
|
+
annotations: {
|
|
1337
|
+
readOnlyHint: true,
|
|
1338
|
+
idempotentHint: true,
|
|
1339
|
+
destructiveHint: false,
|
|
1340
|
+
openWorldHint: false
|
|
1341
|
+
}
|
|
1342
|
+
},
|
|
1343
|
+
async ({
|
|
1344
|
+
repo_path,
|
|
1345
|
+
key,
|
|
1346
|
+
response_format
|
|
1347
|
+
}) => {
|
|
1348
|
+
try {
|
|
1349
|
+
const repoPath = resolveRepoPath(repo_path);
|
|
1350
|
+
const value = await getConfig(repoPath, key);
|
|
1351
|
+
return {
|
|
1352
|
+
content: [{ type: "text", text: render3({ value }, response_format) }],
|
|
1353
|
+
structuredContent: { value }
|
|
1354
|
+
};
|
|
1355
|
+
} catch (error) {
|
|
1356
|
+
return buildError2(error);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
);
|
|
1360
|
+
server2.registerTool(
|
|
1361
|
+
"git_set_config",
|
|
1362
|
+
{
|
|
1363
|
+
title: "Set Git Config",
|
|
1364
|
+
description: "Set repository-local git configuration value.",
|
|
1365
|
+
inputSchema: {
|
|
1366
|
+
repo_path: RepoPathSchema,
|
|
1367
|
+
key: z4.string().min(1),
|
|
1368
|
+
value: z4.string(),
|
|
1369
|
+
response_format: ResponseFormatSchema
|
|
1370
|
+
},
|
|
1371
|
+
annotations: {
|
|
1372
|
+
readOnlyHint: false,
|
|
1373
|
+
idempotentHint: false,
|
|
1374
|
+
destructiveHint: true,
|
|
1375
|
+
openWorldHint: false
|
|
1376
|
+
}
|
|
1377
|
+
},
|
|
1378
|
+
async ({
|
|
1379
|
+
repo_path,
|
|
1380
|
+
key,
|
|
1381
|
+
value,
|
|
1382
|
+
response_format
|
|
1383
|
+
}) => {
|
|
1384
|
+
try {
|
|
1385
|
+
const repoPath = resolveRepoPath(repo_path);
|
|
1386
|
+
const message = await setConfig(repoPath, key, value);
|
|
1387
|
+
return {
|
|
1388
|
+
content: [{ type: "text", text: render3({ message }, response_format) }],
|
|
1389
|
+
structuredContent: { message }
|
|
1390
|
+
};
|
|
1391
|
+
} catch (error) {
|
|
1392
|
+
return buildError2(error);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
);
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
// src/tools/docs.tools.ts
|
|
1399
|
+
import { z as z5 } from "zod";
|
|
1400
|
+
|
|
1401
|
+
// src/services/docs.service.ts
|
|
1402
|
+
function stripHtml(html) {
|
|
1403
|
+
let text = html.replace(/<(script|style)[^>]*>[\s\S]*?<\/\1>/gi, " ");
|
|
1404
|
+
text = text.replace(/<[^>]+>/g, " ");
|
|
1405
|
+
text = text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/ /g, " ").replace(/&#(\d+);/g, (_, code) => String.fromCodePoint(Number(code)));
|
|
1406
|
+
return text.replace(/\s+/g, " ").trim();
|
|
1407
|
+
}
|
|
1408
|
+
function extractBetween(html, startMarker, endMarker) {
|
|
1409
|
+
const startMatch = startMarker.exec(html);
|
|
1410
|
+
if (!startMatch) return html;
|
|
1411
|
+
const startIdx = startMatch.index + startMatch[0].length;
|
|
1412
|
+
const remainder = html.slice(startIdx);
|
|
1413
|
+
const endMatch = endMarker.exec(remainder);
|
|
1414
|
+
return endMatch ? remainder.slice(0, endMatch.index) : remainder;
|
|
1415
|
+
}
|
|
1416
|
+
async function searchGitDocs(query) {
|
|
1417
|
+
const url = `https://git-scm.com/search/results?search=${encodeURIComponent(query)}&language=en`;
|
|
1418
|
+
let html;
|
|
1419
|
+
try {
|
|
1420
|
+
const response = await fetch(url, {
|
|
1421
|
+
headers: { Accept: "text/html", "User-Agent": "git-mcp-docs/1.0" },
|
|
1422
|
+
signal: AbortSignal.timeout(1e4)
|
|
1423
|
+
});
|
|
1424
|
+
if (!response.ok) {
|
|
1425
|
+
throw new Error(`git-scm.com returned HTTP ${response.status}`);
|
|
1426
|
+
}
|
|
1427
|
+
html = await response.text();
|
|
1428
|
+
} catch (err) {
|
|
1429
|
+
throw new Error(`Failed to fetch git docs search: ${err instanceof Error ? err.message : String(err)}`);
|
|
1430
|
+
}
|
|
1431
|
+
const results = [];
|
|
1432
|
+
const listMatch = /<ul[^>]*class="[^"]*result-list[^"]*"[^>]*>([\s\S]*?)<\/ul>/i.exec(html);
|
|
1433
|
+
const listHtml = listMatch ? listMatch[1] : html;
|
|
1434
|
+
const itemRegex = /<li[^>]*>([\s\S]*?)<\/li>/gi;
|
|
1435
|
+
let itemMatch;
|
|
1436
|
+
while ((itemMatch = itemRegex.exec(listHtml)) !== null) {
|
|
1437
|
+
const item = itemMatch[1];
|
|
1438
|
+
const linkMatch = /<a[^>]+href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/i.exec(item);
|
|
1439
|
+
if (!linkMatch) continue;
|
|
1440
|
+
const href = linkMatch[1].trim();
|
|
1441
|
+
const title = stripHtml(linkMatch[2]).trim();
|
|
1442
|
+
if (!title) continue;
|
|
1443
|
+
const excerptMatch = /<(?:p|span)[^>]*class="[^"]*excerpt[^"]*"[^>]*>([\s\S]*?)<\/(?:p|span)>/i.exec(item);
|
|
1444
|
+
const excerpt = excerptMatch ? stripHtml(excerptMatch[1]).trim() : "";
|
|
1445
|
+
const fullUrl = href.startsWith("http") ? href : `https://git-scm.com${href}`;
|
|
1446
|
+
results.push({ title, url: fullUrl, excerpt });
|
|
1447
|
+
}
|
|
1448
|
+
if (results.length === 0) {
|
|
1449
|
+
const linkRegex = /<a[^>]+href="(\/docs\/[^"]+)"[^>]*>([\s\S]*?)<\/a>/gi;
|
|
1450
|
+
let m;
|
|
1451
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1452
|
+
while ((m = linkRegex.exec(html)) !== null && results.length < 20) {
|
|
1453
|
+
const href = m[1];
|
|
1454
|
+
const title = stripHtml(m[2]).trim();
|
|
1455
|
+
if (!title || seen.has(href)) continue;
|
|
1456
|
+
seen.add(href);
|
|
1457
|
+
results.push({ title, url: `https://git-scm.com${href}`, excerpt: "" });
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
return { query, results };
|
|
1461
|
+
}
|
|
1462
|
+
async function fetchGitManPage(command) {
|
|
1463
|
+
const normalized = command.trim().toLowerCase().replace(/^git[- ]/, "");
|
|
1464
|
+
const url = `https://git-scm.com/docs/git-${normalized}`;
|
|
1465
|
+
let html;
|
|
1466
|
+
try {
|
|
1467
|
+
const response = await fetch(url, {
|
|
1468
|
+
headers: { Accept: "text/html", "User-Agent": "git-mcp-docs/1.0" },
|
|
1469
|
+
signal: AbortSignal.timeout(1e4)
|
|
1470
|
+
});
|
|
1471
|
+
if (!response.ok) {
|
|
1472
|
+
if (response.status === 404) {
|
|
1473
|
+
throw new Error(
|
|
1474
|
+
`No man page found for "git ${normalized}". Check the command name or search with action="search".`
|
|
1475
|
+
);
|
|
1476
|
+
}
|
|
1477
|
+
throw new Error(`git-scm.com returned HTTP ${response.status} for git-${normalized}`);
|
|
1478
|
+
}
|
|
1479
|
+
html = await response.text();
|
|
1480
|
+
} catch (err) {
|
|
1481
|
+
if (err instanceof Error && err.message.startsWith("No man page")) throw err;
|
|
1482
|
+
throw new Error(`Failed to fetch git man page: ${err instanceof Error ? err.message : String(err)}`);
|
|
1483
|
+
}
|
|
1484
|
+
const articleContent = extractBetween(
|
|
1485
|
+
html,
|
|
1486
|
+
/<(?:article|div)[^>]*(?:id="main"|class="[^"]*(?:sect|man-page|article)[^"]*")[^>]*>/i,
|
|
1487
|
+
/<\/(?:article|div)>/i
|
|
1488
|
+
);
|
|
1489
|
+
const content = stripHtml(articleContent || html);
|
|
1490
|
+
const header = `# git-${normalized}(1)
|
|
1491
|
+
|
|
1492
|
+
Source: ${url}
|
|
1493
|
+
|
|
1494
|
+
`;
|
|
1495
|
+
const full = header + content;
|
|
1496
|
+
return full.length > CHARACTER_LIMIT ? full.slice(0, CHARACTER_LIMIT) + "\n\n[...truncated \u2014 content exceeded limit]" : full;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
// src/tools/docs.tools.ts
|
|
1500
|
+
function render4(content, format) {
|
|
1501
|
+
if (typeof content === "string" && format === "markdown") {
|
|
1502
|
+
return content;
|
|
1503
|
+
}
|
|
1504
|
+
return JSON.stringify(content, null, 2);
|
|
1505
|
+
}
|
|
1506
|
+
function registerDocsTools(server2) {
|
|
1507
|
+
server2.registerTool(
|
|
1508
|
+
"git_docs",
|
|
1509
|
+
{
|
|
1510
|
+
title: "Git Documentation",
|
|
1511
|
+
description: 'Search and browse official Git documentation from git-scm.com. Use action="search" to find relevant commands and concepts by keyword. Use action="man" to fetch the full man page for a specific git command (e.g. query="commit" fetches the git-commit man page). Useful for answering questions about how to use git commands, understanding options, and discovering the right git command for a task.',
|
|
1512
|
+
inputSchema: {
|
|
1513
|
+
action: z5.enum(["search", "man"]),
|
|
1514
|
+
query: z5.string().min(1).describe(
|
|
1515
|
+
'For action="search": search terms (e.g. "undo last commit"). For action="man": git command name without "git-" prefix (e.g. "commit", "rebase", "merge").'
|
|
1516
|
+
),
|
|
1517
|
+
response_format: ResponseFormatSchema
|
|
1518
|
+
},
|
|
1519
|
+
annotations: {
|
|
1520
|
+
readOnlyHint: true,
|
|
1521
|
+
idempotentHint: true,
|
|
1522
|
+
destructiveHint: false,
|
|
1523
|
+
openWorldHint: true
|
|
1524
|
+
}
|
|
1525
|
+
},
|
|
1526
|
+
async ({
|
|
1527
|
+
action,
|
|
1528
|
+
query,
|
|
1529
|
+
response_format
|
|
1530
|
+
}) => {
|
|
1531
|
+
try {
|
|
1532
|
+
if (action === "search") {
|
|
1533
|
+
const results = await searchGitDocs(query);
|
|
1534
|
+
const structuredContent = {
|
|
1535
|
+
query: results.query,
|
|
1536
|
+
results: results.results
|
|
1537
|
+
};
|
|
1538
|
+
if (response_format === "json") {
|
|
1539
|
+
return {
|
|
1540
|
+
content: [{ type: "text", text: JSON.stringify(structuredContent, null, 2) }],
|
|
1541
|
+
structuredContent
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1544
|
+
if (results.results.length === 0) {
|
|
1545
|
+
const text3 = `No results found for "${query}" on git-scm.com.`;
|
|
1546
|
+
return { content: [{ type: "text", text: text3 }], structuredContent };
|
|
1547
|
+
}
|
|
1548
|
+
const lines = [`## Git Docs Search: "${results.query}"`, ""];
|
|
1549
|
+
for (const r of results.results) {
|
|
1550
|
+
lines.push(`### [${r.title}](${r.url})`);
|
|
1551
|
+
if (r.excerpt) lines.push(r.excerpt);
|
|
1552
|
+
lines.push("");
|
|
1553
|
+
}
|
|
1554
|
+
const text2 = lines.join("\n");
|
|
1555
|
+
return { content: [{ type: "text", text: text2 }], structuredContent };
|
|
1556
|
+
}
|
|
1557
|
+
const text = await fetchGitManPage(query);
|
|
1558
|
+
return {
|
|
1559
|
+
content: [{ type: "text", text: render4(text, response_format) }],
|
|
1560
|
+
structuredContent: { command: query, content: text }
|
|
1561
|
+
};
|
|
1562
|
+
} catch (error) {
|
|
1563
|
+
const gitError = toGitError(error);
|
|
1564
|
+
return { content: [{ type: "text", text: `Error (${gitError.kind}): ${gitError.message}` }] };
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
);
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
// src/tools/flow.tools.ts
|
|
1571
|
+
import { z as z6 } from "zod";
|
|
1572
|
+
|
|
1573
|
+
// src/services/flow.service.ts
|
|
1574
|
+
async function getFlowConfig(git, overrides) {
|
|
1575
|
+
const get = async (key, fallback) => {
|
|
1576
|
+
try {
|
|
1577
|
+
const value = await git.raw(["config", "--local", key]);
|
|
1578
|
+
return value.trim() || fallback;
|
|
1579
|
+
} catch {
|
|
1580
|
+
return fallback;
|
|
1581
|
+
}
|
|
1582
|
+
};
|
|
1583
|
+
const mainBranch = overrides.mainBranch ?? await get("gitflow.branch.master", "main");
|
|
1584
|
+
const developBranch = overrides.developBranch ?? await get("gitflow.branch.develop", "develop");
|
|
1585
|
+
return {
|
|
1586
|
+
mainBranch,
|
|
1587
|
+
developBranch,
|
|
1588
|
+
featurePrefix: await get("gitflow.prefix.feature", "feature/"),
|
|
1589
|
+
releasePrefix: await get("gitflow.prefix.release", "release/"),
|
|
1590
|
+
hotfixPrefix: await get("gitflow.prefix.hotfix", "hotfix/"),
|
|
1591
|
+
supportPrefix: await get("gitflow.prefix.support", "support/"),
|
|
1592
|
+
versionTagPrefix: await get("gitflow.prefix.versiontag", "")
|
|
1593
|
+
};
|
|
1594
|
+
}
|
|
1595
|
+
async function listBranchesByPrefix(git, prefix) {
|
|
1596
|
+
const result = await git.branch(["-a"]);
|
|
1597
|
+
const matching = result.all.map((b) => b.replace(/^remotes\/[^/]+\//, "").trim()).filter((b, idx, arr) => b.startsWith(prefix) && arr.indexOf(b) === idx).map((b) => b.slice(prefix.length));
|
|
1598
|
+
return matching.length > 0 ? matching.join("\n") : `No ${prefix.slice(0, -1)} branches found.`;
|
|
1599
|
+
}
|
|
1600
|
+
async function branchExists(git, branch) {
|
|
1601
|
+
try {
|
|
1602
|
+
const result = await git.branch(["-a"]);
|
|
1603
|
+
return result.all.some((b) => b === branch || b === `remotes/origin/${branch}` || b.endsWith(`/${branch}`));
|
|
1604
|
+
} catch {
|
|
1605
|
+
return false;
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
async function runFlowAction(repoPath, options) {
|
|
1609
|
+
const git = getGit(repoPath);
|
|
1610
|
+
const cfg = await getFlowConfig(git, options);
|
|
1611
|
+
const deleteBranch2 = options.deleteBranch ?? true;
|
|
1612
|
+
const tagOnFinish = options.tag ?? true;
|
|
1613
|
+
switch (options.action) {
|
|
1614
|
+
// -----------------------------------------------------------------------
|
|
1615
|
+
// init
|
|
1616
|
+
// -----------------------------------------------------------------------
|
|
1617
|
+
case "init": {
|
|
1618
|
+
const sets = [
|
|
1619
|
+
["gitflow.branch.master", cfg.mainBranch],
|
|
1620
|
+
["gitflow.branch.develop", cfg.developBranch],
|
|
1621
|
+
["gitflow.prefix.feature", cfg.featurePrefix],
|
|
1622
|
+
["gitflow.prefix.release", cfg.releasePrefix],
|
|
1623
|
+
["gitflow.prefix.hotfix", cfg.hotfixPrefix],
|
|
1624
|
+
["gitflow.prefix.support", cfg.supportPrefix],
|
|
1625
|
+
["gitflow.prefix.versiontag", cfg.versionTagPrefix]
|
|
1626
|
+
];
|
|
1627
|
+
for (const [key, val] of sets) {
|
|
1628
|
+
await git.raw(["config", "--local", key, val]);
|
|
1629
|
+
}
|
|
1630
|
+
if (!await branchExists(git, cfg.developBranch)) {
|
|
1631
|
+
await git.checkoutLocalBranch(cfg.developBranch);
|
|
1632
|
+
const lines = [`Initialized git flow.`, `Created branch: ${cfg.developBranch}`];
|
|
1633
|
+
lines.push(`Main branch: ${cfg.mainBranch}, Develop branch: ${cfg.developBranch}`);
|
|
1634
|
+
return lines.join("\n");
|
|
1635
|
+
}
|
|
1636
|
+
return [
|
|
1637
|
+
`Initialized git flow.`,
|
|
1638
|
+
`Main branch: ${cfg.mainBranch}, Develop branch: ${cfg.developBranch}`,
|
|
1639
|
+
`Prefixes \u2014 feature: ${cfg.featurePrefix}, release: ${cfg.releasePrefix}, hotfix: ${cfg.hotfixPrefix}`
|
|
1640
|
+
].join("\n");
|
|
1641
|
+
}
|
|
1642
|
+
// -----------------------------------------------------------------------
|
|
1643
|
+
// feature
|
|
1644
|
+
// -----------------------------------------------------------------------
|
|
1645
|
+
case "feature-list": {
|
|
1646
|
+
return listBranchesByPrefix(git, cfg.featurePrefix);
|
|
1647
|
+
}
|
|
1648
|
+
case "feature-start": {
|
|
1649
|
+
if (!options.name) throw new Error("name is required for feature-start.");
|
|
1650
|
+
const branch = `${cfg.featurePrefix}${options.name}`;
|
|
1651
|
+
await git.checkoutBranch(branch, cfg.developBranch);
|
|
1652
|
+
return `Created and switched to branch ${branch} from ${cfg.developBranch}.`;
|
|
1653
|
+
}
|
|
1654
|
+
case "feature-finish": {
|
|
1655
|
+
if (!options.name) throw new Error("name is required for feature-finish.");
|
|
1656
|
+
const branch = `${cfg.featurePrefix}${options.name}`;
|
|
1657
|
+
await git.checkout(cfg.developBranch);
|
|
1658
|
+
await git.raw(["merge", "--no-ff", branch, "-m", `Merge branch '${branch}' into ${cfg.developBranch}`]);
|
|
1659
|
+
if (deleteBranch2) {
|
|
1660
|
+
await git.deleteLocalBranch(branch);
|
|
1661
|
+
return `Merged ${branch} into ${cfg.developBranch} and deleted branch.`;
|
|
1662
|
+
}
|
|
1663
|
+
return `Merged ${branch} into ${cfg.developBranch}.`;
|
|
1664
|
+
}
|
|
1665
|
+
case "feature-publish": {
|
|
1666
|
+
if (!options.name) throw new Error("name is required for feature-publish.");
|
|
1667
|
+
const branch = `${cfg.featurePrefix}${options.name}`;
|
|
1668
|
+
const remote = options.remote ?? "origin";
|
|
1669
|
+
await git.push(remote, branch, ["--set-upstream"]);
|
|
1670
|
+
return `Published ${branch} to ${remote}.`;
|
|
1671
|
+
}
|
|
1672
|
+
// -----------------------------------------------------------------------
|
|
1673
|
+
// release
|
|
1674
|
+
// -----------------------------------------------------------------------
|
|
1675
|
+
case "release-list": {
|
|
1676
|
+
return listBranchesByPrefix(git, cfg.releasePrefix);
|
|
1677
|
+
}
|
|
1678
|
+
case "release-start": {
|
|
1679
|
+
if (!options.name) throw new Error("name is required for release-start.");
|
|
1680
|
+
const branch = `${cfg.releasePrefix}${options.name}`;
|
|
1681
|
+
await git.checkoutBranch(branch, cfg.developBranch);
|
|
1682
|
+
return `Created and switched to branch ${branch} from ${cfg.developBranch}.`;
|
|
1683
|
+
}
|
|
1684
|
+
case "release-finish": {
|
|
1685
|
+
if (!options.name) throw new Error("name is required for release-finish.");
|
|
1686
|
+
const branch = `${cfg.releasePrefix}${options.name}`;
|
|
1687
|
+
const tagName = `${cfg.versionTagPrefix}${options.name}`;
|
|
1688
|
+
const tagMsg = options.tagMessage ?? `Release ${options.name}`;
|
|
1689
|
+
await git.checkout(cfg.mainBranch);
|
|
1690
|
+
await git.raw(["merge", "--no-ff", branch, "-m", `Merge branch '${branch}' into ${cfg.mainBranch}`]);
|
|
1691
|
+
if (tagOnFinish) {
|
|
1692
|
+
await git.addAnnotatedTag(tagName, tagMsg);
|
|
1693
|
+
}
|
|
1694
|
+
await git.checkout(cfg.developBranch);
|
|
1695
|
+
await git.raw(["merge", "--no-ff", branch, "-m", `Merge branch '${branch}' into ${cfg.developBranch}`]);
|
|
1696
|
+
const lines = [
|
|
1697
|
+
`Merged ${branch} into ${cfg.mainBranch}.`,
|
|
1698
|
+
...tagOnFinish ? [`Tagged: ${tagName}`] : [],
|
|
1699
|
+
`Merged ${branch} into ${cfg.developBranch}.`
|
|
1700
|
+
];
|
|
1701
|
+
if (deleteBranch2) {
|
|
1702
|
+
await git.deleteLocalBranch(branch);
|
|
1703
|
+
lines.push(`Deleted branch ${branch}.`);
|
|
1704
|
+
}
|
|
1705
|
+
return lines.join("\n");
|
|
1706
|
+
}
|
|
1707
|
+
case "release-publish": {
|
|
1708
|
+
if (!options.name) throw new Error("name is required for release-publish.");
|
|
1709
|
+
const branch = `${cfg.releasePrefix}${options.name}`;
|
|
1710
|
+
const remote = options.remote ?? "origin";
|
|
1711
|
+
await git.push(remote, branch, ["--set-upstream"]);
|
|
1712
|
+
return `Published ${branch} to ${remote}.`;
|
|
1713
|
+
}
|
|
1714
|
+
// -----------------------------------------------------------------------
|
|
1715
|
+
// hotfix
|
|
1716
|
+
// -----------------------------------------------------------------------
|
|
1717
|
+
case "hotfix-list": {
|
|
1718
|
+
return listBranchesByPrefix(git, cfg.hotfixPrefix);
|
|
1719
|
+
}
|
|
1720
|
+
case "hotfix-start": {
|
|
1721
|
+
if (!options.name) throw new Error("name is required for hotfix-start.");
|
|
1722
|
+
const branch = `${cfg.hotfixPrefix}${options.name}`;
|
|
1723
|
+
await git.checkoutBranch(branch, cfg.mainBranch);
|
|
1724
|
+
return `Created and switched to branch ${branch} from ${cfg.mainBranch}.`;
|
|
1725
|
+
}
|
|
1726
|
+
case "hotfix-finish": {
|
|
1727
|
+
if (!options.name) throw new Error("name is required for hotfix-finish.");
|
|
1728
|
+
const branch = `${cfg.hotfixPrefix}${options.name}`;
|
|
1729
|
+
const tagName = `${cfg.versionTagPrefix}${options.name}`;
|
|
1730
|
+
const tagMsg = options.tagMessage ?? `Hotfix ${options.name}`;
|
|
1731
|
+
await git.checkout(cfg.mainBranch);
|
|
1732
|
+
await git.raw(["merge", "--no-ff", branch, "-m", `Merge branch '${branch}' into ${cfg.mainBranch}`]);
|
|
1733
|
+
if (tagOnFinish) {
|
|
1734
|
+
await git.addAnnotatedTag(tagName, tagMsg);
|
|
1735
|
+
}
|
|
1736
|
+
await git.checkout(cfg.developBranch);
|
|
1737
|
+
await git.raw(["merge", "--no-ff", branch, "-m", `Merge branch '${branch}' into ${cfg.developBranch}`]);
|
|
1738
|
+
const lines = [
|
|
1739
|
+
`Merged ${branch} into ${cfg.mainBranch}.`,
|
|
1740
|
+
...tagOnFinish ? [`Tagged: ${tagName}`] : [],
|
|
1741
|
+
`Merged ${branch} into ${cfg.developBranch}.`
|
|
1742
|
+
];
|
|
1743
|
+
if (deleteBranch2) {
|
|
1744
|
+
await git.deleteLocalBranch(branch);
|
|
1745
|
+
lines.push(`Deleted branch ${branch}.`);
|
|
1746
|
+
}
|
|
1747
|
+
return lines.join("\n");
|
|
1748
|
+
}
|
|
1749
|
+
// -----------------------------------------------------------------------
|
|
1750
|
+
// support
|
|
1751
|
+
// -----------------------------------------------------------------------
|
|
1752
|
+
case "support-list": {
|
|
1753
|
+
return listBranchesByPrefix(git, cfg.supportPrefix);
|
|
1754
|
+
}
|
|
1755
|
+
case "support-start": {
|
|
1756
|
+
if (!options.name) throw new Error("name is required for support-start.");
|
|
1757
|
+
const branch = `${cfg.supportPrefix}${options.name}`;
|
|
1758
|
+
await git.checkoutBranch(branch, cfg.mainBranch);
|
|
1759
|
+
return `Created and switched to branch ${branch} from ${cfg.mainBranch}.`;
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
// src/tools/flow.tools.ts
|
|
1765
|
+
function render5(content, format) {
|
|
1766
|
+
if (typeof content === "string" && format === "markdown") {
|
|
1767
|
+
return content;
|
|
1768
|
+
}
|
|
1769
|
+
return JSON.stringify(content, null, 2);
|
|
1770
|
+
}
|
|
1771
|
+
var FLOW_ACTION_VALUES = [
|
|
1772
|
+
"init",
|
|
1773
|
+
"feature-start",
|
|
1774
|
+
"feature-finish",
|
|
1775
|
+
"feature-publish",
|
|
1776
|
+
"feature-list",
|
|
1777
|
+
"release-start",
|
|
1778
|
+
"release-finish",
|
|
1779
|
+
"release-publish",
|
|
1780
|
+
"release-list",
|
|
1781
|
+
"hotfix-start",
|
|
1782
|
+
"hotfix-finish",
|
|
1783
|
+
"hotfix-list",
|
|
1784
|
+
"support-start",
|
|
1785
|
+
"support-list"
|
|
1786
|
+
];
|
|
1787
|
+
function registerFlowTools(server2) {
|
|
1788
|
+
server2.registerTool(
|
|
1789
|
+
"git_flow",
|
|
1790
|
+
{
|
|
1791
|
+
title: "Git Flow Actions",
|
|
1792
|
+
description: 'Implements the git-flow branching model without requiring the git-flow CLI extension. Supports feature, release, hotfix, and support branches. Use "init" first to configure branch names and prefixes in local git config. Branch names (main/develop) and prefixes are read from gitflow.* config with sensible defaults. Actions: init \u2014 set up flow config and develop branch. feature-start/finish/publish/list \u2014 manage feature branches off develop. release-start/finish/publish/list \u2014 manage release branches; finish merges to main+develop and tags. hotfix-start/finish/list \u2014 manage hotfix branches off main; finish merges to main+develop and tags. support-start/list \u2014 create long-lived support branches off main.',
|
|
1793
|
+
inputSchema: {
|
|
1794
|
+
repo_path: RepoPathSchema,
|
|
1795
|
+
action: z6.enum(FLOW_ACTION_VALUES),
|
|
1796
|
+
name: z6.string().optional().describe('Feature/release/hotfix/support name or version (e.g. "my-feature", "1.2.0").'),
|
|
1797
|
+
main_branch: z6.string().optional().describe('Override the main branch name (default: gitflow.branch.master config or "main").'),
|
|
1798
|
+
develop_branch: z6.string().optional().describe('Override the develop branch name (default: gitflow.branch.develop config or "develop").'),
|
|
1799
|
+
remote: z6.string().optional().describe('Remote name for publish operations (default: "origin").'),
|
|
1800
|
+
tag: z6.boolean().default(true).describe("Create an annotated tag when finishing a release or hotfix (default: true)."),
|
|
1801
|
+
tag_message: z6.string().optional().describe("Message for the version tag."),
|
|
1802
|
+
delete_branch: z6.boolean().default(true).describe("Delete the branch after a finish operation (default: true)."),
|
|
1803
|
+
response_format: ResponseFormatSchema
|
|
1804
|
+
},
|
|
1805
|
+
annotations: {
|
|
1806
|
+
readOnlyHint: false,
|
|
1807
|
+
idempotentHint: false,
|
|
1808
|
+
destructiveHint: false,
|
|
1809
|
+
openWorldHint: false
|
|
1810
|
+
}
|
|
1811
|
+
},
|
|
1812
|
+
async ({
|
|
1813
|
+
repo_path,
|
|
1814
|
+
action,
|
|
1815
|
+
name,
|
|
1816
|
+
main_branch,
|
|
1817
|
+
develop_branch,
|
|
1818
|
+
remote,
|
|
1819
|
+
tag,
|
|
1820
|
+
tag_message,
|
|
1821
|
+
delete_branch,
|
|
1822
|
+
response_format
|
|
1823
|
+
}) => {
|
|
1824
|
+
try {
|
|
1825
|
+
const repoPath = resolveRepoPath(repo_path);
|
|
1826
|
+
const output = await runFlowAction(repoPath, {
|
|
1827
|
+
action,
|
|
1828
|
+
name,
|
|
1829
|
+
mainBranch: main_branch,
|
|
1830
|
+
developBranch: develop_branch,
|
|
1831
|
+
remote,
|
|
1832
|
+
tag,
|
|
1833
|
+
tagMessage: tag_message,
|
|
1834
|
+
deleteBranch: delete_branch
|
|
1835
|
+
});
|
|
1836
|
+
return {
|
|
1837
|
+
content: [{ type: "text", text: render5({ output }, response_format) }],
|
|
1838
|
+
structuredContent: { output }
|
|
1839
|
+
};
|
|
1840
|
+
} catch (error) {
|
|
1841
|
+
const gitError = toGitError(error);
|
|
1842
|
+
return { content: [{ type: "text", text: `Error (${gitError.kind}): ${gitError.message}` }] };
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
);
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
// src/tools/inspect.tools.ts
|
|
1849
|
+
import { z as z7 } from "zod";
|
|
1850
|
+
function toText(content, responseFormat) {
|
|
1851
|
+
if (responseFormat === "json") {
|
|
1852
|
+
return JSON.stringify(content, null, 2);
|
|
1853
|
+
}
|
|
1854
|
+
return typeof content === "string" ? content : JSON.stringify(content, null, 2);
|
|
1855
|
+
}
|
|
1856
|
+
function registerInspectTools(server2) {
|
|
1857
|
+
server2.registerTool(
|
|
1858
|
+
"git_status",
|
|
1859
|
+
{
|
|
1860
|
+
title: "Git Status",
|
|
1861
|
+
description: "Get repository working tree and branch status.",
|
|
1862
|
+
inputSchema: {
|
|
1863
|
+
repo_path: RepoPathSchema,
|
|
1864
|
+
response_format: ResponseFormatSchema
|
|
1865
|
+
},
|
|
1866
|
+
annotations: {
|
|
1867
|
+
readOnlyHint: true,
|
|
1868
|
+
idempotentHint: true,
|
|
1869
|
+
destructiveHint: false,
|
|
1870
|
+
openWorldHint: false
|
|
1871
|
+
}
|
|
1872
|
+
},
|
|
1873
|
+
async ({ repo_path, response_format }) => {
|
|
1874
|
+
try {
|
|
1875
|
+
const repoPath = resolveRepoPath(repo_path);
|
|
1876
|
+
const status = await getStatus(repoPath);
|
|
1877
|
+
return {
|
|
1878
|
+
content: [{ type: "text", text: toText(status, response_format) }],
|
|
1879
|
+
structuredContent: { status }
|
|
1880
|
+
};
|
|
1881
|
+
} catch (error) {
|
|
1882
|
+
const gitError = toGitError(error);
|
|
1883
|
+
return { content: [{ type: "text", text: `Error (${gitError.kind}): ${gitError.message}` }] };
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
);
|
|
1887
|
+
server2.registerTool(
|
|
1888
|
+
"git_log",
|
|
1889
|
+
{
|
|
1890
|
+
title: "Git Log",
|
|
1891
|
+
description: "Get commit history with optional filters and pagination.",
|
|
1892
|
+
inputSchema: {
|
|
1893
|
+
repo_path: RepoPathSchema,
|
|
1894
|
+
limit: z7.number().int().min(1).max(200).default(30),
|
|
1895
|
+
offset: z7.number().int().min(0).default(0),
|
|
1896
|
+
author: z7.string().optional(),
|
|
1897
|
+
grep: z7.string().optional(),
|
|
1898
|
+
since: z7.string().optional(),
|
|
1899
|
+
until: z7.string().optional(),
|
|
1900
|
+
file_path: z7.string().optional(),
|
|
1901
|
+
response_format: ResponseFormatSchema
|
|
1902
|
+
},
|
|
1903
|
+
annotations: {
|
|
1904
|
+
readOnlyHint: true,
|
|
1905
|
+
idempotentHint: true,
|
|
1906
|
+
destructiveHint: false,
|
|
1907
|
+
openWorldHint: false
|
|
1908
|
+
}
|
|
1909
|
+
},
|
|
1910
|
+
async ({
|
|
1911
|
+
repo_path,
|
|
1912
|
+
limit,
|
|
1913
|
+
offset,
|
|
1914
|
+
author,
|
|
1915
|
+
grep,
|
|
1916
|
+
since,
|
|
1917
|
+
until,
|
|
1918
|
+
file_path,
|
|
1919
|
+
response_format
|
|
1920
|
+
}) => {
|
|
1921
|
+
try {
|
|
1922
|
+
const repoPath = resolveRepoPath(repo_path);
|
|
1923
|
+
const commits = await getLog(repoPath, {
|
|
1924
|
+
limit,
|
|
1925
|
+
offset,
|
|
1926
|
+
author,
|
|
1927
|
+
grep,
|
|
1928
|
+
since,
|
|
1929
|
+
until,
|
|
1930
|
+
filePath: file_path
|
|
1931
|
+
});
|
|
1932
|
+
return {
|
|
1933
|
+
content: [{ type: "text", text: toText(commits, response_format) }],
|
|
1934
|
+
structuredContent: { commits }
|
|
1935
|
+
};
|
|
1936
|
+
} catch (error) {
|
|
1937
|
+
const gitError = toGitError(error);
|
|
1938
|
+
return { content: [{ type: "text", text: `Error (${gitError.kind}): ${gitError.message}` }] };
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
);
|
|
1942
|
+
server2.registerTool(
|
|
1943
|
+
"git_show",
|
|
1944
|
+
{
|
|
1945
|
+
title: "Git Show",
|
|
1946
|
+
description: "Show patch and metadata for a commit, tag, or ref.",
|
|
1947
|
+
inputSchema: {
|
|
1948
|
+
repo_path: RepoPathSchema,
|
|
1949
|
+
ref: z7.string().min(1),
|
|
1950
|
+
response_format: ResponseFormatSchema
|
|
1951
|
+
},
|
|
1952
|
+
annotations: {
|
|
1953
|
+
readOnlyHint: true,
|
|
1954
|
+
idempotentHint: true,
|
|
1955
|
+
destructiveHint: false,
|
|
1956
|
+
openWorldHint: false
|
|
1957
|
+
}
|
|
1958
|
+
},
|
|
1959
|
+
async ({
|
|
1960
|
+
repo_path,
|
|
1961
|
+
ref,
|
|
1962
|
+
response_format
|
|
1963
|
+
}) => {
|
|
1964
|
+
try {
|
|
1965
|
+
const repoPath = resolveRepoPath(repo_path);
|
|
1966
|
+
const output = await showRef(repoPath, ref);
|
|
1967
|
+
return {
|
|
1968
|
+
content: [{ type: "text", text: toText(output, response_format) }],
|
|
1969
|
+
structuredContent: { ref, output }
|
|
1970
|
+
};
|
|
1971
|
+
} catch (error) {
|
|
1972
|
+
const gitError = toGitError(error);
|
|
1973
|
+
return { content: [{ type: "text", text: `Error (${gitError.kind}): ${gitError.message}` }] };
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
);
|
|
1977
|
+
server2.registerTool(
|
|
1978
|
+
"git_diff",
|
|
1979
|
+
{
|
|
1980
|
+
title: "Git Diff",
|
|
1981
|
+
description: "Show unstaged, staged, or ref-to-ref diff. Supports optional LLM-oriented filtering.",
|
|
1982
|
+
inputSchema: {
|
|
1983
|
+
repo_path: RepoPathSchema,
|
|
1984
|
+
mode: z7.enum(["unstaged", "staged", "refs"]).default("unstaged"),
|
|
1985
|
+
from_ref: z7.string().optional(),
|
|
1986
|
+
to_ref: z7.string().optional(),
|
|
1987
|
+
filtered: z7.boolean().default(false),
|
|
1988
|
+
response_format: ResponseFormatSchema
|
|
1989
|
+
},
|
|
1990
|
+
annotations: {
|
|
1991
|
+
readOnlyHint: true,
|
|
1992
|
+
idempotentHint: true,
|
|
1993
|
+
destructiveHint: false,
|
|
1994
|
+
openWorldHint: false
|
|
1995
|
+
}
|
|
1996
|
+
},
|
|
1997
|
+
async ({
|
|
1998
|
+
repo_path,
|
|
1999
|
+
mode,
|
|
2000
|
+
from_ref,
|
|
2001
|
+
to_ref,
|
|
2002
|
+
filtered,
|
|
2003
|
+
response_format
|
|
2004
|
+
}) => {
|
|
2005
|
+
try {
|
|
2006
|
+
const repoPath = resolveRepoPath(repo_path);
|
|
2007
|
+
const [summary, output] = await Promise.all([
|
|
2008
|
+
getDiffSummary(repoPath, {
|
|
2009
|
+
mode,
|
|
2010
|
+
fromRef: from_ref,
|
|
2011
|
+
toRef: to_ref,
|
|
2012
|
+
filtered
|
|
2013
|
+
}),
|
|
2014
|
+
getDiff(repoPath, {
|
|
2015
|
+
mode,
|
|
2016
|
+
fromRef: from_ref,
|
|
2017
|
+
toRef: to_ref,
|
|
2018
|
+
filtered
|
|
2019
|
+
})
|
|
2020
|
+
]);
|
|
2021
|
+
const payload = { summary, output };
|
|
2022
|
+
return {
|
|
2023
|
+
content: [{ type: "text", text: toText(payload, response_format) }],
|
|
2024
|
+
structuredContent: payload
|
|
2025
|
+
};
|
|
2026
|
+
} catch (error) {
|
|
2027
|
+
const gitError = toGitError(error);
|
|
2028
|
+
return { content: [{ type: "text", text: `Error (${gitError.kind}): ${gitError.message}` }] };
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
);
|
|
2032
|
+
server2.registerTool(
|
|
2033
|
+
"git_blame",
|
|
2034
|
+
{
|
|
2035
|
+
title: "Git Blame",
|
|
2036
|
+
description: "Show line-level author attribution for a file.",
|
|
2037
|
+
inputSchema: {
|
|
2038
|
+
repo_path: RepoPathSchema,
|
|
2039
|
+
file_path: z7.string().min(1),
|
|
2040
|
+
ref: z7.string().optional(),
|
|
2041
|
+
response_format: ResponseFormatSchema
|
|
2042
|
+
},
|
|
2043
|
+
annotations: {
|
|
2044
|
+
readOnlyHint: true,
|
|
2045
|
+
idempotentHint: true,
|
|
2046
|
+
destructiveHint: false,
|
|
2047
|
+
openWorldHint: false
|
|
2048
|
+
}
|
|
2049
|
+
},
|
|
2050
|
+
async ({
|
|
2051
|
+
repo_path,
|
|
2052
|
+
file_path,
|
|
2053
|
+
ref,
|
|
2054
|
+
response_format
|
|
2055
|
+
}) => {
|
|
2056
|
+
try {
|
|
2057
|
+
const repoPath = resolveRepoPath(repo_path);
|
|
2058
|
+
const output = await blameFile(repoPath, file_path, ref);
|
|
2059
|
+
return {
|
|
2060
|
+
content: [{ type: "text", text: toText(output, response_format) }],
|
|
2061
|
+
structuredContent: { file_path, ref, output }
|
|
2062
|
+
};
|
|
2063
|
+
} catch (error) {
|
|
2064
|
+
const gitError = toGitError(error);
|
|
2065
|
+
return { content: [{ type: "text", text: `Error (${gitError.kind}): ${gitError.message}` }] };
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
);
|
|
2069
|
+
server2.registerTool(
|
|
2070
|
+
"git_reflog",
|
|
2071
|
+
{
|
|
2072
|
+
title: "Git Reflog",
|
|
2073
|
+
description: "Show local HEAD and ref movement history for recovery.",
|
|
2074
|
+
inputSchema: {
|
|
2075
|
+
repo_path: RepoPathSchema,
|
|
2076
|
+
limit: z7.number().int().min(1).max(200).default(30),
|
|
2077
|
+
response_format: ResponseFormatSchema
|
|
2078
|
+
},
|
|
2079
|
+
annotations: {
|
|
2080
|
+
readOnlyHint: true,
|
|
2081
|
+
idempotentHint: true,
|
|
2082
|
+
destructiveHint: false,
|
|
2083
|
+
openWorldHint: false
|
|
2084
|
+
}
|
|
2085
|
+
},
|
|
2086
|
+
async ({
|
|
2087
|
+
repo_path,
|
|
2088
|
+
limit,
|
|
2089
|
+
response_format
|
|
2090
|
+
}) => {
|
|
2091
|
+
try {
|
|
2092
|
+
const repoPath = resolveRepoPath(repo_path);
|
|
2093
|
+
const output = await getReflog(repoPath, limit);
|
|
2094
|
+
return {
|
|
2095
|
+
content: [{ type: "text", text: toText(output, response_format) }],
|
|
2096
|
+
structuredContent: { output }
|
|
2097
|
+
};
|
|
2098
|
+
} catch (error) {
|
|
2099
|
+
const gitError = toGitError(error);
|
|
2100
|
+
return { content: [{ type: "text", text: `Error (${gitError.kind}): ${gitError.message}` }] };
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
);
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
// src/tools/lfs.tools.ts
|
|
2107
|
+
import { z as z8 } from "zod";
|
|
2108
|
+
|
|
2109
|
+
// src/services/lfs.service.ts
|
|
2110
|
+
async function runLfsAction(repoPath, options) {
|
|
2111
|
+
const git = getGit(repoPath);
|
|
2112
|
+
switch (options.action) {
|
|
2113
|
+
case "install": {
|
|
2114
|
+
const output = await git.raw(["lfs", "install"]);
|
|
2115
|
+
return output.trim() || "Git LFS installed for this repository.";
|
|
2116
|
+
}
|
|
2117
|
+
case "track": {
|
|
2118
|
+
if (!options.patterns || options.patterns.length === 0) {
|
|
2119
|
+
throw new Error("patterns is required for lfs track.");
|
|
2120
|
+
}
|
|
2121
|
+
const output = await git.raw(["lfs", "track", ...options.patterns]);
|
|
2122
|
+
return output.trim() || `Tracking: ${options.patterns.join(", ")}`;
|
|
2123
|
+
}
|
|
2124
|
+
case "untrack": {
|
|
2125
|
+
if (!options.patterns || options.patterns.length === 0) {
|
|
2126
|
+
throw new Error("patterns is required for lfs untrack.");
|
|
2127
|
+
}
|
|
2128
|
+
const output = await git.raw(["lfs", "untrack", ...options.patterns]);
|
|
2129
|
+
return output.trim() || `Untracked: ${options.patterns.join(", ")}`;
|
|
2130
|
+
}
|
|
2131
|
+
case "ls-files": {
|
|
2132
|
+
const output = await git.raw(["lfs", "ls-files"]);
|
|
2133
|
+
return output.trim() || "No LFS-tracked files found.";
|
|
2134
|
+
}
|
|
2135
|
+
case "status": {
|
|
2136
|
+
const output = await git.raw(["lfs", "status"]);
|
|
2137
|
+
return output.trim() || "No LFS status changes.";
|
|
2138
|
+
}
|
|
2139
|
+
case "pull": {
|
|
2140
|
+
const args = ["lfs", "pull"];
|
|
2141
|
+
if (options.remote) args.push(options.remote);
|
|
2142
|
+
if (options.include) args.push("--include", options.include);
|
|
2143
|
+
if (options.exclude) args.push("--exclude", options.exclude);
|
|
2144
|
+
const output = await git.raw(args);
|
|
2145
|
+
return output.trim() || "LFS pull complete.";
|
|
2146
|
+
}
|
|
2147
|
+
case "push": {
|
|
2148
|
+
if (!options.remote) {
|
|
2149
|
+
throw new Error("remote is required for lfs push.");
|
|
2150
|
+
}
|
|
2151
|
+
const args = ["lfs", "push", options.remote];
|
|
2152
|
+
if (options.everything) args.push("--all");
|
|
2153
|
+
const output = await git.raw(args);
|
|
2154
|
+
return output.trim() || `LFS push to ${options.remote} complete.`;
|
|
2155
|
+
}
|
|
2156
|
+
case "migrate-import": {
|
|
2157
|
+
const args = ["lfs", "migrate", "import"];
|
|
2158
|
+
if (options.everything) args.push("--everything");
|
|
2159
|
+
if (options.include) args.push("--include", options.include);
|
|
2160
|
+
if (options.exclude) args.push("--exclude", options.exclude);
|
|
2161
|
+
const output = await git.raw(args);
|
|
2162
|
+
return output.trim() || "LFS migrate import complete.";
|
|
2163
|
+
}
|
|
2164
|
+
case "migrate-export": {
|
|
2165
|
+
const args = ["lfs", "migrate", "export"];
|
|
2166
|
+
if (options.everything) args.push("--everything");
|
|
2167
|
+
if (options.include) args.push("--include", options.include);
|
|
2168
|
+
if (options.exclude) args.push("--exclude", options.exclude);
|
|
2169
|
+
const output = await git.raw(args);
|
|
2170
|
+
return output.trim() || "LFS migrate export complete.";
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
// src/tools/lfs.tools.ts
|
|
2176
|
+
function render6(content, format) {
|
|
2177
|
+
if (typeof content === "string" && format === "markdown") {
|
|
2178
|
+
return content;
|
|
2179
|
+
}
|
|
2180
|
+
return JSON.stringify(content, null, 2);
|
|
2181
|
+
}
|
|
2182
|
+
function registerLfsTools(server2) {
|
|
2183
|
+
server2.registerTool(
|
|
2184
|
+
"git_lfs",
|
|
2185
|
+
{
|
|
2186
|
+
title: "Git LFS Actions",
|
|
2187
|
+
description: "Manage Git Large File Storage (LFS). Supports tracking/untracking file patterns, listing LFS-tracked files and status, pulling/pushing LFS objects, installing LFS hooks for the repository, and migrating existing files into or out of LFS storage.",
|
|
2188
|
+
inputSchema: {
|
|
2189
|
+
repo_path: RepoPathSchema,
|
|
2190
|
+
action: z8.enum([
|
|
2191
|
+
"track",
|
|
2192
|
+
"untrack",
|
|
2193
|
+
"ls-files",
|
|
2194
|
+
"status",
|
|
2195
|
+
"pull",
|
|
2196
|
+
"push",
|
|
2197
|
+
"install",
|
|
2198
|
+
"migrate-import",
|
|
2199
|
+
"migrate-export"
|
|
2200
|
+
]),
|
|
2201
|
+
patterns: z8.array(z8.string()).optional().describe('File glob patterns for track/untrack (e.g. ["*.psd", "*.zip"]).'),
|
|
2202
|
+
remote: z8.string().optional().describe("Remote name for pull/push operations."),
|
|
2203
|
+
include: z8.string().optional().describe("Comma-separated include patterns for migrate or pull operations."),
|
|
2204
|
+
exclude: z8.string().optional().describe("Comma-separated exclude patterns for migrate or pull operations."),
|
|
2205
|
+
everything: z8.boolean().default(false).describe("Pass --all/--everything to include all refs in push/migrate operations."),
|
|
2206
|
+
response_format: ResponseFormatSchema
|
|
2207
|
+
},
|
|
2208
|
+
annotations: {
|
|
2209
|
+
readOnlyHint: false,
|
|
2210
|
+
idempotentHint: false,
|
|
2211
|
+
destructiveHint: false,
|
|
2212
|
+
openWorldHint: true
|
|
2213
|
+
}
|
|
2214
|
+
},
|
|
2215
|
+
async ({
|
|
2216
|
+
repo_path,
|
|
2217
|
+
action,
|
|
2218
|
+
patterns,
|
|
2219
|
+
remote,
|
|
2220
|
+
include,
|
|
2221
|
+
exclude,
|
|
2222
|
+
everything,
|
|
2223
|
+
response_format
|
|
2224
|
+
}) => {
|
|
2225
|
+
try {
|
|
2226
|
+
const repoPath = resolveRepoPath(repo_path);
|
|
2227
|
+
const output = await runLfsAction(repoPath, {
|
|
2228
|
+
action,
|
|
2229
|
+
patterns,
|
|
2230
|
+
remote,
|
|
2231
|
+
include,
|
|
2232
|
+
exclude,
|
|
2233
|
+
everything
|
|
2234
|
+
});
|
|
2235
|
+
return {
|
|
2236
|
+
content: [{ type: "text", text: render6({ output }, response_format) }],
|
|
2237
|
+
structuredContent: { output }
|
|
2238
|
+
};
|
|
2239
|
+
} catch (error) {
|
|
2240
|
+
const gitError = toGitError(error);
|
|
2241
|
+
return { content: [{ type: "text", text: `Error (${gitError.kind}): ${gitError.message}` }] };
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
);
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
// src/tools/remote.tools.ts
|
|
2248
|
+
import { z as z9 } from "zod";
|
|
2249
|
+
|
|
2250
|
+
// src/services/remote.service.ts
|
|
2251
|
+
function sanitizeRemoteUrl(url) {
|
|
2252
|
+
if (!url) return url;
|
|
2253
|
+
try {
|
|
2254
|
+
const parsed = new URL(url);
|
|
2255
|
+
if (!parsed.username && !parsed.password) return url;
|
|
2256
|
+
parsed.username = "";
|
|
2257
|
+
parsed.password = "";
|
|
2258
|
+
return parsed.toString();
|
|
2259
|
+
} catch {
|
|
2260
|
+
return url;
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
async function listRemotes(repoPath) {
|
|
2264
|
+
const git = getGit(repoPath);
|
|
2265
|
+
const remotes = await git.getRemotes(true);
|
|
2266
|
+
return remotes.map((remote) => ({
|
|
2267
|
+
name: remote.name,
|
|
2268
|
+
fetchUrl: sanitizeRemoteUrl(remote.refs.fetch),
|
|
2269
|
+
pushUrl: sanitizeRemoteUrl(remote.refs.push)
|
|
2270
|
+
}));
|
|
2271
|
+
}
|
|
2272
|
+
async function manageRemote(repoPath, options) {
|
|
2273
|
+
const git = getGit(repoPath);
|
|
2274
|
+
if (options.action === "add") {
|
|
2275
|
+
if (!options.url) {
|
|
2276
|
+
throw new Error("url is required for action='add'");
|
|
2277
|
+
}
|
|
2278
|
+
await git.addRemote(options.name, options.url);
|
|
2279
|
+
return `Added remote ${options.name}.`;
|
|
2280
|
+
}
|
|
2281
|
+
if (options.action === "remove") {
|
|
2282
|
+
await git.removeRemote(options.name);
|
|
2283
|
+
return `Removed remote ${options.name}.`;
|
|
2284
|
+
}
|
|
2285
|
+
if (!options.url) {
|
|
2286
|
+
throw new Error("url is required for action='set-url'");
|
|
2287
|
+
}
|
|
2288
|
+
await git.remote(["set-url", options.name, options.url]);
|
|
2289
|
+
return `Updated remote ${options.name} URL.`;
|
|
2290
|
+
}
|
|
2291
|
+
async function fetchRemote(repoPath, options) {
|
|
2292
|
+
const git = getGit(repoPath);
|
|
2293
|
+
const args = [];
|
|
2294
|
+
if (options.prune) {
|
|
2295
|
+
args.push("--prune");
|
|
2296
|
+
}
|
|
2297
|
+
if (options.remote && options.branch) {
|
|
2298
|
+
await git.fetch(options.remote, options.branch, args);
|
|
2299
|
+
return `Fetched ${options.remote}/${options.branch}.`;
|
|
2300
|
+
} else if (options.remote) {
|
|
2301
|
+
await git.fetch(options.remote, args);
|
|
2302
|
+
return `Fetched ${options.remote}.`;
|
|
2303
|
+
} else {
|
|
2304
|
+
await git.fetch(args);
|
|
2305
|
+
return "Fetched default remote.";
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
async function pullRemote(repoPath, options) {
|
|
2309
|
+
const git = getGit(repoPath);
|
|
2310
|
+
const pullOptions = [];
|
|
2311
|
+
if (options.rebase) {
|
|
2312
|
+
pullOptions.push("--rebase");
|
|
2313
|
+
}
|
|
2314
|
+
await git.pull(options.remote, options.branch, pullOptions);
|
|
2315
|
+
return `Pulled ${options.remote ?? "tracking remote"}${options.branch ? `/${options.branch}` : ""}${options.rebase ? " with rebase" : ""}.`;
|
|
2316
|
+
}
|
|
2317
|
+
async function pushRemote(repoPath, options) {
|
|
2318
|
+
const git = getGit(repoPath);
|
|
2319
|
+
if (options.noVerify && !ALLOW_NO_VERIFY) {
|
|
2320
|
+
throw new Error(
|
|
2321
|
+
"no_verify is disabled on this server. Set GIT_ALLOW_NO_VERIFY=true to permit bypassing git hooks."
|
|
2322
|
+
);
|
|
2323
|
+
}
|
|
2324
|
+
if (options.force && !ALLOW_FORCE_PUSH) {
|
|
2325
|
+
throw new Error(
|
|
2326
|
+
"force push is disabled on this server. Set GIT_ALLOW_FORCE_PUSH=true to enable it. Consider using force_with_lease instead for a safer alternative."
|
|
2327
|
+
);
|
|
2328
|
+
}
|
|
2329
|
+
const pushOptions = [];
|
|
2330
|
+
if (options.setUpstream) {
|
|
2331
|
+
pushOptions.push("--set-upstream");
|
|
2332
|
+
}
|
|
2333
|
+
if (options.forceWithLease) {
|
|
2334
|
+
pushOptions.push("--force-with-lease");
|
|
2335
|
+
}
|
|
2336
|
+
if (options.force) {
|
|
2337
|
+
pushOptions.push("--force");
|
|
2338
|
+
}
|
|
2339
|
+
if (options.tags) {
|
|
2340
|
+
pushOptions.push("--tags");
|
|
2341
|
+
}
|
|
2342
|
+
if (options.noVerify) {
|
|
2343
|
+
pushOptions.push("--no-verify");
|
|
2344
|
+
}
|
|
2345
|
+
await git.push(options.remote, options.branch, pushOptions);
|
|
2346
|
+
return `Pushed ${options.remote ?? "tracking remote"}${options.branch ? `/${options.branch}` : ""}.`;
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2349
|
+
// src/tools/remote.tools.ts
|
|
2350
|
+
function render7(content, format) {
|
|
2351
|
+
if (typeof content === "string" && format === "markdown") {
|
|
2352
|
+
return content;
|
|
2353
|
+
}
|
|
2354
|
+
return JSON.stringify(content, null, 2);
|
|
2355
|
+
}
|
|
2356
|
+
function registerRemoteTools(server2) {
|
|
2357
|
+
server2.registerTool(
|
|
2358
|
+
"git_list_remotes",
|
|
2359
|
+
{
|
|
2360
|
+
title: "List Git Remotes",
|
|
2361
|
+
description: "List configured repository remotes with fetch and push URLs.",
|
|
2362
|
+
inputSchema: {
|
|
2363
|
+
repo_path: RepoPathSchema,
|
|
2364
|
+
response_format: ResponseFormatSchema
|
|
2365
|
+
},
|
|
2366
|
+
annotations: {
|
|
2367
|
+
readOnlyHint: true,
|
|
2368
|
+
idempotentHint: true,
|
|
2369
|
+
destructiveHint: false,
|
|
2370
|
+
openWorldHint: false
|
|
2371
|
+
}
|
|
2372
|
+
},
|
|
2373
|
+
async ({ repo_path, response_format }) => {
|
|
2374
|
+
try {
|
|
2375
|
+
const repoPath = resolveRepoPath(repo_path);
|
|
2376
|
+
const remotes = await listRemotes(repoPath);
|
|
2377
|
+
return {
|
|
2378
|
+
content: [{ type: "text", text: render7({ remotes }, response_format) }],
|
|
2379
|
+
structuredContent: { remotes }
|
|
2380
|
+
};
|
|
2381
|
+
} catch (error) {
|
|
2382
|
+
const gitError = toGitError(error);
|
|
2383
|
+
return { content: [{ type: "text", text: `Error (${gitError.kind}): ${gitError.message}` }] };
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
);
|
|
2387
|
+
server2.registerTool(
|
|
2388
|
+
"git_remote",
|
|
2389
|
+
{
|
|
2390
|
+
title: "Manage Git Remotes",
|
|
2391
|
+
description: "Add, remove, or set URL for remotes using action-based input.",
|
|
2392
|
+
inputSchema: {
|
|
2393
|
+
repo_path: RepoPathSchema,
|
|
2394
|
+
action: z9.enum(["add", "remove", "set-url"]),
|
|
2395
|
+
name: z9.string().min(1),
|
|
2396
|
+
url: z9.string().optional(),
|
|
2397
|
+
response_format: ResponseFormatSchema
|
|
2398
|
+
},
|
|
2399
|
+
annotations: {
|
|
2400
|
+
readOnlyHint: false,
|
|
2401
|
+
idempotentHint: false,
|
|
2402
|
+
destructiveHint: true,
|
|
2403
|
+
openWorldHint: false
|
|
2404
|
+
}
|
|
2405
|
+
},
|
|
2406
|
+
async ({
|
|
2407
|
+
repo_path,
|
|
2408
|
+
action,
|
|
2409
|
+
name,
|
|
2410
|
+
url,
|
|
2411
|
+
response_format
|
|
2412
|
+
}) => {
|
|
2413
|
+
try {
|
|
2414
|
+
const repoPath = resolveRepoPath(repo_path);
|
|
2415
|
+
const message = await manageRemote(repoPath, { action, name, url });
|
|
2416
|
+
return {
|
|
2417
|
+
content: [{ type: "text", text: render7({ message }, response_format) }],
|
|
2418
|
+
structuredContent: { message }
|
|
2419
|
+
};
|
|
2420
|
+
} catch (error) {
|
|
2421
|
+
const gitError = toGitError(error);
|
|
2422
|
+
return { content: [{ type: "text", text: `Error (${gitError.kind}): ${gitError.message}` }] };
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
);
|
|
2426
|
+
server2.registerTool(
|
|
2427
|
+
"git_fetch",
|
|
2428
|
+
{
|
|
2429
|
+
title: "Git Fetch",
|
|
2430
|
+
description: "Fetch updates from remote with optional pruning.",
|
|
2431
|
+
inputSchema: {
|
|
2432
|
+
repo_path: RepoPathSchema,
|
|
2433
|
+
remote: z9.string().optional(),
|
|
2434
|
+
branch: z9.string().optional(),
|
|
2435
|
+
prune: z9.boolean().default(true),
|
|
2436
|
+
response_format: ResponseFormatSchema
|
|
2437
|
+
},
|
|
2438
|
+
annotations: {
|
|
2439
|
+
readOnlyHint: false,
|
|
2440
|
+
idempotentHint: false,
|
|
2441
|
+
destructiveHint: false,
|
|
2442
|
+
openWorldHint: true
|
|
2443
|
+
}
|
|
2444
|
+
},
|
|
2445
|
+
async ({
|
|
2446
|
+
repo_path,
|
|
2447
|
+
remote,
|
|
2448
|
+
branch,
|
|
2449
|
+
prune,
|
|
2450
|
+
response_format
|
|
2451
|
+
}) => {
|
|
2452
|
+
try {
|
|
2453
|
+
const repoPath = resolveRepoPath(repo_path);
|
|
2454
|
+
const message = await fetchRemote(repoPath, { remote, branch, prune });
|
|
2455
|
+
return {
|
|
2456
|
+
content: [{ type: "text", text: render7({ message }, response_format) }],
|
|
2457
|
+
structuredContent: { message }
|
|
2458
|
+
};
|
|
2459
|
+
} catch (error) {
|
|
2460
|
+
const gitError = toGitError(error);
|
|
2461
|
+
return { content: [{ type: "text", text: `Error (${gitError.kind}): ${gitError.message}` }] };
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
);
|
|
2465
|
+
server2.registerTool(
|
|
2466
|
+
"git_pull",
|
|
2467
|
+
{
|
|
2468
|
+
title: "Git Pull",
|
|
2469
|
+
description: "Pull from remote with merge (default) or rebase mode.",
|
|
2470
|
+
inputSchema: {
|
|
2471
|
+
repo_path: RepoPathSchema,
|
|
2472
|
+
remote: z9.string().optional(),
|
|
2473
|
+
branch: z9.string().optional(),
|
|
2474
|
+
rebase: z9.boolean().default(false),
|
|
2475
|
+
response_format: ResponseFormatSchema
|
|
2476
|
+
},
|
|
2477
|
+
annotations: {
|
|
2478
|
+
readOnlyHint: false,
|
|
2479
|
+
idempotentHint: false,
|
|
2480
|
+
destructiveHint: false,
|
|
2481
|
+
openWorldHint: true
|
|
2482
|
+
}
|
|
2483
|
+
},
|
|
2484
|
+
async ({
|
|
2485
|
+
repo_path,
|
|
2486
|
+
remote,
|
|
2487
|
+
branch,
|
|
2488
|
+
rebase,
|
|
2489
|
+
response_format
|
|
2490
|
+
}) => {
|
|
2491
|
+
try {
|
|
2492
|
+
const repoPath = resolveRepoPath(repo_path);
|
|
2493
|
+
const message = await pullRemote(repoPath, { remote, branch, rebase });
|
|
2494
|
+
return {
|
|
2495
|
+
content: [{ type: "text", text: render7({ message }, response_format) }],
|
|
2496
|
+
structuredContent: { message }
|
|
2497
|
+
};
|
|
2498
|
+
} catch (error) {
|
|
2499
|
+
const gitError = toGitError(error);
|
|
2500
|
+
return { content: [{ type: "text", text: `Error (${gitError.kind}): ${gitError.message}` }] };
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
);
|
|
2504
|
+
server2.registerTool(
|
|
2505
|
+
"git_push",
|
|
2506
|
+
{
|
|
2507
|
+
title: "Git Push",
|
|
2508
|
+
description: "Push to remote. Supports safe force (`--force-with-lease`), hard force (if enabled server-side), and bypassing pre-push hooks (if enabled server-side).",
|
|
2509
|
+
inputSchema: {
|
|
2510
|
+
repo_path: RepoPathSchema,
|
|
2511
|
+
remote: z9.string().optional(),
|
|
2512
|
+
branch: z9.string().optional(),
|
|
2513
|
+
set_upstream: z9.boolean().default(false),
|
|
2514
|
+
force_with_lease: z9.boolean().default(false),
|
|
2515
|
+
force: z9.boolean().default(false).describe(
|
|
2516
|
+
"Hard force push (--force). Only accepted when GIT_ALLOW_FORCE_PUSH=true is set on the server. Prefer force_with_lease unless you have a specific reason for a hard force."
|
|
2517
|
+
),
|
|
2518
|
+
no_verify: z9.boolean().default(false).describe(
|
|
2519
|
+
"Bypass pre-push hooks (--no-verify). Only accepted when GIT_ALLOW_NO_VERIFY=true is set on the server."
|
|
2520
|
+
),
|
|
2521
|
+
tags: z9.boolean().default(false),
|
|
2522
|
+
response_format: ResponseFormatSchema
|
|
2523
|
+
},
|
|
2524
|
+
annotations: {
|
|
2525
|
+
readOnlyHint: false,
|
|
2526
|
+
idempotentHint: false,
|
|
2527
|
+
destructiveHint: true,
|
|
2528
|
+
openWorldHint: true
|
|
2529
|
+
}
|
|
2530
|
+
},
|
|
2531
|
+
async ({
|
|
2532
|
+
repo_path,
|
|
2533
|
+
remote,
|
|
2534
|
+
branch,
|
|
2535
|
+
set_upstream,
|
|
2536
|
+
force_with_lease,
|
|
2537
|
+
force,
|
|
2538
|
+
no_verify,
|
|
2539
|
+
tags,
|
|
2540
|
+
response_format
|
|
2541
|
+
}) => {
|
|
2542
|
+
try {
|
|
2543
|
+
const repoPath = resolveRepoPath(repo_path);
|
|
2544
|
+
const message = await pushRemote(repoPath, {
|
|
2545
|
+
remote,
|
|
2546
|
+
branch,
|
|
2547
|
+
setUpstream: set_upstream,
|
|
2548
|
+
forceWithLease: force_with_lease,
|
|
2549
|
+
force,
|
|
2550
|
+
noVerify: no_verify,
|
|
2551
|
+
tags
|
|
2552
|
+
});
|
|
2553
|
+
return {
|
|
2554
|
+
content: [{ type: "text", text: render7({ message }, response_format) }],
|
|
2555
|
+
structuredContent: { message }
|
|
2556
|
+
};
|
|
2557
|
+
} catch (error) {
|
|
2558
|
+
const gitError = toGitError(error);
|
|
2559
|
+
return { content: [{ type: "text", text: `Error (${gitError.kind}): ${gitError.message}` }] };
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
);
|
|
2563
|
+
}
|
|
2564
|
+
|
|
2565
|
+
// src/tools/write.tools.ts
|
|
2566
|
+
import { z as z10 } from "zod";
|
|
2567
|
+
|
|
2568
|
+
// src/services/write.service.ts
|
|
2569
|
+
async function addFiles(repoPath, options) {
|
|
2570
|
+
const git = getGit(repoPath);
|
|
2571
|
+
if (options.all) {
|
|
2572
|
+
await git.add(".");
|
|
2573
|
+
return "Staged all changes.";
|
|
2574
|
+
}
|
|
2575
|
+
const paths = options.paths ?? [];
|
|
2576
|
+
if (paths.length === 0) {
|
|
2577
|
+
throw new Error("Provide paths or set all=true.");
|
|
2578
|
+
}
|
|
2579
|
+
await git.add(paths);
|
|
2580
|
+
return `Staged ${paths.length} path(s).`;
|
|
2581
|
+
}
|
|
2582
|
+
async function restoreFiles(repoPath, options) {
|
|
2583
|
+
const git = getGit(repoPath);
|
|
2584
|
+
if (!options.staged && !options.worktree) {
|
|
2585
|
+
throw new Error("At least one of staged/worktree must be true.");
|
|
2586
|
+
}
|
|
2587
|
+
const args = ["restore"];
|
|
2588
|
+
if (options.staged) {
|
|
2589
|
+
args.push("--staged");
|
|
2590
|
+
}
|
|
2591
|
+
if (options.worktree) {
|
|
2592
|
+
args.push("--worktree");
|
|
2593
|
+
}
|
|
2594
|
+
if (options.source) {
|
|
2595
|
+
args.push("--source", options.source);
|
|
2596
|
+
}
|
|
2597
|
+
args.push("--", ...options.paths);
|
|
2598
|
+
await git.raw(args);
|
|
2599
|
+
return `Restored ${options.paths.length} path(s).`;
|
|
2600
|
+
}
|
|
2601
|
+
async function commitChanges(repoPath, options) {
|
|
2602
|
+
const git = getGit(repoPath);
|
|
2603
|
+
if (options.noVerify && !ALLOW_NO_VERIFY) {
|
|
2604
|
+
throw new Error(
|
|
2605
|
+
"no_verify is disabled on this server. Set GIT_ALLOW_NO_VERIFY=true to permit bypassing git hooks."
|
|
2606
|
+
);
|
|
2607
|
+
}
|
|
2608
|
+
const args = [];
|
|
2609
|
+
if (options.all) {
|
|
2610
|
+
args.push("-a");
|
|
2611
|
+
}
|
|
2612
|
+
if (options.amend) {
|
|
2613
|
+
args.push("--amend");
|
|
2614
|
+
}
|
|
2615
|
+
if (options.noEdit) {
|
|
2616
|
+
args.push("--no-edit");
|
|
2617
|
+
}
|
|
2618
|
+
if (options.noVerify) {
|
|
2619
|
+
args.push("--no-verify");
|
|
2620
|
+
}
|
|
2621
|
+
const shouldSign = options.sign ?? AUTO_SIGN_COMMITS;
|
|
2622
|
+
if (shouldSign) {
|
|
2623
|
+
const key = options.signingKey ?? DEFAULT_SIGNING_KEY;
|
|
2624
|
+
args.push(key ? `--gpg-sign=${key}` : "--gpg-sign");
|
|
2625
|
+
}
|
|
2626
|
+
const result = await git.commit(options.message, args);
|
|
2627
|
+
return `Committed ${result.commit}.`;
|
|
2628
|
+
}
|
|
2629
|
+
async function resetChanges(repoPath, options) {
|
|
2630
|
+
const git = getGit(repoPath);
|
|
2631
|
+
if (options.paths && options.paths.length > 0) {
|
|
2632
|
+
const args2 = ["reset"];
|
|
2633
|
+
if (options.target) {
|
|
2634
|
+
args2.push(options.target);
|
|
2635
|
+
}
|
|
2636
|
+
args2.push("--", ...options.paths);
|
|
2637
|
+
await git.raw(args2);
|
|
2638
|
+
return `Unstaged ${options.paths.length} path(s).`;
|
|
2639
|
+
}
|
|
2640
|
+
const args = ["reset", `--${options.mode}`];
|
|
2641
|
+
if (options.target) {
|
|
2642
|
+
args.push(options.target);
|
|
2643
|
+
}
|
|
2644
|
+
await git.raw(args);
|
|
2645
|
+
return `Reset completed with mode=${options.mode}.`;
|
|
2646
|
+
}
|
|
2647
|
+
async function revertCommit(repoPath, options) {
|
|
2648
|
+
const git = getGit(repoPath);
|
|
2649
|
+
const args = ["revert"];
|
|
2650
|
+
if (options.noCommit) {
|
|
2651
|
+
args.push("--no-commit");
|
|
2652
|
+
}
|
|
2653
|
+
if (typeof options.mainline === "number") {
|
|
2654
|
+
args.push("-m", String(options.mainline));
|
|
2655
|
+
}
|
|
2656
|
+
args.push(options.ref);
|
|
2657
|
+
await git.raw(args);
|
|
2658
|
+
return `Reverted ${options.ref}.`;
|
|
2659
|
+
}
|
|
2660
|
+
|
|
2661
|
+
// src/tools/write.tools.ts
|
|
2662
|
+
function render8(content, format) {
|
|
2663
|
+
if (typeof content === "string" && format === "markdown") {
|
|
2664
|
+
return content;
|
|
2665
|
+
}
|
|
2666
|
+
return JSON.stringify(content, null, 2);
|
|
2667
|
+
}
|
|
2668
|
+
function registerWriteTools(server2) {
|
|
2669
|
+
server2.registerTool(
|
|
2670
|
+
"git_add",
|
|
2671
|
+
{
|
|
2672
|
+
title: "Git Add",
|
|
2673
|
+
description: "Stage files in the index.",
|
|
2674
|
+
inputSchema: {
|
|
2675
|
+
repo_path: RepoPathSchema,
|
|
2676
|
+
all: z10.boolean().default(false),
|
|
2677
|
+
paths: z10.array(z10.string()).optional(),
|
|
2678
|
+
response_format: ResponseFormatSchema
|
|
2679
|
+
},
|
|
2680
|
+
annotations: {
|
|
2681
|
+
readOnlyHint: false,
|
|
2682
|
+
idempotentHint: false,
|
|
2683
|
+
destructiveHint: false,
|
|
2684
|
+
openWorldHint: false
|
|
2685
|
+
}
|
|
2686
|
+
},
|
|
2687
|
+
async ({
|
|
2688
|
+
repo_path,
|
|
2689
|
+
all,
|
|
2690
|
+
paths,
|
|
2691
|
+
response_format
|
|
2692
|
+
}) => {
|
|
2693
|
+
try {
|
|
2694
|
+
const repoPath = resolveRepoPath(repo_path);
|
|
2695
|
+
const message = await addFiles(repoPath, { all, paths });
|
|
2696
|
+
return {
|
|
2697
|
+
content: [{ type: "text", text: render8({ message }, response_format) }],
|
|
2698
|
+
structuredContent: { message }
|
|
2699
|
+
};
|
|
2700
|
+
} catch (error) {
|
|
2701
|
+
const gitError = toGitError(error);
|
|
2702
|
+
return { content: [{ type: "text", text: `Error (${gitError.kind}): ${gitError.message}` }] };
|
|
2703
|
+
}
|
|
2704
|
+
}
|
|
2705
|
+
);
|
|
2706
|
+
server2.registerTool(
|
|
2707
|
+
"git_restore",
|
|
2708
|
+
{
|
|
2709
|
+
title: "Git Restore",
|
|
2710
|
+
description: "Restore paths in worktree and/or index from current state or source ref.",
|
|
2711
|
+
inputSchema: {
|
|
2712
|
+
repo_path: RepoPathSchema,
|
|
2713
|
+
paths: z10.array(z10.string()).min(1),
|
|
2714
|
+
staged: z10.boolean().default(false),
|
|
2715
|
+
worktree: z10.boolean().default(true),
|
|
2716
|
+
source: z10.string().optional(),
|
|
2717
|
+
response_format: ResponseFormatSchema
|
|
2718
|
+
},
|
|
2719
|
+
annotations: {
|
|
2720
|
+
readOnlyHint: false,
|
|
2721
|
+
idempotentHint: true,
|
|
2722
|
+
destructiveHint: true,
|
|
2723
|
+
openWorldHint: false
|
|
2724
|
+
}
|
|
2725
|
+
},
|
|
2726
|
+
async ({
|
|
2727
|
+
repo_path,
|
|
2728
|
+
paths,
|
|
2729
|
+
staged,
|
|
2730
|
+
worktree,
|
|
2731
|
+
source,
|
|
2732
|
+
response_format
|
|
2733
|
+
}) => {
|
|
2734
|
+
try {
|
|
2735
|
+
const repoPath = resolveRepoPath(repo_path);
|
|
2736
|
+
const message = await restoreFiles(repoPath, { paths, staged, worktree, source });
|
|
2737
|
+
return {
|
|
2738
|
+
content: [{ type: "text", text: render8({ message }, response_format) }],
|
|
2739
|
+
structuredContent: { message }
|
|
2740
|
+
};
|
|
2741
|
+
} catch (error) {
|
|
2742
|
+
const gitError = toGitError(error);
|
|
2743
|
+
return { content: [{ type: "text", text: `Error (${gitError.kind}): ${gitError.message}` }] };
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
);
|
|
2747
|
+
server2.registerTool(
|
|
2748
|
+
"git_commit",
|
|
2749
|
+
{
|
|
2750
|
+
title: "Git Commit",
|
|
2751
|
+
description: "Create a commit from staged changes, optionally amending the previous commit. Supports GPG/SSH commit signing and bypassing git hooks (if enabled server-side).",
|
|
2752
|
+
inputSchema: {
|
|
2753
|
+
repo_path: RepoPathSchema,
|
|
2754
|
+
message: z10.string().min(1),
|
|
2755
|
+
all: z10.boolean().default(false),
|
|
2756
|
+
amend: z10.boolean().default(false),
|
|
2757
|
+
no_edit: z10.boolean().default(false),
|
|
2758
|
+
sign: z10.boolean().default(false).describe("Sign the commit with GPG/SSH. Defaults to server AUTO_SIGN_COMMITS setting."),
|
|
2759
|
+
signing_key: z10.string().optional().describe("Specific signing key ID or path. Falls back to GIT_SIGNING_KEY env var."),
|
|
2760
|
+
no_verify: z10.boolean().default(false).describe(
|
|
2761
|
+
"Bypass pre-commit and commit-msg hooks (--no-verify). Only accepted when GIT_ALLOW_NO_VERIFY=true is set on the server."
|
|
2762
|
+
),
|
|
2763
|
+
response_format: ResponseFormatSchema
|
|
2764
|
+
},
|
|
2765
|
+
annotations: {
|
|
2766
|
+
readOnlyHint: false,
|
|
2767
|
+
idempotentHint: false,
|
|
2768
|
+
destructiveHint: false,
|
|
2769
|
+
openWorldHint: false
|
|
2770
|
+
}
|
|
2771
|
+
},
|
|
2772
|
+
async ({
|
|
2773
|
+
repo_path,
|
|
2774
|
+
message,
|
|
2775
|
+
all,
|
|
2776
|
+
amend,
|
|
2777
|
+
no_edit,
|
|
2778
|
+
sign,
|
|
2779
|
+
signing_key,
|
|
2780
|
+
no_verify,
|
|
2781
|
+
response_format
|
|
2782
|
+
}) => {
|
|
2783
|
+
try {
|
|
2784
|
+
const repoPath = resolveRepoPath(repo_path);
|
|
2785
|
+
const commitMessage = await commitChanges(repoPath, {
|
|
2786
|
+
message,
|
|
2787
|
+
all,
|
|
2788
|
+
amend,
|
|
2789
|
+
noEdit: no_edit,
|
|
2790
|
+
sign,
|
|
2791
|
+
signingKey: signing_key,
|
|
2792
|
+
noVerify: no_verify
|
|
2793
|
+
});
|
|
2794
|
+
return {
|
|
2795
|
+
content: [{ type: "text", text: render8({ message: commitMessage }, response_format) }],
|
|
2796
|
+
structuredContent: { message: commitMessage }
|
|
2797
|
+
};
|
|
2798
|
+
} catch (error) {
|
|
2799
|
+
const gitError = toGitError(error);
|
|
2800
|
+
return { content: [{ type: "text", text: `Error (${gitError.kind}): ${gitError.message}` }] };
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
);
|
|
2804
|
+
server2.registerTool(
|
|
2805
|
+
"git_reset",
|
|
2806
|
+
{
|
|
2807
|
+
title: "Git Reset",
|
|
2808
|
+
description: "Reset HEAD/index/worktree by mode. Hard reset requires confirm=true.",
|
|
2809
|
+
inputSchema: {
|
|
2810
|
+
repo_path: RepoPathSchema,
|
|
2811
|
+
mode: z10.enum(["soft", "mixed", "hard"]).default("mixed"),
|
|
2812
|
+
target: z10.string().optional(),
|
|
2813
|
+
paths: z10.array(z10.string()).optional(),
|
|
2814
|
+
confirm: ConfirmSchema,
|
|
2815
|
+
response_format: ResponseFormatSchema
|
|
2816
|
+
},
|
|
2817
|
+
annotations: {
|
|
2818
|
+
readOnlyHint: false,
|
|
2819
|
+
idempotentHint: false,
|
|
2820
|
+
destructiveHint: true,
|
|
2821
|
+
openWorldHint: false
|
|
2822
|
+
}
|
|
2823
|
+
},
|
|
2824
|
+
async ({
|
|
2825
|
+
repo_path,
|
|
2826
|
+
mode,
|
|
2827
|
+
target,
|
|
2828
|
+
paths,
|
|
2829
|
+
confirm,
|
|
2830
|
+
response_format
|
|
2831
|
+
}) => {
|
|
2832
|
+
try {
|
|
2833
|
+
if (mode === "hard" && !confirm) {
|
|
2834
|
+
throw new Error("Hard reset requires confirm=true.");
|
|
2835
|
+
}
|
|
2836
|
+
const repoPath = resolveRepoPath(repo_path);
|
|
2837
|
+
const message = await resetChanges(repoPath, { mode, target, paths });
|
|
2838
|
+
return {
|
|
2839
|
+
content: [{ type: "text", text: render8({ message }, response_format) }],
|
|
2840
|
+
structuredContent: { message }
|
|
2841
|
+
};
|
|
2842
|
+
} catch (error) {
|
|
2843
|
+
const gitError = toGitError(error);
|
|
2844
|
+
return { content: [{ type: "text", text: `Error (${gitError.kind}): ${gitError.message}` }] };
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2847
|
+
);
|
|
2848
|
+
server2.registerTool(
|
|
2849
|
+
"git_revert",
|
|
2850
|
+
{
|
|
2851
|
+
title: "Git Revert",
|
|
2852
|
+
description: "Revert a commit without rewriting history.",
|
|
2853
|
+
inputSchema: {
|
|
2854
|
+
repo_path: RepoPathSchema,
|
|
2855
|
+
ref: z10.string().min(1),
|
|
2856
|
+
no_commit: z10.boolean().default(false),
|
|
2857
|
+
mainline: z10.number().int().min(1).optional(),
|
|
2858
|
+
response_format: ResponseFormatSchema
|
|
2859
|
+
},
|
|
2860
|
+
annotations: {
|
|
2861
|
+
readOnlyHint: false,
|
|
2862
|
+
idempotentHint: false,
|
|
2863
|
+
destructiveHint: true,
|
|
2864
|
+
openWorldHint: false
|
|
2865
|
+
}
|
|
2866
|
+
},
|
|
2867
|
+
async ({
|
|
2868
|
+
repo_path,
|
|
2869
|
+
ref,
|
|
2870
|
+
no_commit,
|
|
2871
|
+
mainline,
|
|
2872
|
+
response_format
|
|
2873
|
+
}) => {
|
|
2874
|
+
try {
|
|
2875
|
+
const repoPath = resolveRepoPath(repo_path);
|
|
2876
|
+
const message = await revertCommit(repoPath, {
|
|
2877
|
+
ref,
|
|
2878
|
+
noCommit: no_commit,
|
|
2879
|
+
mainline
|
|
2880
|
+
});
|
|
2881
|
+
return {
|
|
2882
|
+
content: [{ type: "text", text: render8({ message }, response_format) }],
|
|
2883
|
+
structuredContent: { message }
|
|
2884
|
+
};
|
|
2885
|
+
} catch (error) {
|
|
2886
|
+
const gitError = toGitError(error);
|
|
2887
|
+
return { content: [{ type: "text", text: `Error (${gitError.kind}): ${gitError.message}` }] };
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
);
|
|
2891
|
+
}
|
|
2892
|
+
|
|
2893
|
+
// src/index.ts
|
|
2894
|
+
var server = new McpServer2({
|
|
2895
|
+
name: SERVER_NAME,
|
|
2896
|
+
version: SERVER_VERSION
|
|
2897
|
+
});
|
|
2898
|
+
registerInspectTools(server);
|
|
2899
|
+
registerWriteTools(server);
|
|
2900
|
+
registerBranchTools(server);
|
|
2901
|
+
registerRemoteTools(server);
|
|
2902
|
+
registerAdvancedTools(server);
|
|
2903
|
+
registerContextTools(server);
|
|
2904
|
+
registerLfsTools(server);
|
|
2905
|
+
registerFlowTools(server);
|
|
2906
|
+
registerDocsTools(server);
|
|
2907
|
+
registerGitResources(server);
|
|
2908
|
+
server.registerTool(
|
|
2909
|
+
"git_ping",
|
|
2910
|
+
{
|
|
2911
|
+
title: "Git MCP Ping",
|
|
2912
|
+
description: "Returns a simple response to verify the server is running.",
|
|
2913
|
+
inputSchema: {
|
|
2914
|
+
message: z11.string().default("pong")
|
|
2915
|
+
},
|
|
2916
|
+
annotations: {
|
|
2917
|
+
readOnlyHint: true,
|
|
2918
|
+
idempotentHint: true,
|
|
2919
|
+
destructiveHint: false,
|
|
2920
|
+
openWorldHint: false
|
|
2921
|
+
}
|
|
2922
|
+
},
|
|
2923
|
+
async ({ message }) => {
|
|
2924
|
+
return {
|
|
2925
|
+
content: [{ type: "text", text: `git-mcp-server: ${message}` }],
|
|
2926
|
+
structuredContent: {
|
|
2927
|
+
ok: true,
|
|
2928
|
+
message
|
|
2929
|
+
}
|
|
2930
|
+
};
|
|
2931
|
+
}
|
|
2932
|
+
);
|
|
2933
|
+
async function main() {
|
|
2934
|
+
if (DEFAULT_REPO_PATH) {
|
|
2935
|
+
console.error(`[git-mcp] default repository path: ${DEFAULT_REPO_PATH}`);
|
|
2936
|
+
}
|
|
2937
|
+
if (ALLOW_NO_VERIFY) console.error("[git-mcp] hook bypass enabled (GIT_ALLOW_NO_VERIFY=true)");
|
|
2938
|
+
if (ALLOW_FORCE_PUSH) console.error("[git-mcp] force push enabled (GIT_ALLOW_FORCE_PUSH=true)");
|
|
2939
|
+
if (AUTO_SIGN_COMMITS) console.error("[git-mcp] auto-signing commits (GIT_AUTO_SIGN_COMMITS=true)");
|
|
2940
|
+
if (AUTO_SIGN_TAGS) console.error("[git-mcp] auto-signing tags (GIT_AUTO_SIGN_TAGS=true)");
|
|
2941
|
+
if (DEFAULT_SIGNING_KEY) console.error(`[git-mcp] signing key: ${DEFAULT_SIGNING_KEY}`);
|
|
2942
|
+
const transport = new StdioServerTransport();
|
|
2943
|
+
await server.connect(transport);
|
|
2944
|
+
}
|
|
2945
|
+
main().catch((error) => {
|
|
2946
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2947
|
+
console.error(`Server startup failed: ${message}`);
|
|
2948
|
+
process.exit(1);
|
|
2949
|
+
});
|
|
2950
|
+
//# sourceMappingURL=index.js.map
|