@cyanheads/git-mcp-server 2.1.8 → 2.2.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 +4 -4
- package/dist/mcp-server/server.js +69 -228
- package/dist/mcp-server/tools/gitAdd/index.js +2 -4
- package/dist/mcp-server/tools/gitAdd/logic.js +17 -74
- package/dist/mcp-server/tools/gitAdd/registration.js +38 -59
- package/dist/mcp-server/tools/gitBranch/index.js +3 -5
- package/dist/mcp-server/tools/gitBranch/logic.js +118 -296
- package/dist/mcp-server/tools/gitBranch/registration.js +52 -66
- package/dist/mcp-server/tools/gitCheckout/index.js +2 -3
- package/dist/mcp-server/tools/gitCheckout/logic.js +47 -122
- package/dist/mcp-server/tools/gitCheckout/registration.js +53 -72
- package/dist/mcp-server/tools/gitCherryPick/index.js +3 -5
- package/dist/mcp-server/tools/gitCherryPick/logic.js +55 -162
- package/dist/mcp-server/tools/gitCherryPick/registration.js +52 -67
- package/dist/mcp-server/tools/gitClean/index.js +3 -5
- package/dist/mcp-server/tools/gitClean/logic.js +44 -143
- package/dist/mcp-server/tools/gitClean/registration.js +52 -92
- package/dist/mcp-server/tools/gitClearWorkingDir/index.js +3 -5
- package/dist/mcp-server/tools/gitClearWorkingDir/logic.js +19 -26
- package/dist/mcp-server/tools/gitClearWorkingDir/registration.js +55 -73
- package/dist/mcp-server/tools/gitClone/index.js +2 -4
- package/dist/mcp-server/tools/gitClone/logic.js +50 -171
- package/dist/mcp-server/tools/gitClone/registration.js +51 -42
- package/dist/mcp-server/tools/gitCommit/index.js +2 -4
- package/dist/mcp-server/tools/gitCommit/logic.js +90 -295
- package/dist/mcp-server/tools/gitCommit/registration.js +52 -73
- package/dist/mcp-server/tools/gitDiff/index.js +2 -3
- package/dist/mcp-server/tools/gitDiff/logic.js +78 -254
- package/dist/mcp-server/tools/gitDiff/registration.js +53 -68
- package/dist/mcp-server/tools/gitFetch/index.js +2 -3
- package/dist/mcp-server/tools/gitFetch/logic.js +47 -129
- package/dist/mcp-server/tools/gitFetch/registration.js +54 -66
- package/dist/mcp-server/tools/gitInit/index.js +3 -5
- package/dist/mcp-server/tools/gitInit/logic.js +46 -152
- package/dist/mcp-server/tools/gitInit/registration.js +52 -104
- package/dist/mcp-server/tools/gitLog/index.js +2 -3
- package/dist/mcp-server/tools/gitLog/logic.js +75 -257
- package/dist/mcp-server/tools/gitLog/registration.js +54 -66
- package/dist/mcp-server/tools/gitMerge/index.js +3 -5
- package/dist/mcp-server/tools/gitMerge/logic.js +52 -179
- package/dist/mcp-server/tools/gitMerge/registration.js +52 -71
- package/dist/mcp-server/tools/gitPull/index.js +2 -3
- package/dist/mcp-server/tools/gitPull/logic.js +48 -146
- package/dist/mcp-server/tools/gitPull/registration.js +53 -75
- package/dist/mcp-server/tools/gitPush/index.js +2 -3
- package/dist/mcp-server/tools/gitPush/logic.js +73 -181
- package/dist/mcp-server/tools/gitPush/registration.js +53 -75
- package/dist/mcp-server/tools/gitRebase/index.js +3 -5
- package/dist/mcp-server/tools/gitRebase/logic.js +73 -202
- package/dist/mcp-server/tools/gitRebase/registration.js +52 -70
- package/dist/mcp-server/tools/gitRemote/index.js +3 -5
- package/dist/mcp-server/tools/gitRemote/logic.js +85 -193
- package/dist/mcp-server/tools/gitRemote/registration.js +52 -65
- package/dist/mcp-server/tools/gitReset/index.js +2 -3
- package/dist/mcp-server/tools/gitReset/logic.js +37 -121
- package/dist/mcp-server/tools/gitReset/registration.js +53 -60
- package/dist/mcp-server/tools/gitSetWorkingDir/index.js +3 -5
- package/dist/mcp-server/tools/gitSetWorkingDir/logic.js +45 -133
- package/dist/mcp-server/tools/gitSetWorkingDir/registration.js +55 -85
- package/dist/mcp-server/tools/gitShow/index.js +3 -5
- package/dist/mcp-server/tools/gitShow/logic.js +33 -122
- package/dist/mcp-server/tools/gitShow/registration.js +52 -74
- package/dist/mcp-server/tools/gitStash/index.js +3 -5
- package/dist/mcp-server/tools/gitStash/logic.js +70 -214
- package/dist/mcp-server/tools/gitStash/registration.js +52 -77
- package/dist/mcp-server/tools/gitStatus/index.js +2 -4
- package/dist/mcp-server/tools/gitStatus/logic.js +82 -229
- package/dist/mcp-server/tools/gitStatus/registration.js +52 -66
- package/dist/mcp-server/tools/gitTag/index.js +3 -5
- package/dist/mcp-server/tools/gitTag/logic.js +66 -188
- package/dist/mcp-server/tools/gitTag/registration.js +54 -73
- package/dist/mcp-server/tools/gitWorktree/index.js +3 -5
- package/dist/mcp-server/tools/gitWorktree/logic.js +112 -322
- package/dist/mcp-server/tools/gitWorktree/registration.js +54 -58
- package/dist/mcp-server/tools/gitWrapupInstructions/index.js +5 -3
- package/dist/mcp-server/tools/gitWrapupInstructions/logic.js +26 -38
- package/dist/mcp-server/tools/gitWrapupInstructions/registration.js +54 -70
- package/dist/mcp-server/transports/httpTransport.js +2 -3
- package/package.json +8 -8
|
@@ -1,72 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Defines the core logic, schemas, and types for the git_worktree tool.
|
|
3
|
+
* @module src/mcp-server/tools/gitWorktree/logic
|
|
4
|
+
*/
|
|
1
5
|
import { execFile } from "child_process";
|
|
2
6
|
import { promisify } from "util";
|
|
3
7
|
import { z } from "zod";
|
|
4
8
|
import { logger, sanitization } from "../../../utils/index.js";
|
|
5
|
-
import {
|
|
9
|
+
import { McpError, BaseErrorCode } from "../../../types-global/errors.js";
|
|
6
10
|
const execFileAsync = promisify(execFile);
|
|
7
|
-
//
|
|
11
|
+
// 1. DEFINE the Zod input schema.
|
|
8
12
|
export const GitWorktreeBaseSchema = z.object({
|
|
9
|
-
path: z
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
.string()
|
|
21
|
-
.min(1)
|
|
22
|
-
.optional()
|
|
23
|
-
.describe("Path of the worktree. Required for 'add', 'remove', 'move' modes."),
|
|
24
|
-
// 'add' mode specific
|
|
25
|
-
commitish: z
|
|
26
|
-
.string()
|
|
27
|
-
.min(1)
|
|
28
|
-
.optional()
|
|
29
|
-
.describe("Branch or commit to checkout in the new worktree. Used only in 'add' mode. Defaults to HEAD."),
|
|
30
|
-
newBranch: z
|
|
31
|
-
.string()
|
|
32
|
-
.min(1)
|
|
33
|
-
.optional()
|
|
34
|
-
.describe("Create a new branch in the worktree. Used only in 'add' mode."),
|
|
35
|
-
force: z
|
|
36
|
-
.boolean()
|
|
37
|
-
.default(false)
|
|
38
|
-
.describe("Force the operation (e.g., for 'add' if branch exists, or 'remove' if uncommitted changes)."),
|
|
39
|
-
detach: z
|
|
40
|
-
.boolean()
|
|
41
|
-
.default(false)
|
|
42
|
-
.describe("Detach HEAD in the new worktree. Used only in 'add' mode."),
|
|
43
|
-
// 'move' mode specific
|
|
44
|
-
newPath: z
|
|
45
|
-
.string()
|
|
46
|
-
.min(1)
|
|
47
|
-
.optional()
|
|
48
|
-
.describe("The new path for the worktree. Required for 'move' mode."),
|
|
49
|
-
// 'prune' mode specific
|
|
50
|
-
verbose: z
|
|
51
|
-
.boolean()
|
|
52
|
-
.default(false)
|
|
53
|
-
.describe("Provide more detailed output. Used in 'list' and 'prune' modes."),
|
|
54
|
-
dryRun: z
|
|
55
|
-
.boolean()
|
|
56
|
-
.default(false)
|
|
57
|
-
.describe("Show what would be done without actually doing it. Used in 'prune' mode."),
|
|
58
|
-
expire: z
|
|
59
|
-
.string()
|
|
60
|
-
.min(1)
|
|
61
|
-
.optional()
|
|
62
|
-
.describe("Prune entries older than this time (e.g., '1.month.ago'). Used in 'prune' mode."),
|
|
13
|
+
path: z.string().default(".").describe("Path to the local Git repository."),
|
|
14
|
+
mode: z.enum(["list", "add", "remove", "move", "prune"]).describe("The worktree operation to perform."),
|
|
15
|
+
worktreePath: z.string().min(1).optional().describe("Path of the worktree."),
|
|
16
|
+
commitish: z.string().min(1).optional().describe("Branch or commit to checkout in the new worktree."),
|
|
17
|
+
newBranch: z.string().min(1).optional().describe("Create a new branch in the worktree."),
|
|
18
|
+
force: z.boolean().default(false).describe("Force the operation."),
|
|
19
|
+
detach: z.boolean().default(false).describe("Detach HEAD in the new worktree."),
|
|
20
|
+
newPath: z.string().min(1).optional().describe("The new path for the worktree."),
|
|
21
|
+
verbose: z.boolean().default(false).describe("Provide more detailed output."),
|
|
22
|
+
dryRun: z.boolean().default(false).describe("Show what would be done without actually doing it."),
|
|
23
|
+
expire: z.string().min(1).optional().describe("Prune entries older than this time (e.g., '1.month.ago')."),
|
|
63
24
|
});
|
|
64
|
-
// Apply refinements and export the FINAL schema
|
|
65
25
|
export const GitWorktreeInputSchema = GitWorktreeBaseSchema.refine((data) => !(data.mode === "add" && !data.worktreePath), {
|
|
66
26
|
message: "A 'worktreePath' is required for 'add' mode.",
|
|
67
27
|
path: ["worktreePath"],
|
|
68
|
-
})
|
|
69
|
-
.refine((data) => !(data.mode === "remove" && !data.worktreePath), {
|
|
28
|
+
}).refine((data) => !(data.mode === "remove" && !data.worktreePath), {
|
|
70
29
|
message: "A 'worktreePath' is required for 'remove' mode.",
|
|
71
30
|
path: ["worktreePath"],
|
|
72
31
|
})
|
|
@@ -74,291 +33,122 @@ export const GitWorktreeInputSchema = GitWorktreeBaseSchema.refine((data) => !(d
|
|
|
74
33
|
message: "Both 'worktreePath' (old path) and 'newPath' are required for 'move' mode.",
|
|
75
34
|
path: ["worktreePath", "newPath"],
|
|
76
35
|
});
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
36
|
+
// 2. DEFINE the Zod response schema.
|
|
37
|
+
const WorktreeInfoSchema = z.object({
|
|
38
|
+
path: z.string(),
|
|
39
|
+
head: z.string(),
|
|
40
|
+
branch: z.string().optional(),
|
|
41
|
+
isBare: z.boolean(),
|
|
42
|
+
isLocked: z.boolean(),
|
|
43
|
+
isPrunable: z.boolean(),
|
|
44
|
+
prunableReason: z.string().optional(),
|
|
45
|
+
});
|
|
46
|
+
export const GitWorktreeOutputSchema = z.object({
|
|
47
|
+
success: z.boolean().describe("Indicates if the command was successful."),
|
|
48
|
+
mode: z.string().describe("The mode of operation that was performed."),
|
|
49
|
+
message: z.string().optional().describe("A summary message of the result."),
|
|
50
|
+
worktrees: z.array(WorktreeInfoSchema).optional().describe("A list of worktrees for the 'list' mode."),
|
|
51
|
+
});
|
|
80
52
|
function parsePorcelainWorktreeList(stdout) {
|
|
81
|
-
|
|
82
|
-
const entries = stdout.trim().split("\n\n"); // Entries are separated by double newlines
|
|
83
|
-
for (const entry of entries) {
|
|
53
|
+
return stdout.trim().split("\n\n").map(entry => {
|
|
84
54
|
const lines = entry.trim().split("\n");
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (line.startsWith("
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
else if (line.startsWith("
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
isLocked = true;
|
|
107
|
-
const reasonMatch = line.match(/locked(?: (.+))?/);
|
|
108
|
-
if (reasonMatch && reasonMatch[1]) {
|
|
109
|
-
prunableReason = reasonMatch[1]; // Using prunableReason for lock reason too
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
else if (line.startsWith("prunable")) {
|
|
113
|
-
isPrunable = true;
|
|
114
|
-
const reasonMatch = line.match(/prunable(?: (.+))?/);
|
|
115
|
-
if (reasonMatch && reasonMatch[1]) {
|
|
116
|
-
prunableReason = reasonMatch[1];
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
if (path) {
|
|
121
|
-
// Only add if a path was found
|
|
122
|
-
worktrees.push({
|
|
123
|
-
path,
|
|
124
|
-
head,
|
|
125
|
-
branch,
|
|
126
|
-
isBare,
|
|
127
|
-
isLocked,
|
|
128
|
-
isPrunable,
|
|
129
|
-
prunableReason,
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
return worktrees;
|
|
55
|
+
const info = {
|
|
56
|
+
isBare: false,
|
|
57
|
+
isLocked: false,
|
|
58
|
+
isPrunable: false,
|
|
59
|
+
};
|
|
60
|
+
lines.forEach(line => {
|
|
61
|
+
if (line.startsWith("worktree "))
|
|
62
|
+
info.path = line.substring(9);
|
|
63
|
+
else if (line.startsWith("HEAD "))
|
|
64
|
+
info.head = line.substring(5);
|
|
65
|
+
else if (line.startsWith("branch "))
|
|
66
|
+
info.branch = line.substring(7);
|
|
67
|
+
else if (line.startsWith("bare"))
|
|
68
|
+
info.isBare = true;
|
|
69
|
+
else if (line.startsWith("locked"))
|
|
70
|
+
info.isLocked = true;
|
|
71
|
+
else if (line.startsWith("prunable"))
|
|
72
|
+
info.isPrunable = true;
|
|
73
|
+
});
|
|
74
|
+
return info;
|
|
75
|
+
}).filter(wt => wt.path);
|
|
134
76
|
}
|
|
135
77
|
/**
|
|
136
|
-
*
|
|
78
|
+
* 4. IMPLEMENT the core logic function.
|
|
79
|
+
* @throws {McpError} If the logic encounters an unrecoverable issue.
|
|
137
80
|
*/
|
|
138
|
-
export async function gitWorktreeLogic(
|
|
139
|
-
const operation = `gitWorktreeLogic:${
|
|
140
|
-
logger.debug(`Executing ${operation}`, { ...context,
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
targetPath =
|
|
145
|
-
input.path && input.path !== "." ? input.path : (workingDir ?? ".");
|
|
146
|
-
if (targetPath === "." && !workingDir) {
|
|
147
|
-
logger.warning("Executing git worktree in server's CWD as no path provided and no session WD set.", { ...context, operation });
|
|
148
|
-
targetPath = process.cwd();
|
|
149
|
-
}
|
|
150
|
-
else if (targetPath === "." && workingDir) {
|
|
151
|
-
targetPath = workingDir;
|
|
152
|
-
logger.debug(`Using session working directory: ${targetPath}`, {
|
|
153
|
-
...context,
|
|
154
|
-
operation,
|
|
155
|
-
sessionId: context.sessionId,
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
else {
|
|
159
|
-
logger.debug(`Using provided path: ${targetPath}`, {
|
|
160
|
-
...context,
|
|
161
|
-
operation,
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
targetPath = sanitization.sanitizePath(targetPath, {
|
|
165
|
-
allowAbsolute: true,
|
|
166
|
-
}).sanitizedPath;
|
|
167
|
-
}
|
|
168
|
-
catch (error) {
|
|
169
|
-
logger.error("Path resolution or sanitization failed", {
|
|
170
|
-
...context,
|
|
171
|
-
operation,
|
|
172
|
-
error,
|
|
173
|
-
});
|
|
174
|
-
if (error instanceof McpError)
|
|
175
|
-
throw error;
|
|
176
|
-
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
|
|
81
|
+
export async function gitWorktreeLogic(params, context) {
|
|
82
|
+
const operation = `gitWorktreeLogic:${params.mode}`;
|
|
83
|
+
logger.debug(`Executing ${operation}`, { ...context, params });
|
|
84
|
+
const workingDir = context.getWorkingDirectory();
|
|
85
|
+
if (params.path === "." && !workingDir) {
|
|
86
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, "No session working directory set. Please specify a 'path' or use 'git_set_working_dir' first.");
|
|
177
87
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
switch (
|
|
88
|
+
const targetPath = sanitization.sanitizePath(params.path === "." ? workingDir : params.path, { allowAbsolute: true }).sanitizedPath;
|
|
89
|
+
const buildArgs = () => {
|
|
90
|
+
const baseArgs = ["-C", targetPath, "worktree", params.mode];
|
|
91
|
+
switch (params.mode) {
|
|
182
92
|
case "list":
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
args.push("--porcelain");
|
|
186
|
-
} // Use porcelain for structured output
|
|
187
|
-
logger.debug(`Executing command: git ${args.join(" ")}`, {
|
|
188
|
-
...context,
|
|
189
|
-
operation,
|
|
190
|
-
});
|
|
191
|
-
const { stdout: listStdout } = await execFileAsync("git", args);
|
|
192
|
-
if (input.verbose) {
|
|
193
|
-
const worktrees = parsePorcelainWorktreeList(listStdout);
|
|
194
|
-
result = { success: true, mode: "list", worktrees };
|
|
195
|
-
}
|
|
196
|
-
else {
|
|
197
|
-
// Simple list output parsing (less structured)
|
|
198
|
-
const worktrees = listStdout
|
|
199
|
-
.trim()
|
|
200
|
-
.split("\n")
|
|
201
|
-
.map((line) => {
|
|
202
|
-
const parts = line.split(/\s+/);
|
|
203
|
-
return {
|
|
204
|
-
path: parts[0],
|
|
205
|
-
head: parts[1],
|
|
206
|
-
branch: parts[2]?.replace(/[\[\]]/g, ""), // Remove brackets from branch name
|
|
207
|
-
isBare: false, // Cannot determine from simple list
|
|
208
|
-
isLocked: false, // Cannot determine
|
|
209
|
-
isPrunable: false, // Cannot determine
|
|
210
|
-
};
|
|
211
|
-
});
|
|
212
|
-
result = { success: true, mode: "list", worktrees };
|
|
213
|
-
}
|
|
93
|
+
if (params.verbose)
|
|
94
|
+
baseArgs.push("--verbose");
|
|
214
95
|
break;
|
|
215
96
|
case "add":
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
if (
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
args.push("-b", input.newBranch);
|
|
227
|
-
}
|
|
228
|
-
args.push(sanitizedWorktreePathAdd);
|
|
229
|
-
if (input.commitish) {
|
|
230
|
-
args.push(input.commitish);
|
|
231
|
-
}
|
|
232
|
-
logger.debug(`Executing command: git ${args.join(" ")}`, {
|
|
233
|
-
...context,
|
|
234
|
-
operation,
|
|
235
|
-
});
|
|
236
|
-
await execFileAsync("git", args);
|
|
237
|
-
// To get the HEAD of the new worktree, we might need another command or parse output if available
|
|
238
|
-
// For simplicity, we'll report success. A more robust solution might `git -C new_worktree_path rev-parse HEAD`
|
|
239
|
-
result = {
|
|
240
|
-
success: true,
|
|
241
|
-
mode: "add",
|
|
242
|
-
worktreePath: sanitizedWorktreePathAdd,
|
|
243
|
-
branch: input.newBranch,
|
|
244
|
-
head: "HEAD", // Placeholder, actual SHA would require another call
|
|
245
|
-
message: `Worktree '${sanitizedWorktreePathAdd}' added successfully.`,
|
|
246
|
-
};
|
|
97
|
+
if (params.force)
|
|
98
|
+
baseArgs.push("--force");
|
|
99
|
+
if (params.detach)
|
|
100
|
+
baseArgs.push("--detach");
|
|
101
|
+
if (params.newBranch)
|
|
102
|
+
baseArgs.push("-b", params.newBranch);
|
|
103
|
+
if (params.worktreePath)
|
|
104
|
+
baseArgs.push(params.worktreePath);
|
|
105
|
+
if (params.commitish)
|
|
106
|
+
baseArgs.push(params.commitish);
|
|
247
107
|
break;
|
|
248
108
|
case "remove":
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
args.push("--force");
|
|
254
|
-
}
|
|
255
|
-
args.push(sanitizedWorktreePathRemove);
|
|
256
|
-
logger.debug(`Executing command: git ${args.join(" ")}`, {
|
|
257
|
-
...context,
|
|
258
|
-
operation,
|
|
259
|
-
});
|
|
260
|
-
const { stdout: removeStdout } = await execFileAsync("git", args);
|
|
261
|
-
result = {
|
|
262
|
-
success: true,
|
|
263
|
-
mode: "remove",
|
|
264
|
-
worktreePath: sanitizedWorktreePathRemove,
|
|
265
|
-
message: removeStdout.trim() ||
|
|
266
|
-
`Worktree '${sanitizedWorktreePathRemove}' removed successfully.`,
|
|
267
|
-
};
|
|
109
|
+
if (params.force)
|
|
110
|
+
baseArgs.push("--force");
|
|
111
|
+
if (params.worktreePath)
|
|
112
|
+
baseArgs.push(params.worktreePath);
|
|
268
113
|
break;
|
|
269
114
|
case "move":
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
rootDir: targetPath,
|
|
275
|
-
}).sanitizedPath;
|
|
276
|
-
args = [
|
|
277
|
-
"-C",
|
|
278
|
-
targetPath,
|
|
279
|
-
"worktree",
|
|
280
|
-
"move",
|
|
281
|
-
sanitizedOldPathMove,
|
|
282
|
-
sanitizedNewPathMove,
|
|
283
|
-
];
|
|
284
|
-
logger.debug(`Executing command: git ${args.join(" ")}`, {
|
|
285
|
-
...context,
|
|
286
|
-
operation,
|
|
287
|
-
});
|
|
288
|
-
await execFileAsync("git", args);
|
|
289
|
-
result = {
|
|
290
|
-
success: true,
|
|
291
|
-
mode: "move",
|
|
292
|
-
oldPath: sanitizedOldPathMove,
|
|
293
|
-
newPath: sanitizedNewPathMove,
|
|
294
|
-
message: `Worktree moved from '${sanitizedOldPathMove}' to '${sanitizedNewPathMove}' successfully.`,
|
|
295
|
-
};
|
|
115
|
+
if (params.worktreePath)
|
|
116
|
+
baseArgs.push(params.worktreePath);
|
|
117
|
+
if (params.newPath)
|
|
118
|
+
baseArgs.push(params.newPath);
|
|
296
119
|
break;
|
|
297
120
|
case "prune":
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
if (
|
|
303
|
-
|
|
304
|
-
}
|
|
305
|
-
if (input.expire) {
|
|
306
|
-
args.push(`--expire=${input.expire}`);
|
|
307
|
-
}
|
|
308
|
-
logger.debug(`Executing command: git ${args.join(" ")}`, {
|
|
309
|
-
...context,
|
|
310
|
-
operation,
|
|
311
|
-
});
|
|
312
|
-
const { stdout: pruneStdout, stderr: pruneStderr } = await execFileAsync("git", args);
|
|
313
|
-
// Prune often outputs to stderr even on success for verbose/dry-run
|
|
314
|
-
const pruneMessage = pruneStdout.trim() ||
|
|
315
|
-
pruneStderr.trim() ||
|
|
316
|
-
"Worktree prune operation completed.";
|
|
317
|
-
result = { success: true, mode: "prune", message: pruneMessage };
|
|
318
|
-
if (input.verbose && pruneStdout.trim()) {
|
|
319
|
-
// Attempt to parse verbose output if needed, for now just return raw message
|
|
320
|
-
// result.prunedItems = ...
|
|
321
|
-
}
|
|
121
|
+
if (params.verbose)
|
|
122
|
+
baseArgs.push("--verbose");
|
|
123
|
+
if (params.dryRun)
|
|
124
|
+
baseArgs.push("--dry-run");
|
|
125
|
+
if (params.expire)
|
|
126
|
+
baseArgs.push(`--expire=${params.expire}`);
|
|
322
127
|
break;
|
|
323
|
-
default:
|
|
324
|
-
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid mode: ${input.mode}`, { context, operation });
|
|
325
128
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
});
|
|
332
|
-
|
|
129
|
+
return baseArgs;
|
|
130
|
+
};
|
|
131
|
+
const args = buildArgs();
|
|
132
|
+
try {
|
|
133
|
+
logger.debug(`Executing command: git ${args.join(" ")}`, { ...context, operation });
|
|
134
|
+
const { stdout } = await execFileAsync("git", args);
|
|
135
|
+
if (params.mode === 'list' && params.verbose) {
|
|
136
|
+
return { success: true, mode: params.mode, worktrees: parsePorcelainWorktreeList(stdout) };
|
|
137
|
+
}
|
|
138
|
+
return { success: true, mode: params.mode, message: stdout.trim() || `Worktree ${params.mode} operation successful.` };
|
|
333
139
|
}
|
|
334
140
|
catch (error) {
|
|
335
|
-
const errorMessage = error.stderr || error.
|
|
336
|
-
logger.error(`Failed to execute git worktree command`, {
|
|
337
|
-
...context,
|
|
338
|
-
path: targetPath,
|
|
339
|
-
error: errorMessage,
|
|
340
|
-
stderr: error.stderr,
|
|
341
|
-
stdout: error.stdout,
|
|
342
|
-
});
|
|
141
|
+
const errorMessage = error.stderr || error.message || "";
|
|
142
|
+
logger.error(`Failed to execute git worktree command`, { ...context, operation, errorMessage });
|
|
343
143
|
if (errorMessage.toLowerCase().includes("not a git repository")) {
|
|
344
|
-
throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}
|
|
345
|
-
}
|
|
346
|
-
// Add more specific error handling based on `git worktree` messages
|
|
347
|
-
if (input.mode === "add" && errorMessage.includes("already exists")) {
|
|
348
|
-
throw new McpError(BaseErrorCode.CONFLICT, `Failed to add worktree: Path '${input.worktreePath}' already exists or is a worktree. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
349
|
-
}
|
|
350
|
-
if (input.mode === "add" && errorMessage.includes("is a submodule")) {
|
|
351
|
-
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Failed to add worktree: Path '${input.worktreePath}' is a submodule. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
144
|
+
throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`);
|
|
352
145
|
}
|
|
353
|
-
if (
|
|
354
|
-
|
|
355
|
-
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Failed to remove worktree: Cannot remove the current worktree. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
146
|
+
if (params.mode === "add" && errorMessage.includes("already exists")) {
|
|
147
|
+
throw new McpError(BaseErrorCode.CONFLICT, `Path '${params.worktreePath}' already exists or is a worktree.`);
|
|
356
148
|
}
|
|
357
|
-
if (
|
|
358
|
-
|
|
359
|
-
throw new McpError(BaseErrorCode.CONFLICT, `Failed to remove worktree: '${input.worktreePath}' has uncommitted changes. Use force=true to remove. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
149
|
+
if (params.mode === "remove" && errorMessage.includes("has unclean changes")) {
|
|
150
|
+
throw new McpError(BaseErrorCode.CONFLICT, `Worktree '${params.worktreePath}' has uncommitted changes. Use force=true to remove.`);
|
|
360
151
|
}
|
|
361
|
-
|
|
362
|
-
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Git worktree ${input.mode} failed for path: ${targetPath}. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
152
|
+
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Git worktree ${params.mode} failed: ${errorMessage}`);
|
|
363
153
|
}
|
|
364
154
|
}
|
|
@@ -1,70 +1,66 @@
|
|
|
1
|
-
import { logger, ErrorHandler, requestContextService, } from "../../../utils/index.js";
|
|
2
|
-
import { BaseErrorCode } from "../../../types-global/errors.js";
|
|
3
|
-
import { GitWorktreeBaseSchema, gitWorktreeLogic, } from "./logic.js";
|
|
4
|
-
let _getWorkingDirectory;
|
|
5
|
-
let _getSessionId;
|
|
6
1
|
/**
|
|
7
|
-
*
|
|
8
|
-
* @
|
|
9
|
-
* @param getSidFn - Function to get the session ID from context.
|
|
2
|
+
* @fileoverview Handles registration and error handling for the git_worktree tool.
|
|
3
|
+
* @module src/mcp-server/tools/gitWorktree/registration
|
|
10
4
|
*/
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
logger.info("State accessors initialized for git_worktree tool registration.");
|
|
15
|
-
}
|
|
5
|
+
import { ErrorHandler, logger, requestContextService } from "../../../utils/index.js";
|
|
6
|
+
import { McpError, BaseErrorCode } from "../../../types-global/errors.js";
|
|
7
|
+
import { gitWorktreeLogic, GitWorktreeInputSchema, GitWorktreeOutputSchema, GitWorktreeBaseSchema, } from "./logic.js";
|
|
16
8
|
const TOOL_NAME = "git_worktree";
|
|
17
9
|
const TOOL_DESCRIPTION = "Manages Git worktrees. Supports listing, adding, removing, moving, and pruning worktrees. Returns results as a JSON object.";
|
|
18
10
|
/**
|
|
19
|
-
* Registers the git_worktree tool with the MCP server.
|
|
20
|
-
*
|
|
21
|
-
* @param
|
|
22
|
-
* @
|
|
23
|
-
* @throws {Error} If registration fails or state accessors are not initialized.
|
|
11
|
+
* Registers the git_worktree tool with the MCP server instance.
|
|
12
|
+
* @param server The MCP server instance.
|
|
13
|
+
* @param getWorkingDirectory Function to get the session's working directory.
|
|
14
|
+
* @param getSessionId Function to get the session ID from context.
|
|
24
15
|
*/
|
|
25
|
-
export const registerGitWorktreeTool = async (server) => {
|
|
26
|
-
if (!_getWorkingDirectory || !_getSessionId) {
|
|
27
|
-
throw new Error("State accessors for git_worktree must be initialized before registration.");
|
|
28
|
-
}
|
|
16
|
+
export const registerGitWorktreeTool = async (server, getWorkingDirectory, getSessionId) => {
|
|
29
17
|
const operation = "registerGitWorktreeTool";
|
|
30
18
|
const context = requestContextService.createRequestContext({ operation });
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
19
|
+
server.registerTool(TOOL_NAME, {
|
|
20
|
+
title: "Git Worktree",
|
|
21
|
+
description: TOOL_DESCRIPTION,
|
|
22
|
+
inputSchema: GitWorktreeBaseSchema.shape,
|
|
23
|
+
outputSchema: GitWorktreeOutputSchema.shape,
|
|
24
|
+
annotations: {
|
|
25
|
+
readOnlyHint: false,
|
|
26
|
+
destructiveHint: true, // Can add/remove/move worktrees
|
|
27
|
+
idempotentHint: false,
|
|
28
|
+
openWorldHint: false,
|
|
29
|
+
},
|
|
30
|
+
}, async (params, callContext) => {
|
|
31
|
+
const handlerContext = requestContextService.createRequestContext({
|
|
32
|
+
toolName: TOOL_NAME,
|
|
33
|
+
parentContext: callContext,
|
|
34
|
+
});
|
|
35
|
+
try {
|
|
36
|
+
// Explicitly parse with the refined schema to enforce validation rules
|
|
37
|
+
const validatedParams = GitWorktreeInputSchema.parse(params);
|
|
38
|
+
const sessionId = getSessionId(handlerContext);
|
|
39
|
+
const result = await gitWorktreeLogic(validatedParams, {
|
|
40
|
+
...handlerContext,
|
|
41
|
+
getWorkingDirectory: () => getWorkingDirectory(sessionId),
|
|
38
42
|
});
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
...requestContext,
|
|
43
|
-
sessionId: sessionId,
|
|
44
|
-
getWorkingDirectory: getWorkingDirectoryForSession,
|
|
43
|
+
return {
|
|
44
|
+
structuredContent: result,
|
|
45
|
+
content: [{ type: "text", text: `Success: ${JSON.stringify(result, null, 2)}` }],
|
|
45
46
|
};
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
};
|
|
54
|
-
if (worktreeResult.success) {
|
|
55
|
-
logger.info(`Tool ${TOOL_NAME} (mode: ${toolInput.mode}) executed successfully, returning JSON`, logicContext);
|
|
56
|
-
}
|
|
57
|
-
else {
|
|
58
|
-
logger.warning(`Tool ${TOOL_NAME} (mode: ${toolInput.mode}) failed: ${worktreeResult.message}`, { ...logicContext, errorDetails: worktreeResult.error });
|
|
59
|
-
}
|
|
60
|
-
return { content: [resultContent] };
|
|
61
|
-
}, {
|
|
62
|
-
operation: toolOperation,
|
|
63
|
-
context: logicContext,
|
|
64
|
-
input: validatedArgs,
|
|
65
|
-
errorCode: BaseErrorCode.INTERNAL_ERROR,
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
logger.error(`Error in ${TOOL_NAME} handler`, { error, ...handlerContext });
|
|
50
|
+
const handledError = ErrorHandler.handleError(error, {
|
|
51
|
+
operation: `tool:${TOOL_NAME}`,
|
|
52
|
+
context: handlerContext,
|
|
53
|
+
input: params,
|
|
66
54
|
});
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
55
|
+
const mcpError = handledError instanceof McpError
|
|
56
|
+
? handledError
|
|
57
|
+
: new McpError(BaseErrorCode.INTERNAL_ERROR, "An unexpected error occurred.", { originalError: handledError });
|
|
58
|
+
return {
|
|
59
|
+
isError: true,
|
|
60
|
+
content: [{ type: "text", text: mcpError.message }],
|
|
61
|
+
structuredContent: mcpError.details,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
logger.info(`Tool '${TOOL_NAME}' registered successfully.`, context);
|
|
70
66
|
};
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Barrel file for the gitWrapupInstructions tool.
|
|
3
|
+
* @module src/mcp-server/tools/gitWrapupInstructions/index
|
|
4
|
+
*/
|
|
5
|
+
export { registerGitWrapupInstructionsTool } from "./registration.js";
|