@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,19 +1,34 @@
1
- import { exec } from 'child_process';
2
- import { promisify } from 'util';
3
- import { z } from 'zod';
1
+ import { exec } from "child_process";
2
+ import { promisify } from "util";
3
+ import { z } from "zod";
4
4
  // Import utils from barrel (logger from ../utils/internal/logger.js)
5
- import { logger } from '../../../utils/index.js';
5
+ import { logger } from "../../../utils/index.js";
6
6
  // Import utils from barrel (RequestContext from ../utils/internal/requestContext.js)
7
- import { BaseErrorCode, McpError } from '../../../types-global/errors.js'; // Keep direct import for types-global
7
+ import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; // Keep direct import for types-global
8
8
  // Import utils from barrel (sanitization from ../utils/security/sanitization.js)
9
- import { sanitization } from '../../../utils/index.js';
9
+ import { sanitization } from "../../../utils/index.js";
10
10
  const execAsync = promisify(exec);
11
11
  // Define the input schema for the git_checkout tool using Zod
12
12
  export const GitCheckoutInputSchema = z.object({
13
- path: z.string().min(1).optional().default('.').describe("Path to the Git repository. Defaults to the directory set via `git_set_working_dir` for the session; set 'git_set_working_dir' if not set."),
14
- branchOrPath: z.string().min(1).describe("The branch name (e.g., 'main'), commit hash, tag, or file path(s) (e.g., './src/file.ts') to checkout."),
15
- newBranch: z.string().optional().describe("Create a new branch named <new_branch> (e.g., 'feat/new-feature') and start it at <branchOrPath>."),
16
- force: z.boolean().optional().default(false).describe("Force checkout even if there are uncommitted changes (use with caution, discards local changes)."),
13
+ path: z
14
+ .string()
15
+ .min(1)
16
+ .optional()
17
+ .default(".")
18
+ .describe("Path to the Git repository. Defaults to the directory set via `git_set_working_dir` for the session; set 'git_set_working_dir' if not set."),
19
+ branchOrPath: z
20
+ .string()
21
+ .min(1)
22
+ .describe("The branch name (e.g., 'main'), commit hash, tag, or file path(s) (e.g., './src/file.ts') to checkout."),
23
+ newBranch: z
24
+ .string()
25
+ .optional()
26
+ .describe("Create a new branch named <new_branch> (e.g., 'feat/new-feature') and start it at <branchOrPath>."),
27
+ force: z
28
+ .boolean()
29
+ .optional()
30
+ .default(false)
31
+ .describe("Force checkout even if there are uncommitted changes (use with caution, discards local changes)."),
17
32
  // Add other relevant git checkout options as needed (e.g., --track, -b for new branch shorthand)
18
33
  });
19
34
  /**
@@ -26,12 +41,12 @@ export const GitCheckoutInputSchema = z.object({
26
41
  * @throws {McpError} Throws an McpError if path resolution, validation, or the git command fails unexpectedly.
27
42
  */
28
43
  export async function checkoutGit(input, context) {
29
- const operation = 'checkoutGit';
44
+ const operation = "checkoutGit";
30
45
  logger.debug(`Executing ${operation}`, { ...context, input });
31
46
  let targetPath;
32
47
  try {
33
48
  // Resolve and sanitize the target path
34
- if (input.path && input.path !== '.') {
49
+ if (input.path && input.path !== ".") {
35
50
  targetPath = input.path;
36
51
  }
37
52
  else {
@@ -41,170 +56,95 @@ export async function checkoutGit(input, context) {
41
56
  }
42
57
  targetPath = workingDir;
43
58
  }
44
- targetPath = sanitization.sanitizePath(targetPath, { allowAbsolute: true }).sanitizedPath;
45
- logger.debug('Sanitized path', { ...context, operation, sanitizedPath: targetPath });
59
+ targetPath = sanitization.sanitizePath(targetPath, {
60
+ allowAbsolute: true,
61
+ }).sanitizedPath;
62
+ logger.debug("Sanitized path", {
63
+ ...context,
64
+ operation,
65
+ sanitizedPath: targetPath,
66
+ });
46
67
  }
47
68
  catch (error) {
48
- logger.error('Path resolution or sanitization failed', { ...context, operation, error });
69
+ logger.error("Path resolution or sanitization failed", {
70
+ ...context,
71
+ operation,
72
+ error,
73
+ });
49
74
  if (error instanceof McpError)
50
75
  throw error;
51
76
  throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
52
77
  }
53
78
  // Basic sanitization for branch/path argument
54
- const safeBranchOrPath = input.branchOrPath.replace(/[`$&;*()|<>]/g, ''); // Remove potentially dangerous characters
79
+ const safeBranchOrPath = input.branchOrPath.replace(/[`$&;*()|<>]/g, ""); // Remove potentially dangerous characters
55
80
  try {
56
81
  // Construct the git checkout command
57
82
  let command = `git -C "${targetPath}" checkout`;
58
83
  if (input.force) {
59
- command += ' --force';
84
+ command += " --force";
60
85
  }
61
86
  if (input.newBranch) {
62
- const safeNewBranch = input.newBranch.replace(/[^a-zA-Z0-9_.\-/]/g, ''); // Sanitize new branch name
87
+ const safeNewBranch = input.newBranch.replace(/[^a-zA-Z0-9_.\-/]/g, ""); // Sanitize new branch name
63
88
  command += ` -b ${safeNewBranch}`;
64
89
  }
65
90
  command += ` ${safeBranchOrPath}`; // Add the target branch/path
66
91
  logger.debug(`Executing command: ${command}`, { ...context, operation });
67
92
  // Execute command. Checkout often uses stderr for status messages.
68
93
  const { stdout, stderr } = await execAsync(command);
69
- logger.info(`Git checkout stdout: ${stdout}`, { ...context, operation });
70
- logger.info(`Git checkout stderr: ${stderr}`, { ...context, operation }); // Log stderr as info
71
- // Analyze stderr primarily, fallback to stdout
72
- let message = stderr.trim() || stdout.trim();
73
- let success = true;
74
- let previousBranch = undefined;
75
- let currentBranch = undefined;
76
- let newBranchCreated = !!input.newBranch;
77
- let filesRestored = undefined;
78
- let isDetachedHead = false;
79
- let isFileCheckout = false;
80
- // --- Initial analysis of checkout output ---
81
- // Extract previous branch if available
82
- const prevBranchMatch = stderr.match(/Switched to.*? from ['"]?(.*?)['"]?/);
83
- if (prevBranchMatch) {
84
- previousBranch = prevBranchMatch[1];
85
- }
86
- // Determine primary outcome from stderr/stdout
87
- if (stderr.includes('Switched to a new branch')) {
88
- const currentBranchMatch = stderr.match(/Switched to a new branch ['"]?(.*?)['"]?/);
89
- currentBranch = currentBranchMatch ? currentBranchMatch[1] : input.newBranch; // Use matched or input
90
- message = `Switched to new branch '${currentBranch}'.`;
91
- newBranchCreated = true;
92
- }
93
- else if (stderr.includes('Switched to branch')) {
94
- const currentBranchMatch = stderr.match(/Switched to branch ['"]?(.*?)['"]?/);
95
- currentBranch = currentBranchMatch ? currentBranchMatch[1] : input.branchOrPath; // Use matched or input
96
- message = `Switched to branch '${currentBranch}'.`;
97
- }
98
- else if (stderr.includes('Already on')) {
99
- const currentBranchMatch = stderr.match(/Already on ['"]?(.*?)['"]?/);
100
- currentBranch = currentBranchMatch ? currentBranchMatch[1] : input.branchOrPath; // Use matched or input
101
- message = `Already on '${currentBranch}'.`;
102
- }
103
- else if (stderr.includes('Updated N path') || stdout.includes('Updated N path') || stderr.includes('Your branch is up to date with')) { // Checking out files or confirming current state
104
- // Check if the input looks like file paths rather than a branch/commit
105
- // This is heuristic - might need refinement if branch names look like paths
106
- if (input.branchOrPath.includes('/') || input.branchOrPath.includes('.')) {
107
- isFileCheckout = true;
108
- message = `Restored or checked path(s): ${input.branchOrPath}`;
109
- filesRestored = input.branchOrPath.split('\n').map(p => p.trim()).filter(p => p.length > 0);
110
- }
111
- else {
112
- // Assume it was just confirming the current branch state
113
- message = stderr.trim() || stdout.trim() || `Checked out ${input.branchOrPath}.`;
114
- }
115
- }
116
- else if (stderr.includes('Previous HEAD position was') && stderr.includes('HEAD is now at')) { // Detached HEAD
117
- message = `Checked out commit ${input.branchOrPath} (Detached HEAD state).`;
118
- currentBranch = 'Detached HEAD';
119
- isDetachedHead = true;
120
- }
121
- else if (stderr.includes('Note: switching to') || stderr.includes('Note: checking out')) { // Other detached HEAD variants
122
- message = `Checked out ${input.branchOrPath} (Detached HEAD state).`;
123
- currentBranch = 'Detached HEAD';
124
- isDetachedHead = true;
125
- }
126
- else if (message.includes('fatal:')) {
127
- success = false;
128
- message = `Checkout failed: ${message}`;
129
- logger.error(`Git checkout command indicated failure: ${message}`, { ...context, operation, stdout, stderr });
130
- }
131
- else if (!message && !stdout && !stderr) {
132
- message = 'Checkout command executed silently.'; // Assume success, will verify branch below
133
- logger.info(message, { ...context, operation });
134
- }
135
- else {
136
- // Some other message, treat as informational for now
137
- message = stderr.trim() || stdout.trim();
138
- logger.info(`Git checkout produced message: ${message}`, { ...context, operation });
139
- }
140
- // --- Get definitive current branch IF checkout was successful AND not file checkout/detached HEAD ---
141
- if (success && !isFileCheckout && !isDetachedHead) {
142
- try {
143
- logger.debug('Attempting to get current branch via git branch --show-current', { ...context, operation });
144
- const statusResult = await execAsync(`git -C "${targetPath}" branch --show-current`);
145
- const definitiveCurrentBranch = statusResult.stdout.trim();
146
- if (definitiveCurrentBranch) {
147
- currentBranch = definitiveCurrentBranch;
148
- logger.info(`Confirmed current branch: ${currentBranch}`, { ...context, operation });
149
- // Refine message if it wasn't specific before
150
- if (message.startsWith('Checkout command executed silently') || message.startsWith('Checked out ')) {
151
- message = `Checked out '${currentBranch}'.`;
152
- }
153
- else if (message.startsWith('Already on') && !message.includes(`'${currentBranch}'`)) {
154
- message = `Already on '${currentBranch}'.`; // Update if initial parse was wrong
155
- }
156
- else if (message.startsWith('Switched to branch') && !message.includes(`'${currentBranch}'`)) {
157
- message = `Switched to branch '${currentBranch}'.`; // Update if initial parse was wrong
158
- }
159
- }
160
- else {
161
- // Command succeeded but returned empty - might be detached HEAD after all?
162
- logger.warning('git branch --show-current returned empty, possibly detached HEAD?', { ...context, operation });
163
- // Keep potentially parsed 'Detached HEAD' or fallback to input if needed
164
- currentBranch = currentBranch || 'Unknown (possibly detached)';
165
- if (!message.includes('Detached HEAD'))
166
- message += ' (Could not confirm branch name).';
167
- }
168
- }
169
- catch (statusError) {
170
- logger.warning('Could not determine current branch after checkout', { ...context, operation, error: statusError.message });
171
- // Keep potentially parsed 'Detached HEAD' or fallback to input if needed
172
- currentBranch = currentBranch || 'Unknown (error checking)';
173
- if (!message.includes('Detached HEAD'))
174
- message += ' (Error checking branch name).';
175
- }
176
- }
177
- else if (success && isFileCheckout) {
178
- // If it was a file checkout, still try to get the branch name for context
179
- try {
180
- const statusResult = await execAsync(`git -C "${targetPath}" branch --show-current`);
181
- currentBranch = statusResult.stdout.trim() || 'Unknown (possibly detached)';
182
- }
183
- catch {
184
- currentBranch = 'Unknown (error checking)';
185
- }
186
- logger.info(`Current branch after file checkout: ${currentBranch}`, { ...context, operation });
187
- }
188
- logger.info(`${operation} completed`, { ...context, operation, path: targetPath, success, message, currentBranch });
189
- return { success, message, previousBranch, currentBranch, newBranchCreated, filesRestored };
94
+ const message = stderr.trim() || stdout.trim();
95
+ logger.debug(`Git checkout stdout: ${stdout}`, { ...context, operation });
96
+ if (stderr) {
97
+ logger.debug(`Git checkout stderr: ${stderr}`, { ...context, operation });
98
+ }
99
+ // Get the current branch name after the checkout operation
100
+ let currentBranch;
101
+ try {
102
+ const { stdout: branchStdout } = await execAsync(`git -C "${targetPath}" branch --show-current`);
103
+ currentBranch = branchStdout.trim();
104
+ }
105
+ catch (e) {
106
+ // This can fail in detached HEAD state, which is not an error for checkout
107
+ currentBranch = "Detached HEAD";
108
+ }
109
+ const result = {
110
+ success: true,
111
+ message,
112
+ currentBranch,
113
+ newBranchCreated: !!input.newBranch,
114
+ };
115
+ logger.info("git checkout executed successfully", {
116
+ ...context,
117
+ operation,
118
+ path: targetPath,
119
+ result,
120
+ });
121
+ return result;
190
122
  }
191
123
  catch (error) {
192
- logger.error(`Failed to execute git checkout command`, { ...context, operation, path: targetPath, error: error.message, stderr: error.stderr, stdout: error.stdout });
193
- const errorMessage = error.stderr || error.stdout || error.message || '';
124
+ logger.error(`Failed to execute git checkout command`, {
125
+ ...context,
126
+ operation,
127
+ path: targetPath,
128
+ error: error.message,
129
+ stderr: error.stderr,
130
+ stdout: error.stdout,
131
+ });
132
+ const errorMessage = error.stderr || error.stdout || error.message || "";
194
133
  // Handle specific error cases
195
- if (errorMessage.toLowerCase().includes('not a git repository')) {
134
+ if (errorMessage.toLowerCase().includes("not a git repository")) {
196
135
  throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`, { context, operation, originalError: error });
197
136
  }
198
137
  if (errorMessage.match(/pathspec '.*?' did not match any file\(s\) known to git/)) {
199
138
  throw new McpError(BaseErrorCode.NOT_FOUND, `Branch or pathspec not found: ${input.branchOrPath}. Error: ${errorMessage}`, { context, operation, originalError: error });
200
139
  }
201
- if (errorMessage.includes('already exists')) { // e.g., trying -b with existing branch name
140
+ if (errorMessage.includes("already exists")) {
141
+ // e.g., trying -b with existing branch name
202
142
  throw new McpError(BaseErrorCode.CONFLICT, `Cannot create new branch '${input.newBranch}': it already exists. Error: ${errorMessage}`, { context, operation, originalError: error });
203
143
  }
204
- if (errorMessage.includes('Your local changes to the following files would be overwritten by checkout')) {
144
+ if (errorMessage.includes("Your local changes to the following files would be overwritten by checkout")) {
205
145
  throw new McpError(BaseErrorCode.CONFLICT, `Checkout failed due to uncommitted local changes that would be overwritten. Please commit or stash them first, or use --force. Error: ${errorMessage}`, { context, operation, originalError: error });
206
146
  }
207
- if (errorMessage.includes('invalid reference')) {
147
+ if (errorMessage.includes("invalid reference")) {
208
148
  throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid branch name or reference: ${input.branchOrPath}. Error: ${errorMessage}`, { context, operation, originalError: error });
209
149
  }
210
150
  // Generic internal error for other failures
@@ -1,11 +1,11 @@
1
1
  // Import utils from barrel (ErrorHandler from ../utils/internal/errorHandler.js)
2
- import { ErrorHandler } from '../../../utils/index.js';
2
+ import { ErrorHandler } from "../../../utils/index.js";
3
3
  // Import utils from barrel (logger from ../utils/internal/logger.js)
4
- import { logger } from '../../../utils/index.js';
4
+ import { logger } from "../../../utils/index.js";
5
5
  // Import utils from barrel (requestContextService, RequestContext from ../utils/internal/requestContext.js)
6
- import { BaseErrorCode } from '../../../types-global/errors.js'; // Keep direct import for types-global
7
- import { requestContextService } from '../../../utils/index.js';
8
- import { checkoutGit, GitCheckoutInputSchema } from './logic.js';
6
+ import { BaseErrorCode } from "../../../types-global/errors.js"; // Keep direct import for types-global
7
+ import { requestContextService } from "../../../utils/index.js";
8
+ import { checkoutGit, GitCheckoutInputSchema, } from "./logic.js";
9
9
  let _getWorkingDirectory;
10
10
  let _getSessionId;
11
11
  /**
@@ -17,9 +17,9 @@ let _getSessionId;
17
17
  export function initializeGitCheckoutStateAccessors(getWdFn, getSidFn) {
18
18
  _getWorkingDirectory = getWdFn;
19
19
  _getSessionId = getSidFn;
20
- logger.info('State accessors initialized for git_checkout tool registration.');
20
+ logger.info("State accessors initialized for git_checkout tool registration.");
21
21
  }
22
- const TOOL_NAME = 'git_checkout';
22
+ const TOOL_NAME = "git_checkout";
23
23
  const TOOL_DESCRIPTION = "Switches branches or restores working tree files. Can checkout branches, commits, tags, or specific file paths. Supports creating new branches and forcing checkout.";
24
24
  /**
25
25
  * Registers the git_checkout tool with the MCP server.
@@ -29,15 +29,18 @@ const TOOL_DESCRIPTION = "Switches branches or restores working tree files. Can
29
29
  */
30
30
  export async function registerGitCheckoutTool(server) {
31
31
  if (!_getWorkingDirectory || !_getSessionId) {
32
- throw new Error('State accessors for git_checkout must be initialized before registration.');
32
+ throw new Error("State accessors for git_checkout must be initialized before registration.");
33
33
  }
34
- const operation = 'registerGitCheckoutTool';
34
+ const operation = "registerGitCheckoutTool";
35
35
  const context = requestContextService.createRequestContext({ operation });
36
36
  await ErrorHandler.tryCatch(async () => {
37
37
  server.tool(TOOL_NAME, TOOL_DESCRIPTION, GitCheckoutInputSchema.shape, // Provide the Zod schema shape
38
38
  async (validatedArgs, callContext) => {
39
- const toolOperation = 'tool:git_checkout';
40
- const requestContext = requestContextService.createRequestContext({ operation: toolOperation, parentContext: callContext });
39
+ const toolOperation = "tool:git_checkout";
40
+ const requestContext = requestContextService.createRequestContext({
41
+ operation: toolOperation,
42
+ parentContext: callContext,
43
+ });
41
44
  const sessionId = _getSessionId(requestContext);
42
45
  const getWorkingDirectoryForSession = () => {
43
46
  return _getWorkingDirectory(sessionId);
@@ -53,9 +56,9 @@ export async function registerGitCheckoutTool(server) {
53
56
  const checkoutResult = await checkoutGit(validatedArgs, logicContext);
54
57
  // Format the result as a JSON string within TextContent
55
58
  const resultContent = {
56
- type: 'text',
59
+ type: "text",
57
60
  text: JSON.stringify(checkoutResult, null, 2), // Pretty-print JSON
58
- contentType: 'application/json',
61
+ contentType: "application/json",
59
62
  };
60
63
  // Log based on the success flag in the result
61
64
  if (checkoutResult.success) {
@@ -78,4 +81,3 @@ export async function registerGitCheckoutTool(server) {
78
81
  logger.info(`Tool registered: ${TOOL_NAME}`, context);
79
82
  }, { operation, context, critical: true });
80
83
  }
81
- ;
@@ -2,6 +2,6 @@
2
2
  * @fileoverview Barrel file for the git_cherry_pick tool.
3
3
  * Exports the registration function and state accessor initialization function.
4
4
  */
5
- export { registerGitCherryPickTool, initializeGitCherryPickStateAccessors } from './registration.js';
5
+ export { registerGitCherryPickTool, initializeGitCherryPickStateAccessors, } from "./registration.js";
6
6
  // Export types if needed elsewhere, e.g.:
7
7
  // export type { GitCherryPickInput, GitCherryPickResult } from './logic.js';
@@ -1,21 +1,43 @@
1
- import { exec } from 'child_process';
2
- import { promisify } from 'util';
3
- import { z } from 'zod';
1
+ import { exec } from "child_process";
2
+ import { promisify } from "util";
3
+ import { z } from "zod";
4
4
  // Import utils from barrel (logger from ../utils/internal/logger.js)
5
- import { logger } from '../../../utils/index.js';
5
+ import { logger } from "../../../utils/index.js";
6
6
  // Import utils from barrel (RequestContext from ../utils/internal/requestContext.js)
7
- import { BaseErrorCode, McpError } from '../../../types-global/errors.js'; // Keep direct import for types-global
7
+ import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; // Keep direct import for types-global
8
8
  // Import utils from barrel (sanitization from ../utils/security/sanitization.js)
9
- import { sanitization } from '../../../utils/index.js';
9
+ import { sanitization } from "../../../utils/index.js";
10
10
  const execAsync = promisify(exec);
11
11
  // Define the input schema for the git_cherry-pick tool using Zod
12
12
  export const GitCherryPickInputSchema = z.object({
13
- 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."),
14
- commitRef: z.string().min(1).describe("The commit reference(s) to cherry-pick (e.g., 'hash1', 'hash1..hash3', 'branchName~3..branchName')."),
15
- mainline: z.number().int().min(1).optional().describe("Specify the parent number (starting from 1) when cherry-picking a merge commit."),
16
- strategy: z.enum(['recursive', 'resolve', 'ours', 'theirs', 'octopus', 'subtree']).optional().describe("Specifies a merge strategy *option* (passed via -X)."),
17
- noCommit: z.boolean().default(false).describe("Apply the changes but do not create a commit."),
18
- signoff: z.boolean().default(false).describe("Add a Signed-off-by line to the commit message."),
13
+ path: z
14
+ .string()
15
+ .min(1)
16
+ .optional()
17
+ .default(".")
18
+ .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."),
19
+ commitRef: z
20
+ .string()
21
+ .min(1)
22
+ .describe("The commit reference(s) to cherry-pick (e.g., 'hash1', 'hash1..hash3', 'branchName~3..branchName')."),
23
+ mainline: z
24
+ .number()
25
+ .int()
26
+ .min(1)
27
+ .optional()
28
+ .describe("Specify the parent number (starting from 1) when cherry-picking a merge commit."),
29
+ strategy: z
30
+ .enum(["recursive", "resolve", "ours", "theirs", "octopus", "subtree"])
31
+ .optional()
32
+ .describe("Specifies a merge strategy *option* (passed via -X)."),
33
+ noCommit: z
34
+ .boolean()
35
+ .default(false)
36
+ .describe("Apply the changes but do not create a commit."),
37
+ signoff: z
38
+ .boolean()
39
+ .default(false)
40
+ .describe("Add a Signed-off-by line to the commit message."),
19
41
  // Add options for conflict handling? (e.g., --continue, --abort, --skip) - Maybe separate tool or mode?
20
42
  });
21
43
  /**
@@ -27,31 +49,47 @@ export const GitCherryPickInputSchema = z.object({
27
49
  * @throws {McpError} Throws an McpError for path resolution/validation failures or unexpected errors.
28
50
  */
29
51
  export async function gitCherryPickLogic(input, context) {
30
- const operation = 'gitCherryPickLogic';
52
+ const operation = "gitCherryPickLogic";
31
53
  logger.debug(`Executing ${operation}`, { ...context, input });
32
54
  let targetPath;
33
55
  try {
34
56
  // Resolve and sanitize the target path
35
57
  const workingDir = context.getWorkingDirectory();
36
- targetPath = (input.path && input.path !== '.')
37
- ? input.path
38
- : workingDir ?? '.';
39
- if (targetPath === '.' && !workingDir) {
58
+ targetPath =
59
+ input.path && input.path !== "." ? input.path : (workingDir ?? ".");
60
+ if (targetPath === "." && !workingDir) {
40
61
  logger.warning("Executing git cherry-pick in server's CWD as no path provided and no session WD set.", { ...context, operation });
41
62
  targetPath = process.cwd();
42
63
  }
43
- else if (targetPath === '.' && workingDir) {
64
+ else if (targetPath === "." && workingDir) {
44
65
  targetPath = workingDir;
45
- logger.debug(`Using session working directory: ${targetPath}`, { ...context, operation, sessionId: context.sessionId });
66
+ logger.debug(`Using session working directory: ${targetPath}`, {
67
+ ...context,
68
+ operation,
69
+ sessionId: context.sessionId,
70
+ });
46
71
  }
47
72
  else {
48
- logger.debug(`Using provided path: ${targetPath}`, { ...context, operation });
73
+ logger.debug(`Using provided path: ${targetPath}`, {
74
+ ...context,
75
+ operation,
76
+ });
49
77
  }
50
- targetPath = sanitization.sanitizePath(targetPath, { allowAbsolute: true }).sanitizedPath;
51
- logger.debug('Sanitized path', { ...context, operation, sanitizedPath: targetPath });
78
+ targetPath = sanitization.sanitizePath(targetPath, {
79
+ allowAbsolute: true,
80
+ }).sanitizedPath;
81
+ logger.debug("Sanitized path", {
82
+ ...context,
83
+ operation,
84
+ sanitizedPath: targetPath,
85
+ });
52
86
  }
53
87
  catch (error) {
54
- logger.error('Path resolution or sanitization failed', { ...context, operation, error });
88
+ logger.error("Path resolution or sanitization failed", {
89
+ ...context,
90
+ operation,
91
+ error,
92
+ });
55
93
  if (error instanceof McpError)
56
94
  throw error;
57
95
  throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
@@ -63,9 +101,9 @@ export async function gitCherryPickLogic(input, context) {
63
101
  if (input.strategy)
64
102
  command += ` -X${input.strategy}`; // Note: -X for strategy options
65
103
  if (input.noCommit)
66
- command += ' --no-commit';
104
+ command += " --no-commit";
67
105
  if (input.signoff)
68
- command += ' --signoff';
106
+ command += " --signoff";
69
107
  // Add the commit reference(s) - ensure it's treated as a single argument potentially containing special chars like '..'
70
108
  command += ` "${input.commitRef.replace(/"/g, '\\"')}"`;
71
109
  logger.debug(`Executing command: ${command}`, { ...context, operation });
@@ -77,42 +115,63 @@ export async function gitCherryPickLogic(input, context) {
77
115
  const commitCreated = !input.noCommit && !conflicts;
78
116
  const message = conflicts
79
117
  ? `Cherry-pick resulted in conflicts for commit(s) '${input.commitRef}'. Manual resolution required.`
80
- : `Successfully cherry-picked commit(s) '${input.commitRef}'.` + (commitCreated ? ' New commit created.' : (input.noCommit ? ' Changes staged.' : ''));
81
- logger.info(message, { ...context, operation, path: targetPath, conflicts, commitCreated });
118
+ : `Successfully cherry-picked commit(s) '${input.commitRef}'.` +
119
+ (commitCreated
120
+ ? " New commit created."
121
+ : input.noCommit
122
+ ? " Changes staged."
123
+ : "");
124
+ logger.info("git cherry-pick executed successfully", {
125
+ ...context,
126
+ operation,
127
+ path: targetPath,
128
+ result: { message, conflicts, commitCreated },
129
+ });
82
130
  return { success: true, message, commitCreated, conflicts };
83
131
  }
84
132
  catch (cherryPickError) {
85
- const errorMessage = cherryPickError.stderr || cherryPickError.stdout || cherryPickError.message || '';
133
+ const errorMessage = cherryPickError.stderr ||
134
+ cherryPickError.stdout ||
135
+ cherryPickError.message ||
136
+ "";
86
137
  if (/conflict/i.test(errorMessage)) {
87
138
  logger.warning(`Cherry-pick failed due to conflicts for commit(s) '${input.commitRef}'.`, { ...context, operation, path: targetPath, error: errorMessage });
88
- return { success: false, message: `Failed to cherry-pick commit(s) '${input.commitRef}' due to conflicts. Resolve conflicts manually and potentially use 'git cherry-pick --continue' or '--abort'.`, error: errorMessage, conflicts: true };
139
+ return {
140
+ success: false,
141
+ message: `Failed to cherry-pick commit(s) '${input.commitRef}' due to conflicts. Resolve conflicts manually and potentially use 'git cherry-pick --continue' or '--abort'.`,
142
+ error: errorMessage,
143
+ conflicts: true,
144
+ };
89
145
  }
90
146
  // Rethrow other errors to be caught by the outer try-catch
91
147
  throw cherryPickError;
92
148
  }
93
149
  }
94
150
  catch (error) {
95
- const errorMessage = error.stderr || error.stdout || error.message || '';
96
- logger.error(`Failed to execute git cherry-pick command`, { ...context, operation, path: targetPath, error: errorMessage, stderr: error.stderr, stdout: error.stdout });
151
+ const errorMessage = error.stderr || error.stdout || error.message || "";
152
+ logger.error(`Failed to execute git cherry-pick command`, {
153
+ ...context,
154
+ operation,
155
+ path: targetPath,
156
+ error: errorMessage,
157
+ stderr: error.stderr,
158
+ stdout: error.stdout,
159
+ });
97
160
  // Specific error handling
98
- if (errorMessage.toLowerCase().includes('not a git repository')) {
161
+ if (errorMessage.toLowerCase().includes("not a git repository")) {
99
162
  throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`, { context, operation, originalError: error });
100
163
  }
101
164
  if (/bad revision/i.test(errorMessage)) {
102
- return { success: false, message: `Failed to cherry-pick: Invalid commit reference '${input.commitRef}'.`, error: errorMessage };
165
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Failed to cherry-pick: Invalid commit reference '${input.commitRef}'. Error: ${errorMessage}`, { context, operation, originalError: error });
103
166
  }
104
167
  if (/after resolving the conflicts/i.test(errorMessage)) {
105
168
  // This might indicate a previous conflict state
106
- return { success: false, message: `Failed to cherry-pick: Unresolved conflicts from a previous operation exist. Resolve conflicts and use 'git cherry-pick --continue' or '--abort'.`, error: errorMessage, conflicts: true };
169
+ throw new McpError(BaseErrorCode.CONFLICT, `Failed to cherry-pick: Unresolved conflicts from a previous operation exist. Resolve conflicts and use 'git cherry-pick --continue' or '--abort'. Error: ${errorMessage}`, { context, operation, originalError: error });
107
170
  }
108
171
  if (/your local changes would be overwritten/i.test(errorMessage)) {
109
- return { success: false, message: `Failed to cherry-pick: Your local changes to tracked files would be overwritten. Please commit or stash them.`, error: errorMessage };
172
+ throw new McpError(BaseErrorCode.CONFLICT, `Failed to cherry-pick: Your local changes to tracked files would be overwritten. Please commit or stash them. Error: ${errorMessage}`, { context, operation, originalError: error });
110
173
  }
111
- // Return structured failure for other git errors
112
- return {
113
- success: false,
114
- message: `Git cherry-pick failed for path: ${targetPath}.`,
115
- error: errorMessage
116
- };
174
+ // Throw a generic McpError for other failures
175
+ throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Git cherry-pick failed for path: ${targetPath}. Error: ${errorMessage}`, { context, operation, originalError: error });
117
176
  }
118
177
  }