@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.
- package/README.md +4 -4
- package/dist/mcp-server/server.js +69 -228
- package/dist/mcp-server/tools/gitAdd/index.js +2 -4
- package/dist/mcp-server/tools/gitAdd/logic.js +40 -116
- package/dist/mcp-server/tools/gitAdd/registration.js +39 -59
- package/dist/mcp-server/tools/gitBranch/index.js +3 -5
- package/dist/mcp-server/tools/gitBranch/logic.js +109 -304
- package/dist/mcp-server/tools/gitBranch/registration.js +52 -66
- package/dist/mcp-server/tools/gitCheckout/index.js +2 -3
- package/dist/mcp-server/tools/gitCheckout/logic.js +47 -144
- package/dist/mcp-server/tools/gitCheckout/registration.js +53 -72
- package/dist/mcp-server/tools/gitCherryPick/index.js +3 -5
- package/dist/mcp-server/tools/gitCherryPick/logic.js +47 -173
- package/dist/mcp-server/tools/gitCherryPick/registration.js +52 -67
- package/dist/mcp-server/tools/gitClean/index.js +3 -5
- package/dist/mcp-server/tools/gitClean/logic.js +45 -154
- package/dist/mcp-server/tools/gitClean/registration.js +52 -92
- package/dist/mcp-server/tools/gitClearWorkingDir/index.js +3 -5
- package/dist/mcp-server/tools/gitClearWorkingDir/logic.js +18 -32
- package/dist/mcp-server/tools/gitClearWorkingDir/registration.js +55 -73
- package/dist/mcp-server/tools/gitClone/index.js +2 -4
- package/dist/mcp-server/tools/gitClone/logic.js +47 -187
- package/dist/mcp-server/tools/gitClone/registration.js +51 -42
- package/dist/mcp-server/tools/gitCommit/index.js +2 -4
- package/dist/mcp-server/tools/gitCommit/logic.js +75 -310
- package/dist/mcp-server/tools/gitCommit/registration.js +52 -73
- package/dist/mcp-server/tools/gitDiff/index.js +2 -3
- package/dist/mcp-server/tools/gitDiff/logic.js +72 -264
- package/dist/mcp-server/tools/gitDiff/registration.js +53 -68
- package/dist/mcp-server/tools/gitFetch/index.js +2 -3
- package/dist/mcp-server/tools/gitFetch/logic.js +38 -136
- package/dist/mcp-server/tools/gitFetch/registration.js +54 -66
- package/dist/mcp-server/tools/gitInit/index.js +3 -5
- package/dist/mcp-server/tools/gitInit/logic.js +40 -162
- package/dist/mcp-server/tools/gitInit/registration.js +52 -104
- package/dist/mcp-server/tools/gitLog/index.js +2 -3
- package/dist/mcp-server/tools/gitLog/logic.js +71 -266
- package/dist/mcp-server/tools/gitLog/registration.js +54 -66
- package/dist/mcp-server/tools/gitMerge/index.js +3 -5
- package/dist/mcp-server/tools/gitMerge/logic.js +45 -191
- package/dist/mcp-server/tools/gitMerge/registration.js +52 -71
- package/dist/mcp-server/tools/gitPull/index.js +2 -3
- package/dist/mcp-server/tools/gitPull/logic.js +39 -156
- package/dist/mcp-server/tools/gitPull/registration.js +53 -75
- package/dist/mcp-server/tools/gitPush/index.js +2 -3
- package/dist/mcp-server/tools/gitPush/logic.js +65 -192
- package/dist/mcp-server/tools/gitPush/registration.js +53 -75
- package/dist/mcp-server/tools/gitRebase/index.js +3 -5
- package/dist/mcp-server/tools/gitRebase/logic.js +59 -207
- package/dist/mcp-server/tools/gitRebase/registration.js +52 -70
- package/dist/mcp-server/tools/gitRemote/index.js +3 -5
- package/dist/mcp-server/tools/gitRemote/logic.js +76 -200
- package/dist/mcp-server/tools/gitRemote/registration.js +52 -65
- package/dist/mcp-server/tools/gitReset/index.js +2 -3
- package/dist/mcp-server/tools/gitReset/logic.js +33 -133
- package/dist/mcp-server/tools/gitReset/registration.js +53 -60
- package/dist/mcp-server/tools/gitSetWorkingDir/index.js +3 -5
- package/dist/mcp-server/tools/gitSetWorkingDir/logic.js +39 -144
- package/dist/mcp-server/tools/gitSetWorkingDir/registration.js +55 -85
- package/dist/mcp-server/tools/gitShow/index.js +3 -5
- package/dist/mcp-server/tools/gitShow/logic.js +28 -133
- package/dist/mcp-server/tools/gitShow/registration.js +52 -74
- package/dist/mcp-server/tools/gitStash/index.js +3 -5
- package/dist/mcp-server/tools/gitStash/logic.js +59 -219
- package/dist/mcp-server/tools/gitStash/registration.js +52 -77
- package/dist/mcp-server/tools/gitStatus/index.js +2 -4
- package/dist/mcp-server/tools/gitStatus/logic.js +79 -236
- package/dist/mcp-server/tools/gitStatus/registration.js +52 -66
- package/dist/mcp-server/tools/gitTag/index.js +3 -5
- package/dist/mcp-server/tools/gitTag/logic.js +57 -198
- package/dist/mcp-server/tools/gitTag/registration.js +54 -73
- package/dist/mcp-server/tools/gitWorktree/index.js +3 -5
- package/dist/mcp-server/tools/gitWorktree/logic.js +102 -328
- package/dist/mcp-server/tools/gitWorktree/registration.js +54 -58
- package/dist/mcp-server/tools/gitWrapupInstructions/index.js +5 -3
- package/dist/mcp-server/tools/gitWrapupInstructions/logic.js +25 -43
- package/dist/mcp-server/tools/gitWrapupInstructions/registration.js +54 -70
- package/dist/mcp-server/transports/httpTransport.js +2 -3
- 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
|
-
*
|
|
14
|
-
*
|
|
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
|
-
|
|
19
|
-
|
|
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
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* @param
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
106
|
-
|
|
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
|
|
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
|
-
|
|
5
|
-
import {
|
|
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
|
-
//
|
|
12
|
-
const
|
|
13
|
-
path: z
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
//
|
|
44
|
-
export const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
message: "
|
|
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
|
-
*
|
|
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(
|
|
51
|
+
export async function diffGitChanges(params, context) {
|
|
59
52
|
const operation = "diffGitChanges";
|
|
60
|
-
logger.debug(`Executing ${operation}`, { ...context,
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
*
|
|
14
|
-
*
|
|
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
|
-
|
|
19
|
-
|
|
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
|
|
29
|
-
* @
|
|
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
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
78
|
-
|
|
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
|
+
};
|