@cyanheads/git-mcp-server 2.1.8 → 2.2.1

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.
Files changed (79) hide show
  1. package/README.md +4 -4
  2. package/dist/mcp-server/server.js +69 -228
  3. package/dist/mcp-server/tools/gitAdd/index.js +2 -4
  4. package/dist/mcp-server/tools/gitAdd/logic.js +40 -116
  5. package/dist/mcp-server/tools/gitAdd/registration.js +39 -59
  6. package/dist/mcp-server/tools/gitBranch/index.js +3 -5
  7. package/dist/mcp-server/tools/gitBranch/logic.js +109 -304
  8. package/dist/mcp-server/tools/gitBranch/registration.js +52 -66
  9. package/dist/mcp-server/tools/gitCheckout/index.js +2 -3
  10. package/dist/mcp-server/tools/gitCheckout/logic.js +47 -144
  11. package/dist/mcp-server/tools/gitCheckout/registration.js +53 -72
  12. package/dist/mcp-server/tools/gitCherryPick/index.js +3 -5
  13. package/dist/mcp-server/tools/gitCherryPick/logic.js +47 -173
  14. package/dist/mcp-server/tools/gitCherryPick/registration.js +52 -67
  15. package/dist/mcp-server/tools/gitClean/index.js +3 -5
  16. package/dist/mcp-server/tools/gitClean/logic.js +45 -154
  17. package/dist/mcp-server/tools/gitClean/registration.js +52 -92
  18. package/dist/mcp-server/tools/gitClearWorkingDir/index.js +3 -5
  19. package/dist/mcp-server/tools/gitClearWorkingDir/logic.js +18 -32
  20. package/dist/mcp-server/tools/gitClearWorkingDir/registration.js +55 -73
  21. package/dist/mcp-server/tools/gitClone/index.js +2 -4
  22. package/dist/mcp-server/tools/gitClone/logic.js +47 -187
  23. package/dist/mcp-server/tools/gitClone/registration.js +51 -42
  24. package/dist/mcp-server/tools/gitCommit/index.js +2 -4
  25. package/dist/mcp-server/tools/gitCommit/logic.js +75 -310
  26. package/dist/mcp-server/tools/gitCommit/registration.js +52 -73
  27. package/dist/mcp-server/tools/gitDiff/index.js +2 -3
  28. package/dist/mcp-server/tools/gitDiff/logic.js +72 -264
  29. package/dist/mcp-server/tools/gitDiff/registration.js +53 -68
  30. package/dist/mcp-server/tools/gitFetch/index.js +2 -3
  31. package/dist/mcp-server/tools/gitFetch/logic.js +38 -136
  32. package/dist/mcp-server/tools/gitFetch/registration.js +54 -66
  33. package/dist/mcp-server/tools/gitInit/index.js +3 -5
  34. package/dist/mcp-server/tools/gitInit/logic.js +40 -162
  35. package/dist/mcp-server/tools/gitInit/registration.js +52 -104
  36. package/dist/mcp-server/tools/gitLog/index.js +2 -3
  37. package/dist/mcp-server/tools/gitLog/logic.js +71 -266
  38. package/dist/mcp-server/tools/gitLog/registration.js +54 -66
  39. package/dist/mcp-server/tools/gitMerge/index.js +3 -5
  40. package/dist/mcp-server/tools/gitMerge/logic.js +45 -191
  41. package/dist/mcp-server/tools/gitMerge/registration.js +52 -71
  42. package/dist/mcp-server/tools/gitPull/index.js +2 -3
  43. package/dist/mcp-server/tools/gitPull/logic.js +39 -156
  44. package/dist/mcp-server/tools/gitPull/registration.js +53 -75
  45. package/dist/mcp-server/tools/gitPush/index.js +2 -3
  46. package/dist/mcp-server/tools/gitPush/logic.js +65 -192
  47. package/dist/mcp-server/tools/gitPush/registration.js +53 -75
  48. package/dist/mcp-server/tools/gitRebase/index.js +3 -5
  49. package/dist/mcp-server/tools/gitRebase/logic.js +59 -207
  50. package/dist/mcp-server/tools/gitRebase/registration.js +52 -70
  51. package/dist/mcp-server/tools/gitRemote/index.js +3 -5
  52. package/dist/mcp-server/tools/gitRemote/logic.js +76 -200
  53. package/dist/mcp-server/tools/gitRemote/registration.js +52 -65
  54. package/dist/mcp-server/tools/gitReset/index.js +2 -3
  55. package/dist/mcp-server/tools/gitReset/logic.js +33 -133
  56. package/dist/mcp-server/tools/gitReset/registration.js +53 -60
  57. package/dist/mcp-server/tools/gitSetWorkingDir/index.js +3 -5
  58. package/dist/mcp-server/tools/gitSetWorkingDir/logic.js +39 -144
  59. package/dist/mcp-server/tools/gitSetWorkingDir/registration.js +55 -85
  60. package/dist/mcp-server/tools/gitShow/index.js +3 -5
  61. package/dist/mcp-server/tools/gitShow/logic.js +28 -133
  62. package/dist/mcp-server/tools/gitShow/registration.js +52 -74
  63. package/dist/mcp-server/tools/gitStash/index.js +3 -5
  64. package/dist/mcp-server/tools/gitStash/logic.js +59 -219
  65. package/dist/mcp-server/tools/gitStash/registration.js +52 -77
  66. package/dist/mcp-server/tools/gitStatus/index.js +2 -4
  67. package/dist/mcp-server/tools/gitStatus/logic.js +79 -236
  68. package/dist/mcp-server/tools/gitStatus/registration.js +52 -66
  69. package/dist/mcp-server/tools/gitTag/index.js +3 -5
  70. package/dist/mcp-server/tools/gitTag/logic.js +57 -198
  71. package/dist/mcp-server/tools/gitTag/registration.js +54 -73
  72. package/dist/mcp-server/tools/gitWorktree/index.js +3 -5
  73. package/dist/mcp-server/tools/gitWorktree/logic.js +102 -328
  74. package/dist/mcp-server/tools/gitWorktree/registration.js +54 -58
  75. package/dist/mcp-server/tools/gitWrapupInstructions/index.js +5 -3
  76. package/dist/mcp-server/tools/gitWrapupInstructions/logic.js +25 -43
  77. package/dist/mcp-server/tools/gitWrapupInstructions/registration.js +54 -70
  78. package/dist/mcp-server/transports/httpTransport.js +2 -3
  79. 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 { BaseErrorCode, McpError } from "../../../types-global/errors.js";
9
+ import { McpError, BaseErrorCode } from "../../../types-global/errors.js";
6
10
  const execFileAsync = promisify(execFile);
7
- // Define the BASE input schema for the git_worktree tool using Zod
11
+ // 1. DEFINE the Zod input schema.
8
12
  export const GitWorktreeBaseSchema = z.object({
9
- path: z
10
- .string()
11
- .min(1)
12
- .optional()
13
- .default(".")
14
- .describe("Path to the local Git repository. Defaults to the directory set via `git_set_working_dir` for the session; set 'git_set_working_dir' if not set."),
15
- mode: z
16
- .enum(["list", "add", "remove", "move", "prune"])
17
- .describe("The worktree operation to perform: 'list', 'add', 'remove', 'move', 'prune'."),
18
- // Common optional path for operations
19
- worktreePath: z
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,106 @@ 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
- * Parses the output of `git worktree list --porcelain`.
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
- const worktrees = [];
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
- let path = "";
86
- let head = "";
87
- let branch;
88
- let isBare = false;
89
- let isLocked = false;
90
- let isPrunable = false;
91
- let prunableReason;
92
- for (const line of lines) {
93
- if (line.startsWith("worktree ")) {
94
- path = line.substring("worktree ".length);
95
- }
96
- else if (line.startsWith("HEAD ")) {
97
- head = line.substring("HEAD ".length);
98
- }
99
- else if (line.startsWith("branch ")) {
100
- branch = line.substring("branch ".length);
101
- }
102
- else if (line.startsWith("bare")) {
103
- isBare = true;
104
- }
105
- else if (line.startsWith("locked")) {
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
- * Executes git worktree commands.
78
+ * 4. IMPLEMENT the core logic function.
79
+ * @throws {McpError} If the logic encounters an unrecoverable issue.
137
80
  */
138
- export async function gitWorktreeLogic(input, context) {
139
- const operation = `gitWorktreeLogic:${input.mode}`;
140
- logger.debug(`Executing ${operation}`, { ...context, input });
141
- let targetPath;
142
- try {
143
- const workingDir = context.getWorkingDirectory();
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
- try {
179
- let args;
180
- let result;
181
- switch (input.mode) {
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
- args = ["-C", targetPath, "worktree", "list"];
184
- if (input.verbose) {
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
- // worktreePath is guaranteed by refine
217
- const sanitizedWorktreePathAdd = sanitization.sanitizePath(input.worktreePath, { allowAbsolute: true, rootDir: targetPath }).sanitizedPath;
218
- args = ["-C", targetPath, "worktree", "add"];
219
- if (input.force) {
220
- args.push("--force");
221
- }
222
- if (input.detach) {
223
- args.push("--detach");
224
- }
225
- if (input.newBranch) {
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
- // worktreePath is guaranteed by refine
250
- const sanitizedWorktreePathRemove = sanitization.sanitizePath(input.worktreePath, { allowAbsolute: true, rootDir: targetPath }).sanitizedPath;
251
- args = ["-C", targetPath, "worktree", "remove"];
252
- if (input.force) {
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
- // worktreePath and newPath are guaranteed by refine
271
- const sanitizedOldPathMove = sanitization.sanitizePath(input.worktreePath, { allowAbsolute: true, rootDir: targetPath }).sanitizedPath;
272
- const sanitizedNewPathMove = sanitization.sanitizePath(input.newPath, {
273
- allowAbsolute: true,
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
- args = ["-C", targetPath, "worktree", "prune"];
299
- if (input.dryRun) {
300
- args.push("--dry-run");
301
- }
302
- if (input.verbose) {
303
- args.push("--verbose");
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
- }
326
- logger.info(`git worktree ${input.mode} executed successfully`, {
327
- ...context,
328
- operation,
329
- path: targetPath,
330
- result,
331
- });
332
- return result;
333
- }
334
- catch (error) {
335
- const errorMessage = error.stderr || error.stdout || error.message || "";
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
- });
343
- if (errorMessage.toLowerCase().includes("not a git repository")) {
344
- throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`, { context, operation, originalError: error });
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 });
352
- }
353
- if (input.mode === "remove" &&
354
- errorMessage.includes("cannot remove the current worktree")) {
355
- throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Failed to remove worktree: Cannot remove the current worktree. Error: ${errorMessage}`, { context, operation, originalError: error });
356
- }
357
- if (input.mode === "remove" &&
358
- errorMessage.includes("has unclean changes")) {
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 });
360
128
  }
361
- // Throw a generic McpError for other failures
362
- throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Git worktree ${input.mode} failed for path: ${targetPath}. Error: ${errorMessage}`, { context, operation, originalError: error });
129
+ return baseArgs;
130
+ };
131
+ const args = buildArgs();
132
+ logger.debug(`Executing command: git ${args.join(" ")}`, { ...context, operation });
133
+ const { stdout } = await execFileAsync("git", args);
134
+ if (params.mode === 'list' && params.verbose) {
135
+ return { success: true, mode: params.mode, worktrees: parsePorcelainWorktreeList(stdout) };
363
136
  }
137
+ return { success: true, mode: params.mode, message: stdout.trim() || `Worktree ${params.mode} operation successful.` };
364
138
  }
@@ -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
- * Initializes the state accessors needed by the git_worktree tool registration.
8
- * @param getWdFn - Function to get the working directory for a session.
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
- export function initializeGitWorktreeStateAccessors(getWdFn, getSidFn) {
12
- _getWorkingDirectory = getWdFn;
13
- _getSessionId = getSidFn;
14
- logger.info("State accessors initialized for git_worktree tool registration.");
15
- }
5
+ import { ErrorHandler, logger, requestContextService } from "../../../utils/index.js";
6
+ import { gitWorktreeLogic, GitWorktreeInputSchema, GitWorktreeOutputSchema, GitWorktreeBaseSchema, } from "./logic.js";
16
7
  const TOOL_NAME = "git_worktree";
17
8
  const TOOL_DESCRIPTION = "Manages Git worktrees. Supports listing, adding, removing, moving, and pruning worktrees. Returns results as a JSON object.";
18
9
  /**
19
- * Registers the git_worktree tool with the MCP server.
20
- *
21
- * @param {McpServer} server - The McpServer instance to register the tool with.
22
- * @returns {Promise<void>}
23
- * @throws {Error} If registration fails or state accessors are not initialized.
10
+ * Registers the git_worktree tool with the MCP server instance.
11
+ * @param server The MCP server instance.
12
+ * @param getWorkingDirectory Function to get the session's working directory.
13
+ * @param getSessionId Function to get the session ID from context.
24
14
  */
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
- }
15
+ export const registerGitWorktreeTool = async (server, getWorkingDirectory, getSessionId) => {
29
16
  const operation = "registerGitWorktreeTool";
30
17
  const context = requestContextService.createRequestContext({ operation });
31
- await ErrorHandler.tryCatch(async () => {
32
- server.tool(TOOL_NAME, TOOL_DESCRIPTION, GitWorktreeBaseSchema.shape, async (validatedArgs, callContext) => {
33
- const toolInput = validatedArgs; // Cast for use
34
- const toolOperation = `tool:${TOOL_NAME}:${toolInput.mode}`;
35
- const requestContext = requestContextService.createRequestContext({
36
- operation: toolOperation,
37
- parentContext: callContext,
18
+ server.registerTool(TOOL_NAME, {
19
+ title: "Git Worktree",
20
+ description: TOOL_DESCRIPTION,
21
+ inputSchema: GitWorktreeBaseSchema.shape,
22
+ outputSchema: GitWorktreeOutputSchema.shape,
23
+ annotations: {
24
+ readOnlyHint: false,
25
+ destructiveHint: true, // Can add/remove/move worktrees
26
+ idempotentHint: false,
27
+ openWorldHint: false,
28
+ },
29
+ }, async (params, callContext) => {
30
+ const handlerContext = requestContextService.createRequestContext({
31
+ toolName: TOOL_NAME,
32
+ parentContext: callContext,
33
+ });
34
+ try {
35
+ // Explicitly parse with the refined schema to enforce validation rules
36
+ const validatedParams = GitWorktreeInputSchema.parse(params);
37
+ const sessionId = getSessionId(handlerContext);
38
+ const result = await gitWorktreeLogic(validatedParams, {
39
+ ...handlerContext,
40
+ getWorkingDirectory: () => getWorkingDirectory(sessionId),
38
41
  });
39
- const sessionId = _getSessionId(requestContext);
40
- const getWorkingDirectoryForSession = () => _getWorkingDirectory(sessionId);
41
- const logicContext = {
42
- ...requestContext,
43
- sessionId: sessionId,
44
- getWorkingDirectory: getWorkingDirectoryForSession,
42
+ return {
43
+ structuredContent: result,
44
+ content: [{ type: "text", text: `Success: ${JSON.stringify(result, null, 2)}` }],
45
45
  };
46
- logger.info(`Executing tool: ${TOOL_NAME} (mode: ${toolInput.mode})`, logicContext);
47
- return await ErrorHandler.tryCatch(async () => {
48
- const worktreeResult = await gitWorktreeLogic(toolInput, logicContext);
49
- const resultContent = {
50
- type: "text",
51
- text: JSON.stringify(worktreeResult, null, 2), // Pretty-print JSON
52
- contentType: "application/json",
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,
46
+ }
47
+ catch (error) {
48
+ logger.error(`Error in ${TOOL_NAME} handler`, { error, ...handlerContext });
49
+ const mcpError = ErrorHandler.handleError(error, {
50
+ operation: `tool:${TOOL_NAME}`,
51
+ context: handlerContext,
52
+ input: params,
66
53
  });
67
- });
68
- logger.info(`Tool registered: ${TOOL_NAME}`, context);
69
- }, { operation, context, critical: true });
54
+ return {
55
+ isError: true,
56
+ content: [{ type: "text", text: `Error: ${mcpError.message}` }],
57
+ structuredContent: {
58
+ code: mcpError.code,
59
+ message: mcpError.message,
60
+ details: mcpError.details,
61
+ },
62
+ };
63
+ }
64
+ });
65
+ logger.info(`Tool '${TOOL_NAME}' registered successfully.`, context);
70
66
  };
@@ -1,3 +1,5 @@
1
- export { registerGitWrapupInstructionsTool, initializeGitWrapupInstructionsStateAccessors, } from "./registration.js";
2
- // This tool now requires session-specific state accessors (getWorkingDirectory, getSessionId)
3
- // to fetch git status, so initializeGitWrapupInstructionsStateAccessors is exported for server setup.
1
+ /**
2
+ * @fileoverview Barrel file for the gitWrapupInstructions tool.
3
+ * @module src/mcp-server/tools/gitWrapupInstructions/index
4
+ */
5
+ export { registerGitWrapupInstructionsTool } from "./registration.js";