@cyanheads/git-mcp-server 2.1.0 → 2.1.2

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