@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.
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 +17 -74
  5. package/dist/mcp-server/tools/gitAdd/registration.js +38 -59
  6. package/dist/mcp-server/tools/gitBranch/index.js +3 -5
  7. package/dist/mcp-server/tools/gitBranch/logic.js +118 -296
  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 -122
  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 +55 -162
  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 +44 -143
  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 +19 -26
  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 +50 -171
  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 +90 -295
  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 +78 -254
  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 +47 -129
  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 +46 -152
  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 +75 -257
  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 +52 -179
  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 +48 -146
  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 +73 -181
  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 +73 -202
  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 +85 -193
  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 +37 -121
  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 +45 -133
  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 +33 -122
  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 +70 -214
  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 +82 -229
  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 +66 -188
  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 +112 -322
  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 +26 -38
  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,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
- * 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
128
  }
326
- logger.info(`git worktree ${input.mode} executed successfully`, {
327
- ...context,
328
- operation,
329
- path: targetPath,
330
- result,
331
- });
332
- return result;
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.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
- });
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}`, { 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 });
144
+ throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`);
352
145
  }
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 });
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 (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 });
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
- // 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 });
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
- * 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 { 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 {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.
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
- 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,
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
- const sessionId = _getSessionId(requestContext);
40
- const getWorkingDirectoryForSession = () => _getWorkingDirectory(sessionId);
41
- const logicContext = {
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
- 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,
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
- logger.info(`Tool registered: ${TOOL_NAME}`, context);
69
- }, { operation, context, critical: true });
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
- 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";