@cyanheads/git-mcp-server 2.1.7 → 2.2.0
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 +17 -74
- package/dist/mcp-server/tools/gitAdd/registration.js +38 -59
- package/dist/mcp-server/tools/gitBranch/index.js +3 -5
- package/dist/mcp-server/tools/gitBranch/logic.js +118 -296
- 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 -122
- 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 +55 -162
- 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 +44 -143
- 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 +19 -26
- 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 +50 -171
- 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 +90 -295
- 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 +78 -254
- 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 +47 -129
- 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 +46 -152
- 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 +75 -257
- 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 +52 -179
- 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 +48 -146
- 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 +73 -181
- 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 +73 -202
- 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 +85 -193
- 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 +37 -121
- 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 +45 -133
- 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 +33 -122
- 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 +70 -214
- 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 +82 -229
- 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 +66 -188
- 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 +112 -322
- 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 +26 -38
- package/dist/mcp-server/tools/gitWrapupInstructions/registration.js +54 -70
- package/dist/mcp-server/transports/httpTransport.js +2 -3
- package/package.json +12 -12
|
@@ -1,184 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Defines the core logic, schemas, and types for the git_cherry-pick tool.
|
|
3
|
+
* @module src/mcp-server/tools/gitCherryPick/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
|
-
//
|
|
11
|
+
// 1. DEFINE the Zod input schema.
|
|
12
12
|
export const GitCherryPickInputSchema = z.object({
|
|
13
|
-
path: z
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
.min(1)
|
|
27
|
-
.optional()
|
|
28
|
-
.describe("Specify the parent number (starting from 1) when cherry-picking a merge commit."),
|
|
29
|
-
strategy: z
|
|
30
|
-
.enum(["recursive", "resolve", "ours", "theirs", "octopus", "subtree"])
|
|
31
|
-
.optional()
|
|
32
|
-
.describe("Specifies a merge strategy *option* (passed via -X)."),
|
|
33
|
-
noCommit: z
|
|
34
|
-
.boolean()
|
|
35
|
-
.default(false)
|
|
36
|
-
.describe("Apply the changes but do not create a commit."),
|
|
37
|
-
signoff: z
|
|
38
|
-
.boolean()
|
|
39
|
-
.default(false)
|
|
40
|
-
.describe("Add a Signed-off-by line to the commit message."),
|
|
41
|
-
// Add options for conflict handling? (e.g., --continue, --abort, --skip) - Maybe separate tool or mode?
|
|
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
|
+
commitRef: z.string().min(1).describe("The commit reference(s) to cherry-pick."),
|
|
15
|
+
mainline: z.number().int().min(1).optional().describe("The parent number (1-based) for a merge commit."),
|
|
16
|
+
strategy: z.enum(["recursive", "resolve", "ours", "theirs", "octopus", "subtree"]).optional().describe("The merge strategy to use."),
|
|
17
|
+
noCommit: z.boolean().default(false).describe("Apply changes but do not create a commit."),
|
|
18
|
+
signoff: z.boolean().default(false).describe("Add a 'Signed-off-by' line to the commit message."),
|
|
19
|
+
});
|
|
20
|
+
// 2. DEFINE the Zod response schema.
|
|
21
|
+
export const GitCherryPickOutputSchema = z.object({
|
|
22
|
+
success: z.boolean().describe("Indicates if the command was successful."),
|
|
23
|
+
message: z.string().describe("A summary message of the result."),
|
|
24
|
+
commitCreated: z.boolean().describe("Indicates if a new commit was created."),
|
|
25
|
+
conflicts: z.boolean().describe("Indicates if conflicts occurred."),
|
|
42
26
|
});
|
|
43
27
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* @param {GitCherryPickInput} input - The validated input object.
|
|
47
|
-
* @param {RequestContext} context - The request context for logging and error handling.
|
|
48
|
-
* @returns {Promise<GitCherryPickResult>} A promise that resolves with the structured result.
|
|
49
|
-
* @throws {McpError} Throws an McpError for path resolution/validation failures or unexpected errors.
|
|
28
|
+
* 4. IMPLEMENT the core logic function.
|
|
29
|
+
* @throws {McpError} If the logic encounters an unrecoverable issue.
|
|
50
30
|
*/
|
|
51
|
-
export async function gitCherryPickLogic(
|
|
31
|
+
export async function gitCherryPickLogic(params, context) {
|
|
52
32
|
const operation = "gitCherryPickLogic";
|
|
53
|
-
logger.debug(`Executing ${operation}`, { ...context,
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const workingDir = context.getWorkingDirectory();
|
|
58
|
-
targetPath =
|
|
59
|
-
input.path && input.path !== "." ? input.path : (workingDir ?? ".");
|
|
60
|
-
if (targetPath === "." && !workingDir) {
|
|
61
|
-
logger.warning("Executing git cherry-pick in server's CWD as no path provided and no session WD set.", { ...context, operation });
|
|
62
|
-
targetPath = process.cwd();
|
|
63
|
-
}
|
|
64
|
-
else if (targetPath === "." && workingDir) {
|
|
65
|
-
targetPath = workingDir;
|
|
66
|
-
logger.debug(`Using session working directory: ${targetPath}`, {
|
|
67
|
-
...context,
|
|
68
|
-
operation,
|
|
69
|
-
sessionId: context.sessionId,
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
else {
|
|
73
|
-
logger.debug(`Using provided path: ${targetPath}`, {
|
|
74
|
-
...context,
|
|
75
|
-
operation,
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
targetPath = sanitization.sanitizePath(targetPath, {
|
|
79
|
-
allowAbsolute: true,
|
|
80
|
-
}).sanitizedPath;
|
|
81
|
-
logger.debug("Sanitized path", {
|
|
82
|
-
...context,
|
|
83
|
-
operation,
|
|
84
|
-
sanitizedPath: targetPath,
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
catch (error) {
|
|
88
|
-
logger.error("Path resolution or sanitization failed", {
|
|
89
|
-
...context,
|
|
90
|
-
operation,
|
|
91
|
-
error,
|
|
92
|
-
});
|
|
93
|
-
if (error instanceof McpError)
|
|
94
|
-
throw error;
|
|
95
|
-
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
|
|
33
|
+
logger.debug(`Executing ${operation}`, { ...context, params });
|
|
34
|
+
const workingDir = context.getWorkingDirectory();
|
|
35
|
+
if (params.path === "." && !workingDir) {
|
|
36
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, "No session working directory set. Please specify a 'path' or use 'git_set_working_dir' first.");
|
|
96
37
|
}
|
|
38
|
+
const targetPath = sanitization.sanitizePath(params.path === "." ? workingDir : params.path, { allowAbsolute: true }).sanitizedPath;
|
|
39
|
+
const args = ["-C", targetPath, "cherry-pick"];
|
|
40
|
+
if (params.mainline)
|
|
41
|
+
args.push("-m", String(params.mainline));
|
|
42
|
+
if (params.strategy)
|
|
43
|
+
args.push(`-X${params.strategy}`);
|
|
44
|
+
if (params.noCommit)
|
|
45
|
+
args.push("--no-commit");
|
|
46
|
+
if (params.signoff)
|
|
47
|
+
args.push("--signoff");
|
|
48
|
+
args.push(params.commitRef);
|
|
97
49
|
try {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
if (input.signoff) {
|
|
109
|
-
args.push("--signoff");
|
|
110
|
-
}
|
|
111
|
-
// Add the commit reference(s)
|
|
112
|
-
args.push(input.commitRef);
|
|
113
|
-
logger.debug(`Executing command: git ${args.join(" ")}`, {
|
|
114
|
-
...context,
|
|
115
|
-
operation,
|
|
116
|
-
});
|
|
117
|
-
try {
|
|
118
|
-
const { stdout, stderr } = await execFileAsync("git", args);
|
|
119
|
-
// Check stdout/stderr for conflict messages, although exit code 0 usually means success
|
|
120
|
-
const output = stdout + stderr;
|
|
121
|
-
const conflicts = /conflict/i.test(output);
|
|
122
|
-
const commitCreated = !input.noCommit && !conflicts;
|
|
123
|
-
const message = conflicts
|
|
124
|
-
? `Cherry-pick resulted in conflicts for commit(s) '${input.commitRef}'. Manual resolution required.`
|
|
125
|
-
: `Successfully cherry-picked commit(s) '${input.commitRef}'.` +
|
|
126
|
-
(commitCreated
|
|
127
|
-
? " New commit created."
|
|
128
|
-
: input.noCommit
|
|
129
|
-
? " Changes staged."
|
|
130
|
-
: "");
|
|
131
|
-
logger.info("git cherry-pick executed successfully", {
|
|
132
|
-
...context,
|
|
133
|
-
operation,
|
|
134
|
-
path: targetPath,
|
|
135
|
-
result: { message, conflicts, commitCreated },
|
|
136
|
-
});
|
|
137
|
-
return { success: true, message, commitCreated, conflicts };
|
|
138
|
-
}
|
|
139
|
-
catch (cherryPickError) {
|
|
140
|
-
const errorMessage = cherryPickError.stderr ||
|
|
141
|
-
cherryPickError.stdout ||
|
|
142
|
-
cherryPickError.message ||
|
|
143
|
-
"";
|
|
144
|
-
if (/conflict/i.test(errorMessage)) {
|
|
145
|
-
logger.warning(`Cherry-pick failed due to conflicts for commit(s) '${input.commitRef}'.`, { ...context, operation, path: targetPath, error: errorMessage });
|
|
146
|
-
return {
|
|
147
|
-
success: false,
|
|
148
|
-
message: `Failed to cherry-pick commit(s) '${input.commitRef}' due to conflicts. Resolve conflicts manually and potentially use 'git cherry-pick --continue' or '--abort'.`,
|
|
149
|
-
error: errorMessage,
|
|
150
|
-
conflicts: true,
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
// Rethrow other errors to be caught by the outer try-catch
|
|
154
|
-
throw cherryPickError;
|
|
155
|
-
}
|
|
50
|
+
logger.debug(`Executing command: git ${args.join(" ")}`, { ...context, operation });
|
|
51
|
+
const { stdout, stderr } = await execFileAsync("git", args);
|
|
52
|
+
const output = stdout + stderr;
|
|
53
|
+
const conflicts = /conflict/i.test(output);
|
|
54
|
+
const commitCreated = !params.noCommit && !conflicts;
|
|
55
|
+
const message = conflicts
|
|
56
|
+
? `Cherry-pick resulted in conflicts for commit(s) '${params.commitRef}'. Manual resolution required.`
|
|
57
|
+
: `Successfully cherry-picked commit(s) '${params.commitRef}'.`;
|
|
58
|
+
return { success: true, message, commitCreated, conflicts };
|
|
156
59
|
}
|
|
157
60
|
catch (error) {
|
|
158
61
|
const errorMessage = error.stderr || error.stdout || error.message || "";
|
|
159
|
-
logger.error(`Failed to execute git cherry-pick command`, {
|
|
160
|
-
...context,
|
|
161
|
-
operation,
|
|
162
|
-
path: targetPath,
|
|
163
|
-
error: errorMessage,
|
|
164
|
-
stderr: error.stderr,
|
|
165
|
-
stdout: error.stdout,
|
|
166
|
-
});
|
|
167
|
-
// Specific error handling
|
|
62
|
+
logger.error(`Failed to execute git cherry-pick command`, { ...context, operation, errorMessage });
|
|
168
63
|
if (errorMessage.toLowerCase().includes("not a git repository")) {
|
|
169
|
-
throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}
|
|
64
|
+
throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`);
|
|
170
65
|
}
|
|
171
|
-
if (/
|
|
172
|
-
throw new McpError(BaseErrorCode.
|
|
66
|
+
if (/conflict/i.test(errorMessage)) {
|
|
67
|
+
throw new McpError(BaseErrorCode.CONFLICT, `Failed to cherry-pick due to conflicts. Resolve conflicts and use 'git cherry-pick --continue' or '--abort'.`);
|
|
173
68
|
}
|
|
174
|
-
if (/
|
|
175
|
-
|
|
176
|
-
throw new McpError(BaseErrorCode.CONFLICT, `Failed to cherry-pick: Unresolved conflicts from a previous operation exist. Resolve conflicts and use 'git cherry-pick --continue' or '--abort'. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
69
|
+
if (/bad revision/i.test(errorMessage)) {
|
|
70
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid commit reference '${params.commitRef}'.`);
|
|
177
71
|
}
|
|
178
72
|
if (/your local changes would be overwritten/i.test(errorMessage)) {
|
|
179
|
-
throw new McpError(BaseErrorCode.CONFLICT,
|
|
73
|
+
throw new McpError(BaseErrorCode.CONFLICT, "Your local changes would be overwritten. Please commit or stash them first.");
|
|
180
74
|
}
|
|
181
|
-
|
|
182
|
-
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Git cherry-pick failed for path: ${targetPath}. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
75
|
+
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Git cherry-pick failed: ${errorMessage}`);
|
|
183
76
|
}
|
|
184
77
|
}
|
|
@@ -1,79 +1,64 @@
|
|
|
1
|
-
// Import utils from barrel (logger from ../utils/internal/logger.js)
|
|
2
|
-
import { logger } from "../../../utils/index.js";
|
|
3
|
-
// Import utils from barrel (ErrorHandler from ../utils/internal/errorHandler.js)
|
|
4
|
-
import { ErrorHandler } from "../../../utils/index.js";
|
|
5
|
-
// Import utils from barrel (requestContextService 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 { GitCherryPickInputSchema, gitCherryPickLogic, } from "./logic.js";
|
|
9
|
-
let _getWorkingDirectory;
|
|
10
|
-
let _getSessionId;
|
|
11
1
|
/**
|
|
12
|
-
*
|
|
13
|
-
* @
|
|
14
|
-
* @param getSidFn - Function to get the session ID from context.
|
|
2
|
+
* @fileoverview Handles registration and error handling for the git_cherry-pick tool.
|
|
3
|
+
* @module src/mcp-server/tools/gitCherryPick/registration
|
|
15
4
|
*/
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
logger.info("State accessors initialized for git_cherry_pick tool registration.");
|
|
20
|
-
}
|
|
5
|
+
import { ErrorHandler, logger, requestContextService } from "../../../utils/index.js";
|
|
6
|
+
import { McpError, BaseErrorCode } from "../../../types-global/errors.js";
|
|
7
|
+
import { gitCherryPickLogic, GitCherryPickInputSchema, GitCherryPickOutputSchema, } from "./logic.js";
|
|
21
8
|
const TOOL_NAME = "git_cherry_pick";
|
|
22
9
|
const TOOL_DESCRIPTION = "Applies the changes introduced by existing commits. Supports picking single commits or ranges, handling merge commits, and options like --no-commit and --signoff. Returns results as a JSON object, indicating success, failure, or conflicts.";
|
|
23
10
|
/**
|
|
24
|
-
* Registers the git_cherry_pick tool with the MCP server.
|
|
25
|
-
*
|
|
26
|
-
* @param
|
|
27
|
-
* @
|
|
28
|
-
* @throws {Error} If registration fails or state accessors are not initialized.
|
|
11
|
+
* Registers the git_cherry_pick tool with the MCP server instance.
|
|
12
|
+
* @param server The MCP server instance.
|
|
13
|
+
* @param getWorkingDirectory Function to get the session's working directory.
|
|
14
|
+
* @param getSessionId Function to get the session ID from context.
|
|
29
15
|
*/
|
|
30
|
-
export const registerGitCherryPickTool = async (server) => {
|
|
31
|
-
if (!_getWorkingDirectory || !_getSessionId) {
|
|
32
|
-
throw new Error("State accessors for git_cherry_pick must be initialized before registration.");
|
|
33
|
-
}
|
|
16
|
+
export const registerGitCherryPickTool = async (server, getWorkingDirectory, getSessionId) => {
|
|
34
17
|
const operation = "registerGitCherryPickTool";
|
|
35
18
|
const context = requestContextService.createRequestContext({ operation });
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
19
|
+
server.registerTool(TOOL_NAME, {
|
|
20
|
+
title: "Git Cherry-Pick",
|
|
21
|
+
description: TOOL_DESCRIPTION,
|
|
22
|
+
inputSchema: GitCherryPickInputSchema.shape,
|
|
23
|
+
outputSchema: GitCherryPickOutputSchema.shape,
|
|
24
|
+
annotations: {
|
|
25
|
+
readOnlyHint: false,
|
|
26
|
+
destructiveHint: false, // Not typically destructive, but can cause conflicts
|
|
27
|
+
idempotentHint: false,
|
|
28
|
+
openWorldHint: false,
|
|
29
|
+
},
|
|
30
|
+
}, async (params, callContext) => {
|
|
31
|
+
const handlerContext = requestContextService.createRequestContext({
|
|
32
|
+
toolName: TOOL_NAME,
|
|
33
|
+
parentContext: callContext,
|
|
34
|
+
});
|
|
35
|
+
try {
|
|
36
|
+
const sessionId = getSessionId(handlerContext);
|
|
37
|
+
const result = await gitCherryPickLogic(params, {
|
|
38
|
+
...handlerContext,
|
|
39
|
+
getWorkingDirectory: () => getWorkingDirectory(sessionId),
|
|
43
40
|
});
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
...requestContext,
|
|
48
|
-
sessionId: sessionId,
|
|
49
|
-
getWorkingDirectory: getWorkingDirectoryForSession,
|
|
41
|
+
return {
|
|
42
|
+
structuredContent: result,
|
|
43
|
+
content: [{ type: "text", text: `Success: ${JSON.stringify(result, null, 2)}` }],
|
|
50
44
|
};
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
};
|
|
59
|
-
if (cherryPickResult.success) {
|
|
60
|
-
logger.info(`Tool ${TOOL_NAME} executed successfully (Conflicts: ${!!cherryPickResult.conflicts}), returning JSON`, logicContext);
|
|
61
|
-
}
|
|
62
|
-
else {
|
|
63
|
-
logger.warning(`Tool ${TOOL_NAME} failed: ${cherryPickResult.message}`, {
|
|
64
|
-
...logicContext,
|
|
65
|
-
errorDetails: cherryPickResult.error,
|
|
66
|
-
conflicts: cherryPickResult.conflicts,
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
return { content: [resultContent] };
|
|
70
|
-
}, {
|
|
71
|
-
operation: toolOperation,
|
|
72
|
-
context: logicContext,
|
|
73
|
-
input: validatedArgs,
|
|
74
|
-
errorCode: BaseErrorCode.INTERNAL_ERROR,
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
logger.error(`Error in ${TOOL_NAME} handler`, { error, ...handlerContext });
|
|
48
|
+
const handledError = ErrorHandler.handleError(error, {
|
|
49
|
+
operation: `tool:${TOOL_NAME}`,
|
|
50
|
+
context: handlerContext,
|
|
51
|
+
input: params,
|
|
75
52
|
});
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
53
|
+
const mcpError = handledError instanceof McpError
|
|
54
|
+
? handledError
|
|
55
|
+
: new McpError(BaseErrorCode.INTERNAL_ERROR, "An unexpected error occurred.", { originalError: handledError });
|
|
56
|
+
return {
|
|
57
|
+
isError: true,
|
|
58
|
+
content: [{ type: "text", text: mcpError.message }],
|
|
59
|
+
structuredContent: mcpError.details,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
logger.info(`Tool '${TOOL_NAME}' registered successfully.`, context);
|
|
79
64
|
};
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview Barrel file for the
|
|
3
|
-
*
|
|
2
|
+
* @fileoverview Barrel file for the gitClean tool.
|
|
3
|
+
* @module src/mcp-server/tools/gitClean/index
|
|
4
4
|
*/
|
|
5
|
-
export { registerGitCleanTool
|
|
6
|
-
// Export types if needed elsewhere, e.g.:
|
|
7
|
-
// export type { GitCleanInput, GitCleanResult } from './logic.js';
|
|
5
|
+
export { registerGitCleanTool } from "./registration.js";
|
|
@@ -1,168 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Defines the core logic, schemas, and types for the git_clean tool.
|
|
3
|
+
* @module src/mcp-server/tools/gitClean/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
|
-
// No refinements needed here, but the 'force' check is critical in the logic
|
|
11
|
+
// 1. DEFINE the Zod input schema.
|
|
13
12
|
export const GitCleanInputSchema = z.object({
|
|
14
|
-
path: z
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
.describe("Show what would be deleted without actually deleting (-n flag)."),
|
|
27
|
-
directories: z
|
|
28
|
-
.boolean()
|
|
29
|
-
.default(false)
|
|
30
|
-
.describe("Remove untracked directories in addition to files (-d flag)."),
|
|
31
|
-
ignored: z
|
|
32
|
-
.boolean()
|
|
33
|
-
.default(false)
|
|
34
|
-
.describe("Remove ignored files as well (-x flag). Use with extreme caution."),
|
|
35
|
-
// exclude: z.string().optional().describe("Exclude files matching pattern (-e <pattern>)"), // Consider adding later
|
|
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
|
+
force: z.boolean().describe("REQUIRED confirmation. Must be true to run the destructive clean operation."),
|
|
15
|
+
dryRun: z.boolean().default(false).describe("Show what would be deleted without actually deleting."),
|
|
16
|
+
directories: z.boolean().default(false).describe("Remove untracked directories in addition to files."),
|
|
17
|
+
ignored: z.boolean().default(false).describe("Remove ignored files as well. Use with extreme caution."),
|
|
18
|
+
});
|
|
19
|
+
// 2. DEFINE the Zod response schema.
|
|
20
|
+
export const GitCleanOutputSchema = z.object({
|
|
21
|
+
success: z.boolean().describe("Indicates if the command was successful."),
|
|
22
|
+
message: z.string().describe("A summary message of the result."),
|
|
23
|
+
filesAffected: z.array(z.string()).describe("A list of files that were or would be affected."),
|
|
24
|
+
dryRun: z.boolean().describe("Indicates if the operation was a dry run."),
|
|
36
25
|
});
|
|
37
26
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* @param {GitCleanInput} input - The validated input object.
|
|
42
|
-
* @param {RequestContext} context - The request context for logging and error handling.
|
|
43
|
-
* @returns {Promise<GitCleanResult>} A promise that resolves with the structured result.
|
|
44
|
-
* @throws {McpError} Throws an McpError for path/validation failures, if force=false, or unexpected errors.
|
|
27
|
+
* 4. IMPLEMENT the core logic function.
|
|
28
|
+
* @throws {McpError} If the logic encounters an unrecoverable issue.
|
|
45
29
|
*/
|
|
46
|
-
export async function gitCleanLogic(
|
|
30
|
+
export async function gitCleanLogic(params, context) {
|
|
47
31
|
const operation = "gitCleanLogic";
|
|
48
|
-
logger.debug(`Executing ${operation}`, { ...context,
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
logger.error("Attempted to run git clean without force=true.", {
|
|
52
|
-
...context,
|
|
53
|
-
operation,
|
|
54
|
-
});
|
|
55
|
-
throw new McpError(BaseErrorCode.VALIDATION_ERROR, "Operation aborted: 'force' parameter must be explicitly set to true to execute 'git clean'. This is a destructive command.", { context, operation });
|
|
32
|
+
logger.debug(`Executing ${operation}`, { ...context, params });
|
|
33
|
+
if (!params.force) {
|
|
34
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, "Operation aborted: 'force' must be true to execute 'git clean'.");
|
|
56
35
|
}
|
|
57
|
-
// Log that the force check passed
|
|
58
36
|
logger.warning("Executing 'git clean' with force=true. This is a destructive operation.", { ...context, operation });
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const workingDir = context.getWorkingDirectory();
|
|
63
|
-
targetPath =
|
|
64
|
-
input.path && input.path !== "." ? input.path : (workingDir ?? ".");
|
|
65
|
-
if (targetPath === "." && !workingDir) {
|
|
66
|
-
logger.warning("Executing git clean in server's CWD as no path provided and no session WD set.", { ...context, operation });
|
|
67
|
-
targetPath = process.cwd();
|
|
68
|
-
}
|
|
69
|
-
else if (targetPath === "." && workingDir) {
|
|
70
|
-
targetPath = workingDir;
|
|
71
|
-
logger.debug(`Using session working directory: ${targetPath}`, {
|
|
72
|
-
...context,
|
|
73
|
-
operation,
|
|
74
|
-
sessionId: context.sessionId,
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
else {
|
|
78
|
-
logger.debug(`Using provided path: ${targetPath}`, {
|
|
79
|
-
...context,
|
|
80
|
-
operation,
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
targetPath = sanitization.sanitizePath(targetPath, {
|
|
84
|
-
allowAbsolute: true,
|
|
85
|
-
}).sanitizedPath;
|
|
86
|
-
logger.debug("Sanitized path", {
|
|
87
|
-
...context,
|
|
88
|
-
operation,
|
|
89
|
-
sanitizedPath: targetPath,
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
catch (error) {
|
|
93
|
-
logger.error("Path resolution or sanitization failed", {
|
|
94
|
-
...context,
|
|
95
|
-
operation,
|
|
96
|
-
error,
|
|
97
|
-
});
|
|
98
|
-
if (error instanceof McpError)
|
|
99
|
-
throw error;
|
|
100
|
-
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
|
|
37
|
+
const workingDir = context.getWorkingDirectory();
|
|
38
|
+
if (params.path === "." && !workingDir) {
|
|
39
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, "No session working directory set. Please specify a 'path' or use 'git_set_working_dir' first.");
|
|
101
40
|
}
|
|
41
|
+
const targetPath = sanitization.sanitizePath(params.path === "." ? workingDir : params.path, { allowAbsolute: true }).sanitizedPath;
|
|
42
|
+
const args = ["-C", targetPath, "clean", "-f"];
|
|
43
|
+
if (params.dryRun)
|
|
44
|
+
args.push("-n");
|
|
45
|
+
if (params.directories)
|
|
46
|
+
args.push("-d");
|
|
47
|
+
if (params.ignored)
|
|
48
|
+
args.push("-x");
|
|
102
49
|
try {
|
|
103
|
-
|
|
104
|
-
// Force (-f) is always added because the logic checks input.force
|
|
105
|
-
const args = ["-C", targetPath, "clean", "-f"];
|
|
106
|
-
if (input.dryRun) {
|
|
107
|
-
args.push("-n");
|
|
108
|
-
}
|
|
109
|
-
if (input.directories) {
|
|
110
|
-
args.push("-d");
|
|
111
|
-
}
|
|
112
|
-
if (input.ignored) {
|
|
113
|
-
args.push("-x");
|
|
114
|
-
}
|
|
115
|
-
logger.debug(`Executing command: git ${args.join(" ")}`, {
|
|
116
|
-
...context,
|
|
117
|
-
operation,
|
|
118
|
-
});
|
|
50
|
+
logger.debug(`Executing command: git ${args.join(" ")}`, { ...context, operation });
|
|
119
51
|
const { stdout, stderr } = await execFileAsync("git", args);
|
|
120
52
|
if (stderr) {
|
|
121
|
-
|
|
122
|
-
logger.warning(`Git clean command produced stderr`, {
|
|
123
|
-
...context,
|
|
124
|
-
operation,
|
|
125
|
-
stderr,
|
|
126
|
-
});
|
|
53
|
+
logger.warning(`Git clean command produced stderr`, { ...context, operation, stderr });
|
|
127
54
|
}
|
|
128
|
-
|
|
129
|
-
const
|
|
130
|
-
.trim()
|
|
131
|
-
.split("\n")
|
|
132
|
-
.map((line) => line
|
|
133
|
-
.replace(/^Would remove /i, "")
|
|
134
|
-
.replace(/^Removing /i, "")
|
|
135
|
-
.trim()) // Clean up prefixes
|
|
136
|
-
.filter((file) => file); // Remove empty lines
|
|
137
|
-
const message = input.dryRun
|
|
55
|
+
const filesAffected = stdout.trim().split("\n").map(line => line.replace(/^Would remove |^Removing /i, "").trim()).filter(Boolean);
|
|
56
|
+
const message = params.dryRun
|
|
138
57
|
? `Dry run complete. Files that would be removed: ${filesAffected.length}`
|
|
139
58
|
: `Clean operation complete. Files removed: ${filesAffected.length}`;
|
|
140
|
-
|
|
141
|
-
...context,
|
|
142
|
-
operation,
|
|
143
|
-
path: targetPath,
|
|
144
|
-
dryRun: input.dryRun,
|
|
145
|
-
filesAffectedCount: filesAffected.length,
|
|
146
|
-
});
|
|
147
|
-
return { success: true, message, filesAffected, dryRun: input.dryRun };
|
|
59
|
+
return { success: true, message, filesAffected, dryRun: params.dryRun };
|
|
148
60
|
}
|
|
149
61
|
catch (error) {
|
|
150
62
|
const errorMessage = error.stderr || error.message || "";
|
|
151
|
-
logger.error(`Failed to execute git clean command`, {
|
|
152
|
-
...context,
|
|
153
|
-
operation,
|
|
154
|
-
path: targetPath,
|
|
155
|
-
error: errorMessage,
|
|
156
|
-
stderr: error.stderr,
|
|
157
|
-
stdout: error.stdout,
|
|
158
|
-
});
|
|
159
|
-
// Specific error handling
|
|
63
|
+
logger.error(`Failed to execute git clean command`, { ...context, operation, errorMessage });
|
|
160
64
|
if (errorMessage.toLowerCase().includes("not a git repository")) {
|
|
161
|
-
throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}
|
|
65
|
+
throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`);
|
|
162
66
|
}
|
|
163
|
-
|
|
164
|
-
// but returns non-zero exit code on general failure.
|
|
165
|
-
// Throw a generic McpError for other failures
|
|
166
|
-
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Git clean failed for path: ${targetPath}. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
67
|
+
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Git clean failed: ${errorMessage}`);
|
|
167
68
|
}
|
|
168
69
|
}
|