@cyanheads/git-mcp-server 2.1.8 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/README.md +4 -4
  2. package/dist/mcp-server/server.js +69 -228
  3. package/dist/mcp-server/tools/gitAdd/index.js +2 -4
  4. package/dist/mcp-server/tools/gitAdd/logic.js +40 -116
  5. package/dist/mcp-server/tools/gitAdd/registration.js +39 -59
  6. package/dist/mcp-server/tools/gitBranch/index.js +3 -5
  7. package/dist/mcp-server/tools/gitBranch/logic.js +109 -304
  8. package/dist/mcp-server/tools/gitBranch/registration.js +52 -66
  9. package/dist/mcp-server/tools/gitCheckout/index.js +2 -3
  10. package/dist/mcp-server/tools/gitCheckout/logic.js +47 -144
  11. package/dist/mcp-server/tools/gitCheckout/registration.js +53 -72
  12. package/dist/mcp-server/tools/gitCherryPick/index.js +3 -5
  13. package/dist/mcp-server/tools/gitCherryPick/logic.js +47 -173
  14. package/dist/mcp-server/tools/gitCherryPick/registration.js +52 -67
  15. package/dist/mcp-server/tools/gitClean/index.js +3 -5
  16. package/dist/mcp-server/tools/gitClean/logic.js +45 -154
  17. package/dist/mcp-server/tools/gitClean/registration.js +52 -92
  18. package/dist/mcp-server/tools/gitClearWorkingDir/index.js +3 -5
  19. package/dist/mcp-server/tools/gitClearWorkingDir/logic.js +18 -32
  20. package/dist/mcp-server/tools/gitClearWorkingDir/registration.js +55 -73
  21. package/dist/mcp-server/tools/gitClone/index.js +2 -4
  22. package/dist/mcp-server/tools/gitClone/logic.js +47 -187
  23. package/dist/mcp-server/tools/gitClone/registration.js +51 -42
  24. package/dist/mcp-server/tools/gitCommit/index.js +2 -4
  25. package/dist/mcp-server/tools/gitCommit/logic.js +75 -310
  26. package/dist/mcp-server/tools/gitCommit/registration.js +52 -73
  27. package/dist/mcp-server/tools/gitDiff/index.js +2 -3
  28. package/dist/mcp-server/tools/gitDiff/logic.js +72 -264
  29. package/dist/mcp-server/tools/gitDiff/registration.js +53 -68
  30. package/dist/mcp-server/tools/gitFetch/index.js +2 -3
  31. package/dist/mcp-server/tools/gitFetch/logic.js +38 -136
  32. package/dist/mcp-server/tools/gitFetch/registration.js +54 -66
  33. package/dist/mcp-server/tools/gitInit/index.js +3 -5
  34. package/dist/mcp-server/tools/gitInit/logic.js +40 -162
  35. package/dist/mcp-server/tools/gitInit/registration.js +52 -104
  36. package/dist/mcp-server/tools/gitLog/index.js +2 -3
  37. package/dist/mcp-server/tools/gitLog/logic.js +71 -266
  38. package/dist/mcp-server/tools/gitLog/registration.js +54 -66
  39. package/dist/mcp-server/tools/gitMerge/index.js +3 -5
  40. package/dist/mcp-server/tools/gitMerge/logic.js +45 -191
  41. package/dist/mcp-server/tools/gitMerge/registration.js +52 -71
  42. package/dist/mcp-server/tools/gitPull/index.js +2 -3
  43. package/dist/mcp-server/tools/gitPull/logic.js +39 -156
  44. package/dist/mcp-server/tools/gitPull/registration.js +53 -75
  45. package/dist/mcp-server/tools/gitPush/index.js +2 -3
  46. package/dist/mcp-server/tools/gitPush/logic.js +65 -192
  47. package/dist/mcp-server/tools/gitPush/registration.js +53 -75
  48. package/dist/mcp-server/tools/gitRebase/index.js +3 -5
  49. package/dist/mcp-server/tools/gitRebase/logic.js +59 -207
  50. package/dist/mcp-server/tools/gitRebase/registration.js +52 -70
  51. package/dist/mcp-server/tools/gitRemote/index.js +3 -5
  52. package/dist/mcp-server/tools/gitRemote/logic.js +76 -200
  53. package/dist/mcp-server/tools/gitRemote/registration.js +52 -65
  54. package/dist/mcp-server/tools/gitReset/index.js +2 -3
  55. package/dist/mcp-server/tools/gitReset/logic.js +33 -133
  56. package/dist/mcp-server/tools/gitReset/registration.js +53 -60
  57. package/dist/mcp-server/tools/gitSetWorkingDir/index.js +3 -5
  58. package/dist/mcp-server/tools/gitSetWorkingDir/logic.js +39 -144
  59. package/dist/mcp-server/tools/gitSetWorkingDir/registration.js +55 -85
  60. package/dist/mcp-server/tools/gitShow/index.js +3 -5
  61. package/dist/mcp-server/tools/gitShow/logic.js +28 -133
  62. package/dist/mcp-server/tools/gitShow/registration.js +52 -74
  63. package/dist/mcp-server/tools/gitStash/index.js +3 -5
  64. package/dist/mcp-server/tools/gitStash/logic.js +59 -219
  65. package/dist/mcp-server/tools/gitStash/registration.js +52 -77
  66. package/dist/mcp-server/tools/gitStatus/index.js +2 -4
  67. package/dist/mcp-server/tools/gitStatus/logic.js +79 -236
  68. package/dist/mcp-server/tools/gitStatus/registration.js +52 -66
  69. package/dist/mcp-server/tools/gitTag/index.js +3 -5
  70. package/dist/mcp-server/tools/gitTag/logic.js +57 -198
  71. package/dist/mcp-server/tools/gitTag/registration.js +54 -73
  72. package/dist/mcp-server/tools/gitWorktree/index.js +3 -5
  73. package/dist/mcp-server/tools/gitWorktree/logic.js +102 -328
  74. package/dist/mcp-server/tools/gitWorktree/registration.js +54 -58
  75. package/dist/mcp-server/tools/gitWrapupInstructions/index.js +5 -3
  76. package/dist/mcp-server/tools/gitWrapupInstructions/logic.js +25 -43
  77. package/dist/mcp-server/tools/gitWrapupInstructions/registration.js +54 -70
  78. package/dist/mcp-server/transports/httpTransport.js +2 -3
  79. package/package.json +8 -8
@@ -1,25 +1,9 @@
1
- // Import utils from barrel (ErrorHandler from ../utils/internal/errorHandler.js)
2
- import { ErrorHandler } from "../../../utils/index.js";
3
- // Import utils from barrel (logger from ../utils/internal/logger.js)
4
- import { logger } from "../../../utils/index.js";
5
- // Import utils from barrel (requestContextService from ../utils/internal/requestContext.js)
6
- import { requestContextService } from "../../../utils/index.js";
7
- // Import the result type along with the function and input schema
8
- import { BaseErrorCode } from "../../../types-global/errors.js"; // Keep direct import for types-global
9
- import { commitGitChanges, GitCommitInputSchema, } from "./logic.js";
10
- let _getWorkingDirectory;
11
- let _getSessionId;
12
1
  /**
13
- * Initializes the state accessors needed by the tool registration.
14
- * This should be called once during server setup.
15
- * @param getWdFn - Function to get the working directory for a session.
16
- * @param getSidFn - Function to get the session ID from context.
2
+ * @fileoverview Handles registration and error handling for the git_commit tool.
3
+ * @module src/mcp-server/tools/gitCommit/registration
17
4
  */
18
- export function initializeGitCommitStateAccessors(getWdFn, getSidFn) {
19
- _getWorkingDirectory = getWdFn;
20
- _getSessionId = getSidFn;
21
- logger.info("State accessors initialized for git_commit tool registration.");
22
- }
5
+ import { ErrorHandler, logger, requestContextService } from "../../../utils/index.js";
6
+ import { commitGitChanges, GitCommitInputSchema, GitCommitOutputSchema, } from "./logic.js";
23
7
  const TOOL_NAME = "git_commit";
24
8
  const TOOL_DESCRIPTION = `Commits staged changes to the Git repository index with a descriptive message. Supports author override, amending, and empty commits. Returns a JSON result.
25
9
 
@@ -45,63 +29,58 @@ Closes #123 (if applicable).
45
29
  - Commit related changes logically. Use the optional \`filesToStage\` parameter to auto-stage specific files before committing.
46
30
  - The \`path\` defaults to the session's working directory unless overridden. If \`GIT_SIGN_COMMITS=true\` is set, commits are signed (\`-S\`), with an optional \`forceUnsignedOnFailure\` fallback.`;
47
31
  /**
48
- * Registers the git_commit tool with the MCP server.
49
- * Uses the high-level server.tool() method for registration, schema validation, and routing.
50
- *
51
- * @param {McpServer} server - The McpServer instance to register the tool with.
52
- * @returns {Promise<void>}
53
- * @throws {Error} If registration fails or state accessors are not initialized.
32
+ * Registers the git_commit tool with the MCP server instance.
33
+ * @param server The MCP server instance.
34
+ * @param getWorkingDirectory Function to get the session's working directory.
35
+ * @param getSessionId Function to get the session ID from context.
54
36
  */
55
- export const registerGitCommitTool = async (server) => {
56
- if (!_getWorkingDirectory || !_getSessionId) {
57
- throw new Error("State accessors for git_commit must be initialized before registration.");
58
- }
37
+ export const registerGitCommitTool = async (server, getWorkingDirectory, getSessionId) => {
59
38
  const operation = "registerGitCommitTool";
60
39
  const context = requestContextService.createRequestContext({ operation });
61
- await ErrorHandler.tryCatch(async () => {
62
- server.tool(TOOL_NAME, TOOL_DESCRIPTION, GitCommitInputSchema.shape, // Provide the Zod schema shape
63
- async (validatedArgs, callContext) => {
64
- const toolOperation = "tool:git_commit";
65
- const requestContext = requestContextService.createRequestContext({
66
- operation: toolOperation,
67
- parentContext: callContext,
40
+ server.registerTool(TOOL_NAME, {
41
+ title: "Git Commit",
42
+ description: TOOL_DESCRIPTION,
43
+ inputSchema: GitCommitInputSchema.shape,
44
+ outputSchema: GitCommitOutputSchema.shape,
45
+ annotations: {
46
+ readOnlyHint: false,
47
+ destructiveHint: false,
48
+ idempotentHint: false, // Committing is not idempotent
49
+ openWorldHint: false,
50
+ },
51
+ }, async (params, callContext) => {
52
+ const handlerContext = requestContextService.createRequestContext({
53
+ toolName: TOOL_NAME,
54
+ parentContext: callContext,
55
+ });
56
+ try {
57
+ const sessionId = getSessionId(handlerContext);
58
+ const result = await commitGitChanges(params, {
59
+ ...handlerContext,
60
+ getWorkingDirectory: () => getWorkingDirectory(sessionId),
68
61
  });
69
- const sessionId = _getSessionId(requestContext);
70
- const getWorkingDirectoryForSession = () => {
71
- return _getWorkingDirectory(sessionId);
72
- };
73
- const logicContext = {
74
- ...requestContext,
75
- sessionId: sessionId,
76
- getWorkingDirectory: getWorkingDirectoryForSession,
62
+ return {
63
+ structuredContent: result,
64
+ content: [{ type: "text", text: `Success: ${JSON.stringify(result, null, 2)}` }],
77
65
  };
78
- logger.info(`Executing tool: ${TOOL_NAME}`, logicContext);
79
- return await ErrorHandler.tryCatch(async () => {
80
- // Call the core logic function which now returns a GitCommitResult object
81
- const commitResult = await commitGitChanges(validatedArgs, logicContext);
82
- // Format the result as a JSON string within TextContent
83
- const resultContent = {
84
- type: "text",
85
- // Stringify the JSON object for the response content
86
- text: JSON.stringify(commitResult, null, 2), // Pretty-print JSON
87
- contentType: "application/json",
88
- };
89
- // Log based on the success flag in the result
90
- if (commitResult.success) {
91
- logger.info(`Tool ${TOOL_NAME} executed successfully, returning JSON`, logicContext);
92
- }
93
- else {
94
- logger.info(`Tool ${TOOL_NAME} completed with non-fatal condition (e.g., nothing to commit), returning JSON`, logicContext);
95
- }
96
- // Even if success is false (e.g., nothing to commit), it's not a tool execution error
97
- return { content: [resultContent] };
98
- }, {
99
- operation: toolOperation,
100
- context: logicContext,
101
- input: validatedArgs,
102
- errorCode: BaseErrorCode.INTERNAL_ERROR,
66
+ }
67
+ catch (error) {
68
+ logger.error(`Error in ${TOOL_NAME} handler`, { error, ...handlerContext });
69
+ const mcpError = ErrorHandler.handleError(error, {
70
+ operation: `tool:${TOOL_NAME}`,
71
+ context: handlerContext,
72
+ input: params,
103
73
  });
104
- });
105
- logger.info(`Tool registered: ${TOOL_NAME}`, context);
106
- }, { operation, context, critical: true });
74
+ return {
75
+ isError: true,
76
+ content: [{ type: "text", text: `Error: ${mcpError.message}` }],
77
+ structuredContent: {
78
+ code: mcpError.code,
79
+ message: mcpError.message,
80
+ details: mcpError.details,
81
+ },
82
+ };
83
+ }
84
+ });
85
+ logger.info(`Tool '${TOOL_NAME}' registered successfully.`, context);
107
86
  };
@@ -1,6 +1,5 @@
1
1
  /**
2
2
  * @fileoverview Barrel file for the gitDiff tool.
3
+ * @module src/mcp-server/tools/gitDiff/index
3
4
  */
4
- export { registerGitDiffTool, initializeGitDiffStateAccessors, } from "./registration.js";
5
- // Export types if needed elsewhere, e.g.:
6
- // export type { GitDiffInput, GitDiffResult } from './logic.js';
5
+ export { registerGitDiffTool } from "./registration.js";
@@ -1,279 +1,87 @@
1
+ /**
2
+ * @fileoverview Defines the core logic, schemas, and types for the git_diff tool.
3
+ * @module src/mcp-server/tools/gitDiff/logic
4
+ */
1
5
  import { execFile } from "child_process";
2
6
  import { promisify } from "util";
3
7
  import { z } from "zod";
4
- // Import utils from barrel (logger from ../utils/internal/logger.js)
5
- import { logger } from "../../../utils/index.js";
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
8
- // Import utils from barrel (sanitization from ../utils/security/sanitization.js)
9
- import { sanitization } from "../../../utils/index.js";
8
+ import { logger, sanitization } from "../../../utils/index.js";
9
+ import { McpError, BaseErrorCode } from "../../../types-global/errors.js";
10
10
  const execFileAsync = promisify(execFile);
11
- // Define the base input schema without refinement
12
- const GitDiffInputBaseSchema = z.object({
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."),
41
- // Add options like --name-only, --stat, context lines (-U<n>) if needed
11
+ // 1. DEFINE the Zod input schema.
12
+ export const GitDiffBaseSchema = z.object({
13
+ path: z.string().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."),
15
+ commit2: z.string().optional().describe("Second commit, branch, or ref for comparison."),
16
+ staged: z.boolean().default(false).describe("Show diff of changes staged for the next commit."),
17
+ file: z.string().optional().describe("Limit the diff output to a specific file path."),
18
+ includeUntracked: z.boolean().default(false).describe("Include untracked files in the diff output."),
19
+ });
20
+ export const GitDiffInputSchema = GitDiffBaseSchema.refine(data => !(data.staged && (data.commit1 || data.commit2)), {
21
+ message: "Cannot use 'staged' option with specific commit references.",
22
+ path: ["staged"],
42
23
  });
43
- // Export the shape for registration
44
- export const GitDiffInputShape = GitDiffInputBaseSchema.shape;
45
- // Define the final schema with refinement for validation during execution
46
- export const GitDiffInputSchema = GitDiffInputBaseSchema.refine((data) => !(data.staged && (data.commit1 || data.commit2)), {
47
- message: "Cannot use 'staged' option with specific commit references (commit1 or commit2).",
48
- path: ["staged", "commit1", "commit2"], // Indicate related fields
24
+ // 2. DEFINE the Zod response schema.
25
+ export const GitDiffOutputSchema = z.object({
26
+ success: z.boolean().describe("Indicates if the command was successful."),
27
+ diff: z.string().describe("The diff output. Will be 'No changes found.' if there are no differences."),
28
+ message: z.string().describe("A summary message of the result."),
49
29
  });
30
+ async function getUntrackedFilesDiff(targetPath, context) {
31
+ const { stdout } = await execFileAsync("git", ["-C", targetPath, "ls-files", "--others", "--exclude-standard"]);
32
+ const untrackedFiles = stdout.trim().split("\n").filter(Boolean);
33
+ if (untrackedFiles.length === 0)
34
+ return "";
35
+ let diffs = "";
36
+ for (const file of untrackedFiles) {
37
+ const { stdout: diffOut } = await execFileAsync("git", ["-C", targetPath, "diff", "--no-index", "/dev/null", file]).catch(err => {
38
+ if (err.stdout)
39
+ return { stdout: err.stdout };
40
+ logger.warning(`Failed to diff untracked file: ${file}`, { ...context, error: err.message });
41
+ return { stdout: "" };
42
+ });
43
+ diffs += diffOut;
44
+ }
45
+ return diffs;
46
+ }
50
47
  /**
51
- * Executes the 'git diff' command and returns the diff output.
52
- *
53
- * @param {GitDiffInput} input - The validated input object.
54
- * @param {RequestContext} context - The request context for logging and error handling.
55
- * @returns {Promise<GitDiffResult>} A promise that resolves with the structured diff result.
56
- * @throws {McpError} Throws an McpError if path resolution, validation, or the git command fails unexpectedly.
48
+ * 4. IMPLEMENT the core logic function.
49
+ * @throws {McpError} If the logic encounters an unrecoverable issue.
57
50
  */
58
- export async function diffGitChanges(input, context) {
51
+ export async function diffGitChanges(params, context) {
59
52
  const operation = "diffGitChanges";
60
- logger.debug(`Executing ${operation}`, { ...context, input });
61
- let targetPath;
62
- try {
63
- // Resolve and sanitize the target path
64
- if (input.path && input.path !== ".") {
65
- targetPath = input.path;
66
- }
67
- else {
68
- const workingDir = context.getWorkingDirectory();
69
- if (!workingDir) {
70
- throw new McpError(BaseErrorCode.VALIDATION_ERROR, "No path provided and no working directory set for the session.", { context, operation });
71
- }
72
- targetPath = workingDir;
73
- }
74
- targetPath = sanitization.sanitizePath(targetPath, {
75
- allowAbsolute: true,
76
- }).sanitizedPath;
77
- logger.debug("Sanitized path", {
78
- ...context,
79
- operation,
80
- sanitizedPath: targetPath,
81
- });
53
+ logger.debug(`Executing ${operation}`, { ...context, params });
54
+ const workingDir = context.getWorkingDirectory();
55
+ if (params.path === "." && !workingDir) {
56
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, "No session working directory set. Please specify a 'path' or use 'git_set_working_dir' first.");
82
57
  }
83
- catch (error) {
84
- logger.error("Path resolution or sanitization failed", {
85
- ...context,
86
- operation,
87
- error,
88
- });
89
- if (error instanceof McpError)
90
- throw error;
91
- throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
58
+ const targetPath = sanitization.sanitizePath(params.path === "." ? workingDir : params.path, { allowAbsolute: true }).sanitizedPath;
59
+ const args = ["-C", targetPath, "diff"];
60
+ if (params.staged) {
61
+ args.push("--staged");
92
62
  }
93
- // Basic sanitization for refs and file path
94
- const safeCommit1 = input.commit1?.replace(/[`$&;*()|<>]/g, "");
95
- const safeCommit2 = input.commit2?.replace(/[`$&;*()|<>]/g, "");
96
- const safeFile = input.file?.replace(/[`$&;*()|<>]/g, "");
97
- let untrackedFilesDiff = "";
98
- let untrackedFilesCount = 0;
99
- try {
100
- // Construct the standard git diff command
101
- const standardDiffArgs = ["-C", targetPath, "diff"];
102
- if (input.staged) {
103
- standardDiffArgs.push("--staged"); // Or --cached
104
- }
105
- else {
106
- // Add commit references if not doing staged diff
107
- if (safeCommit1) {
108
- standardDiffArgs.push(safeCommit1);
109
- }
110
- if (safeCommit2) {
111
- standardDiffArgs.push(safeCommit2);
112
- }
113
- }
114
- // Add file path limiter if provided for standard diff
115
- // Note: `input.file` will not apply to the untracked files part unless we explicitly filter them.
116
- // For simplicity, `includeUntracked` will show all untracked files if `input.file` is also set.
117
- if (safeFile) {
118
- standardDiffArgs.push("--", safeFile); // Use '--' to separate paths from revisions
119
- }
120
- logger.debug(`Executing standard diff command: git ${standardDiffArgs.join(" ")}`, {
121
- ...context,
122
- operation,
123
- });
124
- const { stdout: standardStdout, stderr: standardStderr } = await execFileAsync("git", standardDiffArgs, {
125
- maxBuffer: 1024 * 1024 * 20,
126
- });
127
- if (standardStderr) {
128
- logger.warning(`Git diff (standard) stderr: ${standardStderr}`, {
129
- ...context,
130
- operation,
131
- });
132
- }
133
- let combinedDiffOutput = standardStdout;
134
- // Handle untracked files if requested
135
- if (input.includeUntracked) {
136
- logger.debug("Including untracked files.", { ...context, operation });
137
- const listUntrackedArgs = [
138
- "-C",
139
- targetPath,
140
- "ls-files",
141
- "--others",
142
- "--exclude-standard",
143
- ];
144
- try {
145
- const { stdout: untrackedFilesStdOut } = await execFileAsync("git", listUntrackedArgs);
146
- const untrackedFiles = untrackedFilesStdOut
147
- .trim()
148
- .split("\n")
149
- .filter((f) => f); // Filter out empty lines
150
- if (untrackedFiles.length > 0) {
151
- logger.info(`Found ${untrackedFiles.length} untracked files.`, {
152
- ...context,
153
- operation,
154
- untrackedFiles,
155
- });
156
- let individualUntrackedDiffs = "";
157
- for (const untrackedFile of untrackedFiles) {
158
- // Sanitize each untracked file path before using in command
159
- const safeUntrackedFile = untrackedFile.replace(/[`$&;*()|<>]/g, "");
160
- // Skip if file path becomes empty after sanitization (unlikely but safe)
161
- if (!safeUntrackedFile)
162
- continue;
163
- const untrackedDiffArgs = [
164
- "-C",
165
- targetPath,
166
- "diff",
167
- "--no-index",
168
- "/dev/null",
169
- safeUntrackedFile,
170
- ];
171
- logger.debug(`Executing diff for untracked file: git ${untrackedDiffArgs.join(" ")}`, { ...context, operation, file: safeUntrackedFile });
172
- try {
173
- const { stdout: untrackedFileDiffOut } = await execFileAsync("git", untrackedDiffArgs);
174
- individualUntrackedDiffs += untrackedFileDiffOut;
175
- untrackedFilesCount++;
176
- }
177
- catch (untrackedError) {
178
- // For `git diff --no-index`, a non-zero exit code (usually 1) means differences were found.
179
- // The actual diff output will be in untrackedError.stdout.
180
- if (untrackedError.stdout) {
181
- individualUntrackedDiffs += untrackedError.stdout;
182
- untrackedFilesCount++;
183
- // Log stderr if it exists, as it might contain actual error messages despite stdout having the diff
184
- if (untrackedError.stderr) {
185
- logger.warning(`Stderr while diffing untracked file ${safeUntrackedFile} (diff captured from stdout): ${untrackedError.stderr}`, { ...context, operation, file: safeUntrackedFile });
186
- }
187
- }
188
- else {
189
- // If stdout is empty, then it's a more genuine failure.
190
- logger.warning(`Failed to diff untracked file: ${safeUntrackedFile}. Error: ${untrackedError.message}`, {
191
- ...context,
192
- operation,
193
- file: safeUntrackedFile,
194
- errorDetails: {
195
- stderr: untrackedError.stderr,
196
- stdout: untrackedError.stdout,
197
- code: untrackedError.code,
198
- },
199
- });
200
- individualUntrackedDiffs += `\n--- Diff for untracked file ${safeUntrackedFile} failed: ${untrackedError.message}\n`;
201
- }
202
- }
203
- }
204
- if (individualUntrackedDiffs) {
205
- // Add a separator if standard diff also had output
206
- if (combinedDiffOutput.trim()) {
207
- combinedDiffOutput += "\n";
208
- }
209
- combinedDiffOutput += individualUntrackedDiffs;
210
- }
211
- }
212
- else {
213
- logger.info("No untracked files found.", { ...context, operation });
214
- }
215
- }
216
- catch (lsFilesError) {
217
- logger.warning(`Failed to list untracked files. Error: ${lsFilesError.message}`, {
218
- ...context,
219
- operation,
220
- error: lsFilesError.stderr || lsFilesError.stdout,
221
- });
222
- // Proceed without untracked files if listing fails
223
- }
224
- }
225
- const isNoChanges = combinedDiffOutput.trim() === "";
226
- const finalDiffOutput = isNoChanges
227
- ? "No changes found."
228
- : combinedDiffOutput;
229
- let message = isNoChanges
230
- ? "No changes found."
231
- : "Diff generated successfully.";
232
- if (untrackedFilesCount > 0) {
233
- message += ` Included ${untrackedFilesCount} untracked file(s).`;
234
- }
235
- logger.info(message, {
236
- ...context,
237
- operation,
238
- path: targetPath,
239
- untrackedFilesProcessed: untrackedFilesCount,
240
- });
241
- return {
242
- success: true,
243
- diff: finalDiffOutput,
244
- message,
245
- untrackedFilesProcessed: untrackedFilesCount,
246
- };
63
+ else {
64
+ if (params.commit1)
65
+ args.push(params.commit1);
66
+ if (params.commit2)
67
+ args.push(params.commit2);
247
68
  }
248
- catch (error) {
249
- // This catch block now primarily handles errors from the *standard* diff command
250
- // or catastrophic failures before/after untracked file processing.
251
- logger.error(`Failed to execute git diff operation`, {
252
- ...context,
253
- operation,
254
- path: targetPath,
255
- error: error.message,
256
- stderr: error.stderr,
257
- stdout: error.stdout,
258
- });
259
- const errorMessage = error.stderr || error.stdout || error.message || "";
260
- // Handle specific error cases
261
- if (errorMessage.toLowerCase().includes("not a git repository")) {
262
- throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`, { context, operation, originalError: error });
263
- }
264
- if (errorMessage.includes("fatal: bad object") ||
265
- errorMessage.includes("unknown revision or path not in the working tree")) {
266
- const invalidRef = input.commit1 || input.commit2 || input.file;
267
- throw new McpError(BaseErrorCode.NOT_FOUND, `Invalid commit reference or file path specified: '${invalidRef}'. Error: ${errorMessage}`, { context, operation, originalError: error });
268
- }
269
- if (errorMessage.includes("ambiguous argument")) {
270
- const ambiguousArg = input.commit1 || input.commit2 || input.file;
271
- throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Ambiguous argument provided: '${ambiguousArg}'. Error: ${errorMessage}`, { context, operation, originalError: error });
69
+ if (params.file)
70
+ args.push("--", params.file);
71
+ logger.debug(`Executing command: git ${args.join(" ")}`, { ...context, operation });
72
+ const { stdout } = await execFileAsync("git", args, { maxBuffer: 1024 * 1024 * 20 });
73
+ let combinedDiff = stdout;
74
+ if (params.includeUntracked) {
75
+ const untrackedDiff = await getUntrackedFilesDiff(targetPath, context);
76
+ if (untrackedDiff) {
77
+ combinedDiff += (combinedDiff ? "\n" : "") + untrackedDiff;
272
78
  }
273
- // If the command exits with an error but stdout has content, it might still be useful (e.g., diff with conflicts)
274
- // However, standard 'git diff' usually exits 0 even with differences. Errors typically mean invalid input/repo state.
275
- // We'll treat most exec errors as failures.
276
- // Generic internal error for other failures
277
- throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to get git diff for path: ${targetPath}. Error: ${errorMessage}`, { context, operation, originalError: error });
278
79
  }
80
+ const noChanges = combinedDiff.trim() === "";
81
+ const message = noChanges ? "No changes found." : `Diff generated successfully.${params.includeUntracked ? " Untracked files included." : ""}`;
82
+ return {
83
+ success: true,
84
+ diff: noChanges ? "No changes found." : combinedDiff,
85
+ message,
86
+ };
279
87
  }
@@ -1,79 +1,64 @@
1
- // Import utils from barrel (ErrorHandler from ../utils/internal/errorHandler.js)
2
- import { ErrorHandler } from "../../../utils/index.js";
3
- // Import utils from barrel (logger from ../utils/internal/logger.js)
4
- import { logger } from "../../../utils/index.js";
5
- // Import utils from barrel (requestContextService, RequestContext from ../utils/internal/requestContext.js)
6
- import { requestContextService } from "../../../utils/index.js";
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";
10
- let _getWorkingDirectory;
11
- let _getSessionId;
12
1
  /**
13
- * Initializes the state accessors needed by the tool registration.
14
- * This should be called once during server setup.
15
- * @param getWdFn - Function to get the working directory for a session.
16
- * @param getSidFn - Function to get the session ID from context.
2
+ * @fileoverview Handles registration and error handling for the git_diff tool.
3
+ * @module src/mcp-server/tools/gitDiff/registration
17
4
  */
18
- export function initializeGitDiffStateAccessors(getWdFn, getSidFn) {
19
- _getWorkingDirectory = getWdFn;
20
- _getSessionId = getSidFn;
21
- logger.info("State accessors initialized for git_diff tool registration.");
22
- }
5
+ import { ErrorHandler, logger, requestContextService } from "../../../utils/index.js";
6
+ import { diffGitChanges, GitDiffOutputSchema, GitDiffBaseSchema, } from "./logic.js";
23
7
  const TOOL_NAME = "git_diff";
24
8
  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
9
  /**
26
- * Registers the git_diff tool with the MCP server.
27
- *
28
- * @param {McpServer} server - The MCP server instance.
29
- * @throws {Error} If state accessors are not initialized.
10
+ * Registers the git_diff tool with the MCP server instance.
11
+ * @param server The MCP server instance.
12
+ * @param getWorkingDirectory Function to get the session's working directory.
13
+ * @param getSessionId Function to get the session ID from context.
30
14
  */
31
- export async function registerGitDiffTool(server) {
32
- if (!_getWorkingDirectory || !_getSessionId) {
33
- throw new Error("State accessors for git_diff must be initialized before registration.");
34
- }
15
+ export const registerGitDiffTool = async (server, getWorkingDirectory, getSessionId) => {
35
16
  const operation = "registerGitDiffTool";
36
17
  const context = requestContextService.createRequestContext({ operation });
37
- await ErrorHandler.tryCatch(async () => {
38
- // Use the exported shape for registration
39
- server.tool(TOOL_NAME, TOOL_DESCRIPTION, GitDiffInputShape, // Provide the Zod base schema shape
40
- async (validatedArgs, callContext) => {
41
- const toolOperation = "tool:git_diff";
42
- const requestContext = requestContextService.createRequestContext({
43
- operation: toolOperation,
44
- parentContext: callContext,
18
+ server.registerTool(TOOL_NAME, {
19
+ title: "Git Diff",
20
+ description: TOOL_DESCRIPTION,
21
+ inputSchema: GitDiffBaseSchema.shape,
22
+ outputSchema: GitDiffOutputSchema.shape,
23
+ annotations: {
24
+ readOnlyHint: true,
25
+ destructiveHint: false,
26
+ idempotentHint: true,
27
+ openWorldHint: false,
28
+ },
29
+ }, async (params, callContext) => {
30
+ const handlerContext = requestContextService.createRequestContext({
31
+ toolName: TOOL_NAME,
32
+ parentContext: callContext,
33
+ });
34
+ try {
35
+ const sessionId = getSessionId(handlerContext);
36
+ const result = await diffGitChanges(params, {
37
+ ...handlerContext,
38
+ getWorkingDirectory: () => getWorkingDirectory(sessionId),
45
39
  });
46
- const sessionId = _getSessionId(requestContext);
47
- const getWorkingDirectoryForSession = () => {
48
- return _getWorkingDirectory(sessionId);
49
- };
50
- const logicContext = {
51
- ...requestContext,
52
- sessionId: sessionId,
53
- getWorkingDirectory: getWorkingDirectoryForSession,
40
+ return {
41
+ structuredContent: result,
42
+ content: [{ type: "text", text: result.diff, contentType: "text/plain; charset=utf-8" }],
54
43
  };
55
- logger.info(`Executing tool: ${TOOL_NAME}`, logicContext);
56
- return await ErrorHandler.tryCatch(async () => {
57
- // Call the core logic function
58
- const diffResult = await diffGitChanges(validatedArgs, logicContext);
59
- // Format the result (the diff string) as plain text within TextContent
60
- const resultContent = {
61
- type: "text",
62
- // Return the raw diff output directly
63
- text: diffResult.diff,
64
- // Indicate the content type is plain text diff
65
- contentType: "text/plain; charset=utf-8", // Or 'text/x-diff'
66
- };
67
- logger.info(`Tool ${TOOL_NAME} executed successfully: ${diffResult.message}`, logicContext);
68
- // Success is determined by the logic function
69
- return { content: [resultContent] };
70
- }, {
71
- operation: toolOperation,
72
- context: logicContext,
73
- input: validatedArgs,
74
- errorCode: BaseErrorCode.INTERNAL_ERROR, // Default if unexpected error in logic
44
+ }
45
+ catch (error) {
46
+ logger.error(`Error in ${TOOL_NAME} handler`, { error, ...handlerContext });
47
+ const mcpError = ErrorHandler.handleError(error, {
48
+ operation: `tool:${TOOL_NAME}`,
49
+ context: handlerContext,
50
+ input: params,
75
51
  });
76
- });
77
- logger.info(`Tool registered: ${TOOL_NAME}`, context);
78
- }, { operation, context, critical: true });
79
- }
52
+ return {
53
+ isError: true,
54
+ content: [{ type: "text", text: `Error: ${mcpError.message}` }],
55
+ structuredContent: {
56
+ code: mcpError.code,
57
+ message: mcpError.message,
58
+ details: mcpError.details,
59
+ },
60
+ };
61
+ }
62
+ });
63
+ logger.info(`Tool '${TOOL_NAME}' registered successfully.`, context);
64
+ };