@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,27 +1,49 @@
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 base input schema without refinement
12
12
  const GitDiffInputBaseSchema = 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
- commit1: z.string().optional().describe("First commit, branch, or ref for comparison. If omitted, compares against the working tree or index (depending on 'staged')."),
15
- commit2: z.string().optional().describe("Second commit, branch, or ref for comparison. If omitted, compares commit1 against the working tree or index."),
16
- staged: z.boolean().optional().default(false).describe("Show diff of changes staged for the next commit (compares index against HEAD). Overrides commit1/commit2 if true."),
17
- file: z.string().optional().describe("Limit the diff output to a specific file path."),
18
- includeUntracked: z.boolean().optional().default(false).describe("Include untracked files in the diff output (shows their full content as new files). This is a non-standard extension."),
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
+ commit1: z
20
+ .string()
21
+ .optional()
22
+ .describe("First commit, branch, or ref for comparison. If omitted, compares against the working tree or index (depending on 'staged')."),
23
+ commit2: z
24
+ .string()
25
+ .optional()
26
+ .describe("Second commit, branch, or ref for comparison. If omitted, compares commit1 against the working tree or index."),
27
+ staged: z
28
+ .boolean()
29
+ .optional()
30
+ .default(false)
31
+ .describe("Show diff of changes staged for the next commit (compares index against HEAD). Overrides commit1/commit2 if true."),
32
+ file: z
33
+ .string()
34
+ .optional()
35
+ .describe("Limit the diff output to a specific file path."),
36
+ includeUntracked: z
37
+ .boolean()
38
+ .optional()
39
+ .default(false)
40
+ .describe("Include untracked files in the diff output (shows their full content as new files). This is a non-standard extension."),
19
41
  // Add options like --name-only, --stat, context lines (-U<n>) if needed
20
42
  });
21
43
  // Export the shape for registration
22
44
  export const GitDiffInputShape = GitDiffInputBaseSchema.shape;
23
45
  // Define the final schema with refinement for validation during execution
24
- export const GitDiffInputSchema = GitDiffInputBaseSchema.refine(data => !(data.staged && (data.commit1 || data.commit2)), {
46
+ export const GitDiffInputSchema = GitDiffInputBaseSchema.refine((data) => !(data.staged && (data.commit1 || data.commit2)), {
25
47
  message: "Cannot use 'staged' option with specific commit references (commit1 or commit2).",
26
48
  path: ["staged", "commit1", "commit2"], // Indicate related fields
27
49
  });
@@ -34,12 +56,12 @@ export const GitDiffInputSchema = GitDiffInputBaseSchema.refine(data => !(data.s
34
56
  * @throws {McpError} Throws an McpError if path resolution, validation, or the git command fails unexpectedly.
35
57
  */
36
58
  export async function diffGitChanges(input, context) {
37
- const operation = 'diffGitChanges';
59
+ const operation = "diffGitChanges";
38
60
  logger.debug(`Executing ${operation}`, { ...context, input });
39
61
  let targetPath;
40
62
  try {
41
63
  // Resolve and sanitize the target path
42
- if (input.path && input.path !== '.') {
64
+ if (input.path && input.path !== ".") {
43
65
  targetPath = input.path;
44
66
  }
45
67
  else {
@@ -49,26 +71,36 @@ export async function diffGitChanges(input, context) {
49
71
  }
50
72
  targetPath = workingDir;
51
73
  }
52
- targetPath = sanitization.sanitizePath(targetPath, { allowAbsolute: true }).sanitizedPath;
53
- logger.debug('Sanitized path', { ...context, operation, sanitizedPath: targetPath });
74
+ targetPath = sanitization.sanitizePath(targetPath, {
75
+ allowAbsolute: true,
76
+ }).sanitizedPath;
77
+ logger.debug("Sanitized path", {
78
+ ...context,
79
+ operation,
80
+ sanitizedPath: targetPath,
81
+ });
54
82
  }
55
83
  catch (error) {
56
- logger.error('Path resolution or sanitization failed', { ...context, operation, error });
84
+ logger.error("Path resolution or sanitization failed", {
85
+ ...context,
86
+ operation,
87
+ error,
88
+ });
57
89
  if (error instanceof McpError)
58
90
  throw error;
59
91
  throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
60
92
  }
61
93
  // Basic sanitization for refs and file path
62
- const safeCommit1 = input.commit1?.replace(/[`$&;*()|<>]/g, '');
63
- const safeCommit2 = input.commit2?.replace(/[`$&;*()|<>]/g, '');
64
- const safeFile = input.file?.replace(/[`$&;*()|<>]/g, '');
65
- let untrackedFilesDiff = '';
94
+ const safeCommit1 = input.commit1?.replace(/[`$&;*()|<>]/g, "");
95
+ const safeCommit2 = input.commit2?.replace(/[`$&;*()|<>]/g, "");
96
+ const safeFile = input.file?.replace(/[`$&;*()|<>]/g, "");
97
+ let untrackedFilesDiff = "";
66
98
  let untrackedFilesCount = 0;
67
99
  try {
68
100
  // Construct the standard git diff command
69
101
  let standardDiffCommand = `git -C "${targetPath}" diff`;
70
102
  if (input.staged) {
71
- standardDiffCommand += ' --staged'; // Or --cached
103
+ standardDiffCommand += " --staged"; // Or --cached
72
104
  }
73
105
  else {
74
106
  // Add commit references if not doing staged diff
@@ -85,25 +117,38 @@ export async function diffGitChanges(input, context) {
85
117
  if (safeFile) {
86
118
  standardDiffCommand += ` -- "${safeFile}"`; // Use '--' to separate paths from revisions
87
119
  }
88
- logger.debug(`Executing standard diff command: ${standardDiffCommand}`, { ...context, operation });
120
+ logger.debug(`Executing standard diff command: ${standardDiffCommand}`, {
121
+ ...context,
122
+ operation,
123
+ });
89
124
  const { stdout: standardStdout, stderr: standardStderr } = await execAsync(standardDiffCommand, { maxBuffer: 1024 * 1024 * 20 });
90
125
  if (standardStderr) {
91
- logger.warning(`Git diff (standard) stderr: ${standardStderr}`, { ...context, operation });
126
+ logger.warning(`Git diff (standard) stderr: ${standardStderr}`, {
127
+ ...context,
128
+ operation,
129
+ });
92
130
  }
93
131
  let combinedDiffOutput = standardStdout;
94
132
  // Handle untracked files if requested
95
133
  if (input.includeUntracked) {
96
- logger.debug('Including untracked files.', { ...context, operation });
134
+ logger.debug("Including untracked files.", { ...context, operation });
97
135
  const listUntrackedCommand = `git -C "${targetPath}" ls-files --others --exclude-standard`;
98
136
  try {
99
137
  const { stdout: untrackedFilesStdOut } = await execAsync(listUntrackedCommand);
100
- const untrackedFiles = untrackedFilesStdOut.trim().split('\n').filter(f => f); // Filter out empty lines
138
+ const untrackedFiles = untrackedFilesStdOut
139
+ .trim()
140
+ .split("\n")
141
+ .filter((f) => f); // Filter out empty lines
101
142
  if (untrackedFiles.length > 0) {
102
- logger.info(`Found ${untrackedFiles.length} untracked files.`, { ...context, operation, untrackedFiles });
103
- let individualUntrackedDiffs = '';
143
+ logger.info(`Found ${untrackedFiles.length} untracked files.`, {
144
+ ...context,
145
+ operation,
146
+ untrackedFiles,
147
+ });
148
+ let individualUntrackedDiffs = "";
104
149
  for (const untrackedFile of untrackedFiles) {
105
150
  // Sanitize each untracked file path before using in command
106
- const safeUntrackedFile = untrackedFile.replace(/[`$&;*()|<>]/g, '');
151
+ const safeUntrackedFile = untrackedFile.replace(/[`$&;*()|<>]/g, "");
107
152
  // Skip if file path becomes empty after sanitization (unlikely but safe)
108
153
  if (!safeUntrackedFile)
109
154
  continue;
@@ -127,7 +172,16 @@ export async function diffGitChanges(input, context) {
127
172
  }
128
173
  else {
129
174
  // If stdout is empty, then it's a more genuine failure.
130
- logger.warning(`Failed to diff untracked file: ${safeUntrackedFile}. Error: ${untrackedError.message}`, { ...context, operation, file: safeUntrackedFile, errorDetails: { stderr: untrackedError.stderr, stdout: untrackedError.stdout, code: untrackedError.code } });
175
+ logger.warning(`Failed to diff untracked file: ${safeUntrackedFile}. Error: ${untrackedError.message}`, {
176
+ ...context,
177
+ operation,
178
+ file: safeUntrackedFile,
179
+ errorDetails: {
180
+ stderr: untrackedError.stderr,
181
+ stdout: untrackedError.stdout,
182
+ code: untrackedError.code,
183
+ },
184
+ });
131
185
  individualUntrackedDiffs += `\n--- Diff for untracked file ${safeUntrackedFile} failed: ${untrackedError.message}\n`;
132
186
  }
133
187
  }
@@ -135,43 +189,69 @@ export async function diffGitChanges(input, context) {
135
189
  if (individualUntrackedDiffs) {
136
190
  // Add a separator if standard diff also had output
137
191
  if (combinedDiffOutput.trim()) {
138
- combinedDiffOutput += '\n';
192
+ combinedDiffOutput += "\n";
139
193
  }
140
194
  combinedDiffOutput += individualUntrackedDiffs;
141
195
  }
142
196
  }
143
197
  else {
144
- logger.info('No untracked files found.', { ...context, operation });
198
+ logger.info("No untracked files found.", { ...context, operation });
145
199
  }
146
200
  }
147
201
  catch (lsFilesError) {
148
- logger.warning(`Failed to list untracked files. Error: ${lsFilesError.message}`, { ...context, operation, error: lsFilesError.stderr || lsFilesError.stdout });
202
+ logger.warning(`Failed to list untracked files. Error: ${lsFilesError.message}`, {
203
+ ...context,
204
+ operation,
205
+ error: lsFilesError.stderr || lsFilesError.stdout,
206
+ });
149
207
  // Proceed without untracked files if listing fails
150
208
  }
151
209
  }
152
- const isNoChanges = combinedDiffOutput.trim() === '';
153
- const finalDiffOutput = isNoChanges ? 'No changes found.' : combinedDiffOutput;
154
- let message = isNoChanges ? 'No changes found.' : 'Diff generated successfully.';
210
+ const isNoChanges = combinedDiffOutput.trim() === "";
211
+ const finalDiffOutput = isNoChanges
212
+ ? "No changes found."
213
+ : combinedDiffOutput;
214
+ let message = isNoChanges
215
+ ? "No changes found."
216
+ : "Diff generated successfully.";
155
217
  if (untrackedFilesCount > 0) {
156
218
  message += ` Included ${untrackedFilesCount} untracked file(s).`;
157
219
  }
158
- logger.info(`${operation} completed successfully. ${message}`, { ...context, operation, path: targetPath });
159
- return { success: true, diff: finalDiffOutput, message, untrackedFilesProcessed: untrackedFilesCount };
220
+ logger.info(message, {
221
+ ...context,
222
+ operation,
223
+ path: targetPath,
224
+ untrackedFilesProcessed: untrackedFilesCount,
225
+ });
226
+ return {
227
+ success: true,
228
+ diff: finalDiffOutput,
229
+ message,
230
+ untrackedFilesProcessed: untrackedFilesCount,
231
+ };
160
232
  }
161
233
  catch (error) {
162
234
  // This catch block now primarily handles errors from the *standard* diff command
163
235
  // or catastrophic failures before/after untracked file processing.
164
- logger.error(`Failed to execute git diff operation`, { ...context, operation, path: targetPath, error: error.message, stderr: error.stderr, stdout: error.stdout });
165
- const errorMessage = error.stderr || error.stdout || error.message || '';
236
+ logger.error(`Failed to execute git diff operation`, {
237
+ ...context,
238
+ operation,
239
+ path: targetPath,
240
+ error: error.message,
241
+ stderr: error.stderr,
242
+ stdout: error.stdout,
243
+ });
244
+ const errorMessage = error.stderr || error.stdout || error.message || "";
166
245
  // Handle specific error cases
167
- if (errorMessage.toLowerCase().includes('not a git repository')) {
246
+ if (errorMessage.toLowerCase().includes("not a git repository")) {
168
247
  throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`, { context, operation, originalError: error });
169
248
  }
170
- if (errorMessage.includes('fatal: bad object') || errorMessage.includes('unknown revision or path not in the working tree')) {
249
+ if (errorMessage.includes("fatal: bad object") ||
250
+ errorMessage.includes("unknown revision or path not in the working tree")) {
171
251
  const invalidRef = input.commit1 || input.commit2 || input.file;
172
252
  throw new McpError(BaseErrorCode.NOT_FOUND, `Invalid commit reference or file path specified: '${invalidRef}'. Error: ${errorMessage}`, { context, operation, originalError: error });
173
253
  }
174
- if (errorMessage.includes('ambiguous argument')) {
254
+ if (errorMessage.includes("ambiguous argument")) {
175
255
  const ambiguousArg = input.commit1 || input.commit2 || input.file;
176
256
  throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Ambiguous argument provided: '${ambiguousArg}'. Error: ${errorMessage}`, { context, operation, originalError: error });
177
257
  }
@@ -1,12 +1,12 @@
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 { requestContextService } from '../../../utils/index.js';
6
+ import { requestContextService } from "../../../utils/index.js";
7
7
  // Import the shape and the final schema/types
8
- import { BaseErrorCode } from '../../../types-global/errors.js'; // Keep direct import for types-global
9
- import { diffGitChanges, GitDiffInputShape } from './logic.js';
8
+ import { BaseErrorCode } from "../../../types-global/errors.js"; // Keep direct import for types-global
9
+ import { diffGitChanges, GitDiffInputShape, } from "./logic.js";
10
10
  let _getWorkingDirectory;
11
11
  let _getSessionId;
12
12
  /**
@@ -18,9 +18,9 @@ let _getSessionId;
18
18
  export function initializeGitDiffStateAccessors(getWdFn, getSidFn) {
19
19
  _getWorkingDirectory = getWdFn;
20
20
  _getSessionId = getSidFn;
21
- logger.info('State accessors initialized for git_diff tool registration.');
21
+ logger.info("State accessors initialized for git_diff tool registration.");
22
22
  }
23
- const TOOL_NAME = 'git_diff';
23
+ const TOOL_NAME = "git_diff";
24
24
  const TOOL_DESCRIPTION = "Shows changes between commits, commit and working tree, etc. Can show staged changes or diff specific files. An optional 'includeUntracked' parameter (boolean) can be used to also show the content of untracked files. Returns the diff output as plain text.";
25
25
  /**
26
26
  * Registers the git_diff tool with the MCP server.
@@ -30,16 +30,19 @@ const TOOL_DESCRIPTION = "Shows changes between commits, commit and working tree
30
30
  */
31
31
  export async function registerGitDiffTool(server) {
32
32
  if (!_getWorkingDirectory || !_getSessionId) {
33
- throw new Error('State accessors for git_diff must be initialized before registration.');
33
+ throw new Error("State accessors for git_diff must be initialized before registration.");
34
34
  }
35
- const operation = 'registerGitDiffTool';
35
+ const operation = "registerGitDiffTool";
36
36
  const context = requestContextService.createRequestContext({ operation });
37
37
  await ErrorHandler.tryCatch(async () => {
38
38
  // Use the exported shape for registration
39
39
  server.tool(TOOL_NAME, TOOL_DESCRIPTION, GitDiffInputShape, // Provide the Zod base schema shape
40
40
  async (validatedArgs, callContext) => {
41
- const toolOperation = 'tool:git_diff';
42
- const requestContext = requestContextService.createRequestContext({ operation: toolOperation, parentContext: callContext });
41
+ const toolOperation = "tool:git_diff";
42
+ const requestContext = requestContextService.createRequestContext({
43
+ operation: toolOperation,
44
+ parentContext: callContext,
45
+ });
43
46
  const sessionId = _getSessionId(requestContext);
44
47
  const getWorkingDirectoryForSession = () => {
45
48
  return _getWorkingDirectory(sessionId);
@@ -55,11 +58,11 @@ export async function registerGitDiffTool(server) {
55
58
  const diffResult = await diffGitChanges(validatedArgs, logicContext);
56
59
  // Format the result (the diff string) as plain text within TextContent
57
60
  const resultContent = {
58
- type: 'text',
61
+ type: "text",
59
62
  // Return the raw diff output directly
60
63
  text: diffResult.diff,
61
64
  // Indicate the content type is plain text diff
62
- contentType: 'text/plain; charset=utf-8', // Or 'text/x-diff'
65
+ contentType: "text/plain; charset=utf-8", // Or 'text/x-diff'
63
66
  };
64
67
  logger.info(`Tool ${TOOL_NAME} executed successfully: ${diffResult.message}`, logicContext);
65
68
  // Success is determined by the logic function
@@ -74,4 +77,3 @@ export async function registerGitDiffTool(server) {
74
77
  logger.info(`Tool registered: ${TOOL_NAME}`, context);
75
78
  }, { operation, context, critical: true });
76
79
  }
77
- ;
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @fileoverview Barrel file for the gitFetch tool.
3
3
  */
4
- export { registerGitFetchTool, initializeGitFetchStateAccessors } from './registration.js';
4
+ export { registerGitFetchTool, initializeGitFetchStateAccessors, } from "./registration.js";
5
5
  // Export types if needed elsewhere, e.g.:
6
6
  // export type { GitFetchInput, GitFetchResult } from './logic.js';
@@ -1,19 +1,35 @@
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_fetch tool using Zod
12
12
  export const GitFetchInputSchema = 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
- remote: z.string().optional().describe("The remote repository to fetch from (e.g., 'origin'). If omitted, fetches from 'origin' or the default configured remote."),
15
- prune: z.boolean().optional().default(false).describe("Before fetching, remove any remote-tracking references that no longer exist on the remote."),
16
- tags: z.boolean().optional().default(false).describe("Fetch all tags from the remote (in addition to whatever else is fetched)."),
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
+ remote: z
20
+ .string()
21
+ .optional()
22
+ .describe("The remote repository to fetch from (e.g., 'origin'). If omitted, fetches from 'origin' or the default configured remote."),
23
+ prune: z
24
+ .boolean()
25
+ .optional()
26
+ .default(false)
27
+ .describe("Before fetching, remove any remote-tracking references that no longer exist on the remote."),
28
+ tags: z
29
+ .boolean()
30
+ .optional()
31
+ .default(false)
32
+ .describe("Fetch all tags from the remote (in addition to whatever else is fetched)."),
17
33
  all: z.boolean().optional().default(false).describe("Fetch all remotes."),
18
34
  // Add options like --depth, specific refspecs if needed
19
35
  });
@@ -26,12 +42,12 @@ export const GitFetchInputSchema = z.object({
26
42
  * @throws {McpError} Throws an McpError if path resolution, validation, or the git command fails unexpectedly.
27
43
  */
28
44
  export async function fetchGitRemote(input, context) {
29
- const operation = 'fetchGitRemote';
45
+ const operation = "fetchGitRemote";
30
46
  logger.debug(`Executing ${operation}`, { ...context, input });
31
47
  let targetPath;
32
48
  try {
33
49
  // Resolve and sanitize the target path
34
- if (input.path && input.path !== '.') {
50
+ if (input.path && input.path !== ".") {
35
51
  targetPath = input.path;
36
52
  }
37
53
  else {
@@ -41,28 +57,38 @@ export async function fetchGitRemote(input, context) {
41
57
  }
42
58
  targetPath = workingDir;
43
59
  }
44
- targetPath = sanitization.sanitizePath(targetPath, { allowAbsolute: true }).sanitizedPath;
45
- logger.debug('Sanitized path', { ...context, operation, sanitizedPath: targetPath });
60
+ targetPath = sanitization.sanitizePath(targetPath, {
61
+ allowAbsolute: true,
62
+ }).sanitizedPath;
63
+ logger.debug("Sanitized path", {
64
+ ...context,
65
+ operation,
66
+ sanitizedPath: targetPath,
67
+ });
46
68
  }
47
69
  catch (error) {
48
- logger.error('Path resolution or sanitization failed', { ...context, operation, error });
70
+ logger.error("Path resolution or sanitization failed", {
71
+ ...context,
72
+ operation,
73
+ error,
74
+ });
49
75
  if (error instanceof McpError)
50
76
  throw error;
51
77
  throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
52
78
  }
53
79
  // Basic sanitization for remote name
54
- const safeRemote = input.remote?.replace(/[^a-zA-Z0-9_.\-/]/g, '');
80
+ const safeRemote = input.remote?.replace(/[^a-zA-Z0-9_.\-/]/g, "");
55
81
  try {
56
82
  // Construct the git fetch command
57
83
  let command = `git -C "${targetPath}" fetch`;
58
84
  if (input.prune) {
59
- command += ' --prune';
85
+ command += " --prune";
60
86
  }
61
87
  if (input.tags) {
62
- command += ' --tags';
88
+ command += " --tags";
63
89
  }
64
90
  if (input.all) {
65
- command += ' --all';
91
+ command += " --all";
66
92
  }
67
93
  else if (safeRemote) {
68
94
  command += ` ${safeRemote}`; // Fetch specific remote if 'all' is not used
@@ -71,47 +97,50 @@ export async function fetchGitRemote(input, context) {
71
97
  logger.debug(`Executing command: ${command}`, { ...context, operation });
72
98
  // Execute command. Fetch output is primarily on stderr.
73
99
  const { stdout, stderr } = await execAsync(command);
74
- logger.info(`Git fetch stdout: ${stdout}`, { ...context, operation }); // stdout is usually empty
75
- logger.info(`Git fetch stderr: ${stderr}`, { ...context, operation }); // stderr contains fetch details
76
- // Analyze stderr for success/summary
77
- let message = stderr.trim() || 'Fetch command executed.'; // Use stderr as the primary message
78
- let summary = undefined;
79
- // Check for common patterns in stderr
80
- if (stderr.includes('Updating') || stderr.includes('->') || stderr.includes('new tag') || stderr.includes('new branch')) {
81
- message = 'Fetch successful.';
82
- summary = stderr.trim(); // Use the full stderr as summary
83
- }
84
- else if (stderr.trim() === '') {
85
- // Sometimes fetch completes successfully with no output if nothing changed
86
- message = 'Fetch successful (no changes detected).';
100
+ logger.debug(`Git fetch stdout: ${stdout}`, { ...context, operation }); // stdout is usually empty
101
+ if (stderr) {
102
+ logger.debug(`Git fetch stderr: ${stderr}`, { ...context, operation }); // stderr contains fetch details
87
103
  }
88
- else if (message.includes('fatal:')) {
89
- // Should be caught by catch block, but double-check
90
- logger.error(`Git fetch command indicated failure: ${message}`, { ...context, operation, stdout, stderr });
91
- // Re-throw as an internal error if not caught below
92
- throw new Error(`Fetch failed: ${message}`);
93
- }
94
- logger.info(`${operation} completed successfully. ${message}`, { ...context, operation, path: targetPath });
104
+ // Analyze stderr for success/summary
105
+ const message = "Fetch successful.";
106
+ const summary = stderr.trim() || "No changes detected.";
107
+ logger.info(message, {
108
+ ...context,
109
+ operation,
110
+ path: targetPath,
111
+ summary,
112
+ });
95
113
  return { success: true, message, summary };
96
114
  }
97
115
  catch (error) {
98
- logger.error(`Failed to execute git fetch command`, { ...context, operation, path: targetPath, error: error.message, stderr: error.stderr, stdout: error.stdout });
99
- const errorMessage = error.stderr || error.stdout || error.message || '';
116
+ logger.error(`Failed to execute git fetch command`, {
117
+ ...context,
118
+ operation,
119
+ path: targetPath,
120
+ error: error.message,
121
+ stderr: error.stderr,
122
+ stdout: error.stdout,
123
+ });
124
+ const errorMessage = error.stderr || error.stdout || error.message || "";
100
125
  // Handle specific error cases
101
- if (errorMessage.toLowerCase().includes('not a git repository')) {
126
+ if (errorMessage.toLowerCase().includes("not a git repository")) {
102
127
  throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`, { context, operation, originalError: error });
103
128
  }
104
- if (errorMessage.includes('resolve host') || errorMessage.includes('Could not read from remote repository') || errorMessage.includes('Connection timed out')) {
105
- throw new McpError(BaseErrorCode.SERVICE_UNAVAILABLE, `Failed to connect to remote repository '${input.remote || 'default'}'. Error: ${errorMessage}`, { context, operation, originalError: error });
129
+ if (errorMessage.includes("resolve host") ||
130
+ errorMessage.includes("Could not read from remote repository") ||
131
+ errorMessage.includes("Connection timed out")) {
132
+ throw new McpError(BaseErrorCode.SERVICE_UNAVAILABLE, `Failed to connect to remote repository '${input.remote || "default"}'. Error: ${errorMessage}`, { context, operation, originalError: error });
106
133
  }
107
- if (errorMessage.includes('fatal: ') && errorMessage.includes('couldn\'t find remote ref')) {
134
+ if (errorMessage.includes("fatal: ") &&
135
+ errorMessage.includes("couldn't find remote ref")) {
108
136
  throw new McpError(BaseErrorCode.NOT_FOUND, `Remote ref not found. Error: ${errorMessage}`, { context, operation, originalError: error });
109
137
  }
110
- if (errorMessage.includes('Authentication failed') || errorMessage.includes('Permission denied')) {
111
- throw new McpError(BaseErrorCode.UNAUTHORIZED, `Authentication failed for remote repository '${input.remote || 'default'}'. Error: ${errorMessage}`, { context, operation, originalError: error });
138
+ if (errorMessage.includes("Authentication failed") ||
139
+ errorMessage.includes("Permission denied")) {
140
+ throw new McpError(BaseErrorCode.UNAUTHORIZED, `Authentication failed for remote repository '${input.remote || "default"}'. Error: ${errorMessage}`, { context, operation, originalError: error });
112
141
  }
113
- if (errorMessage.includes('does not appear to be a git repository')) {
114
- throw new McpError(BaseErrorCode.NOT_FOUND, `Remote '${input.remote || 'default'}' does not appear to be a git repository. Error: ${errorMessage}`, { context, operation, originalError: error });
142
+ if (errorMessage.includes("does not appear to be a git repository")) {
143
+ throw new McpError(BaseErrorCode.NOT_FOUND, `Remote '${input.remote || "default"}' does not appear to be a git repository. Error: ${errorMessage}`, { context, operation, originalError: error });
115
144
  }
116
145
  // Generic internal error for other failures
117
146
  throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to git fetch for path: ${targetPath}. Error: ${errorMessage}`, { context, operation, originalError: error });
@@ -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 { fetchGitRemote, GitFetchInputSchema } 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 { fetchGitRemote, GitFetchInputSchema, } from "./logic.js";
9
9
  let _getWorkingDirectory;
10
10
  let _getSessionId;
11
11
  /**
@@ -17,9 +17,9 @@ let _getSessionId;
17
17
  export function initializeGitFetchStateAccessors(getWdFn, getSidFn) {
18
18
  _getWorkingDirectory = getWdFn;
19
19
  _getSessionId = getSidFn;
20
- logger.info('State accessors initialized for git_fetch tool registration.');
20
+ logger.info("State accessors initialized for git_fetch tool registration.");
21
21
  }
22
- const TOOL_NAME = 'git_fetch';
22
+ const TOOL_NAME = "git_fetch";
23
23
  const TOOL_DESCRIPTION = "Downloads objects and refs from one or more other repositories. Can fetch specific remotes or all, prune stale branches, and fetch tags.";
24
24
  /**
25
25
  * Registers the git_fetch tool with the MCP server.
@@ -29,15 +29,18 @@ const TOOL_DESCRIPTION = "Downloads objects and refs from one or more other repo
29
29
  */
30
30
  export async function registerGitFetchTool(server) {
31
31
  if (!_getWorkingDirectory || !_getSessionId) {
32
- throw new Error('State accessors for git_fetch must be initialized before registration.');
32
+ throw new Error("State accessors for git_fetch must be initialized before registration.");
33
33
  }
34
- const operation = 'registerGitFetchTool';
34
+ const operation = "registerGitFetchTool";
35
35
  const context = requestContextService.createRequestContext({ operation });
36
36
  await ErrorHandler.tryCatch(async () => {
37
37
  server.tool(TOOL_NAME, TOOL_DESCRIPTION, GitFetchInputSchema.shape, // Provide the Zod schema shape
38
38
  async (validatedArgs, callContext) => {
39
- const toolOperation = 'tool:git_fetch';
40
- const requestContext = requestContextService.createRequestContext({ operation: toolOperation, parentContext: callContext });
39
+ const toolOperation = "tool:git_fetch";
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,10 +56,10 @@ export async function registerGitFetchTool(server) {
53
56
  const fetchResult = await fetchGitRemote(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
  // Stringify the entire GitFetchResult object
58
61
  text: JSON.stringify(fetchResult, null, 2), // Pretty-print JSON
59
- contentType: 'application/json',
62
+ contentType: "application/json",
60
63
  };
61
64
  logger.info(`Tool ${TOOL_NAME} executed successfully: ${fetchResult.message}`, logicContext);
62
65
  // Success is determined by the logic function and included in the result object
@@ -71,4 +74,3 @@ export async function registerGitFetchTool(server) {
71
74
  logger.info(`Tool registered: ${TOOL_NAME}`, context);
72
75
  }, { operation, context, critical: true });
73
76
  }
74
- ;
@@ -2,6 +2,6 @@
2
2
  * @fileoverview Barrel file for the git_init tool.
3
3
  * Exports the registration function and the state accessor initializer.
4
4
  */
5
- export { registerGitInitTool, initializeGitInitStateAccessors } from './registration.js';
5
+ export { registerGitInitTool, initializeGitInitStateAccessors, } from "./registration.js";
6
6
  // Export types if needed elsewhere, e.g.:
7
7
  // export type { GitInitInput, GitInitResult } from './logic.js';