@cyanheads/git-mcp-server 2.1.8 → 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 +8 -8
|
@@ -1,209 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Defines the core logic, schemas, and types for the git_merge tool.
|
|
3
|
+
* @module src/mcp-server/tools/gitMerge/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 path from "path"; // Import path module
|
|
10
|
-
import { sanitization } from "../../../utils/index.js";
|
|
8
|
+
import { logger, sanitization } from "../../../utils/index.js";
|
|
9
|
+
import { McpError, BaseErrorCode } from "../../../types-global/errors.js";
|
|
11
10
|
const execFileAsync = promisify(execFile);
|
|
12
|
-
//
|
|
11
|
+
// 1. DEFINE the Zod input schema.
|
|
13
12
|
export const GitMergeInputSchema = z.object({
|
|
14
|
-
path: z
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
.boolean()
|
|
30
|
-
.default(false)
|
|
31
|
-
.describe("Create a merge commit even when the merge resolves as a fast-forward (`--no-ff`)."),
|
|
32
|
-
squash: z
|
|
33
|
-
.boolean()
|
|
34
|
-
.default(false)
|
|
35
|
-
.describe("Combine merged changes into a single commit (`--squash`). Requires manual commit afterwards."),
|
|
36
|
-
abort: z
|
|
37
|
-
.boolean()
|
|
38
|
-
.default(false)
|
|
39
|
-
.describe("Abort the current merge process (resolves conflicts)."),
|
|
40
|
-
// 'continue' might be too complex for initial implementation due to requiring index manipulation
|
|
13
|
+
path: z.string().default(".").describe("Path to the Git repository."),
|
|
14
|
+
branch: z.string().min(1).describe("The name of the branch to merge into the current branch."),
|
|
15
|
+
commitMessage: z.string().optional().describe("Commit message for the merge commit."),
|
|
16
|
+
noFf: z.boolean().default(false).describe("Create a merge commit even if a fast-forward is possible."),
|
|
17
|
+
squash: z.boolean().default(false).describe("Combine merged changes into a single commit (requires manual commit)."),
|
|
18
|
+
abort: z.boolean().default(false).describe("Abort the current merge process."),
|
|
19
|
+
});
|
|
20
|
+
// 2. DEFINE the Zod response schema.
|
|
21
|
+
export const GitMergeOutputSchema = 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
|
+
conflict: z.boolean().optional().describe("True if the merge resulted in conflicts."),
|
|
25
|
+
fastForward: z.boolean().optional().describe("True if the merge was a fast-forward."),
|
|
26
|
+
aborted: z.boolean().optional().describe("True if the merge was aborted."),
|
|
27
|
+
needsManualCommit: z.boolean().optional().describe("True if --squash was used."),
|
|
41
28
|
});
|
|
42
29
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* @param {GitMergeInput} input - The validated input object.
|
|
46
|
-
* @param {RequestContext} context - The request context.
|
|
47
|
-
* @returns {Promise<GitMergeResult>} A promise that resolves with the structured merge result.
|
|
48
|
-
* @throws {McpError} Throws an McpError for path issues, command failures, or unexpected errors.
|
|
30
|
+
* 4. IMPLEMENT the core logic function.
|
|
31
|
+
* @throws {McpError} If the logic encounters an unrecoverable issue.
|
|
49
32
|
*/
|
|
50
|
-
export async function gitMergeLogic(
|
|
33
|
+
export async function gitMergeLogic(params, context) {
|
|
51
34
|
const operation = "gitMergeLogic";
|
|
52
|
-
logger.debug(`Executing ${operation}`, { ...context,
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
let resolvedPath;
|
|
57
|
-
if (input.path && input.path !== ".") {
|
|
58
|
-
// If a specific path is given, resolve it absolutely first
|
|
59
|
-
// Assuming input.path could be relative *to the server's CWD* if no session WD is set,
|
|
60
|
-
// but it's safer to require absolute paths or rely on session WD.
|
|
61
|
-
// For simplicity, let's assume input.path is intended relative to session WD if set, or absolute otherwise.
|
|
62
|
-
const workingDir = context.getWorkingDirectory();
|
|
63
|
-
if (workingDir) {
|
|
64
|
-
resolvedPath = path.resolve(workingDir, input.path); // Resolve relative to session WD
|
|
65
|
-
}
|
|
66
|
-
else if (path.isAbsolute(input.path)) {
|
|
67
|
-
resolvedPath = input.path; // Use absolute path directly
|
|
68
|
-
}
|
|
69
|
-
else {
|
|
70
|
-
// If relative path given without session WD, it's ambiguous. Error out.
|
|
71
|
-
throw new McpError(BaseErrorCode.VALIDATION_ERROR, "Relative path provided but no working directory set for the session.", { context, operation });
|
|
72
|
-
}
|
|
73
|
-
logger.debug(`Resolved provided path: ${resolvedPath}`, {
|
|
74
|
-
...context,
|
|
75
|
-
operation,
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
else {
|
|
79
|
-
const workingDir = context.getWorkingDirectory();
|
|
80
|
-
if (!workingDir) {
|
|
81
|
-
throw new McpError(BaseErrorCode.VALIDATION_ERROR, "No path provided and no working directory set for the session.", { context, operation });
|
|
82
|
-
}
|
|
83
|
-
resolvedPath = workingDir; // Use session working directory
|
|
84
|
-
logger.debug(`Using session working directory: ${resolvedPath}`, {
|
|
85
|
-
...context,
|
|
86
|
-
operation,
|
|
87
|
-
sessionId: context.sessionId,
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
// Sanitize the resolved path
|
|
91
|
-
// We assume the resolved path should be absolute for git commands.
|
|
92
|
-
// sanitizePath checks for traversal and normalizes.
|
|
93
|
-
targetPath = sanitization.sanitizePath(resolvedPath, {
|
|
94
|
-
allowAbsolute: true,
|
|
95
|
-
}).sanitizedPath;
|
|
96
|
-
logger.debug(`Sanitized path: ${targetPath}`, { ...context, operation });
|
|
35
|
+
logger.debug(`Executing ${operation}`, { ...context, params });
|
|
36
|
+
const workingDir = context.getWorkingDirectory();
|
|
37
|
+
if (params.path === "." && !workingDir) {
|
|
38
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, "No session working directory set. Please specify a 'path' or use 'git_set_working_dir' first.");
|
|
97
39
|
}
|
|
98
|
-
|
|
99
|
-
logger.error("Path resolution or sanitization failed", {
|
|
100
|
-
...context,
|
|
101
|
-
operation,
|
|
102
|
-
error,
|
|
103
|
-
});
|
|
104
|
-
if (error instanceof McpError) {
|
|
105
|
-
throw error;
|
|
106
|
-
}
|
|
107
|
-
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
|
|
108
|
-
}
|
|
109
|
-
// --- Construct the git merge command ---
|
|
40
|
+
const targetPath = sanitization.sanitizePath(params.path === "." ? workingDir : params.path, { allowAbsolute: true }).sanitizedPath;
|
|
110
41
|
const args = ["-C", targetPath, "merge"];
|
|
111
|
-
if (
|
|
42
|
+
if (params.abort) {
|
|
112
43
|
args.push("--abort");
|
|
113
44
|
}
|
|
114
45
|
else {
|
|
115
|
-
|
|
116
|
-
if (input.noFf) {
|
|
46
|
+
if (params.noFf)
|
|
117
47
|
args.push("--no-ff");
|
|
118
|
-
|
|
119
|
-
if (input.squash) {
|
|
48
|
+
if (params.squash)
|
|
120
49
|
args.push("--squash");
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
args.push("-m", input.commitMessage);
|
|
125
|
-
}
|
|
126
|
-
else if (input.squash && input.commitMessage) {
|
|
127
|
-
logger.warning("Commit message provided with --squash, but it will be ignored. Squash requires a separate commit.", { ...context, operation });
|
|
128
|
-
}
|
|
129
|
-
args.push(input.branch); // Add branch to merge
|
|
50
|
+
if (params.commitMessage && !params.squash)
|
|
51
|
+
args.push("-m", params.commitMessage);
|
|
52
|
+
args.push(params.branch);
|
|
130
53
|
}
|
|
131
|
-
logger.debug(`Executing command: git ${args.join(" ")}`, {
|
|
132
|
-
...context,
|
|
133
|
-
operation,
|
|
134
|
-
});
|
|
135
|
-
// --- Execute and Parse ---
|
|
136
54
|
try {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
logger.debug(`Command stderr: ${stderr}`, { ...context, operation }); // Log stderr even on success
|
|
141
|
-
const result = {
|
|
55
|
+
logger.debug(`Executing command: git ${args.join(" ")}`, { ...context, operation });
|
|
56
|
+
const { stdout } = await execFileAsync("git", args);
|
|
57
|
+
return {
|
|
142
58
|
success: true,
|
|
143
|
-
message: stdout.trim() ||
|
|
59
|
+
message: stdout.trim() || "Merge command executed successfully.",
|
|
60
|
+
fastForward: stdout.includes("Fast-forward"),
|
|
61
|
+
needsManualCommit: params.squash,
|
|
62
|
+
aborted: params.abort,
|
|
144
63
|
};
|
|
145
|
-
if (input.abort) {
|
|
146
|
-
result.aborted = true;
|
|
147
|
-
result.message = "Merge aborted successfully.";
|
|
148
|
-
}
|
|
149
|
-
else if (stdout.includes("Fast-forward")) {
|
|
150
|
-
result.fastForward = true;
|
|
151
|
-
}
|
|
152
|
-
else if (stdout.includes("Merge made by") || stdout.includes("merging")) {
|
|
153
|
-
const match = stdout.match(/Merge commit '([a-f0-9]+)'/);
|
|
154
|
-
result.mergedCommitHash = match ? match[1] : undefined;
|
|
155
|
-
result.fastForward = false;
|
|
156
|
-
}
|
|
157
|
-
else if (stdout.includes("Squash commit -- not updating HEAD")) {
|
|
158
|
-
result.needsManualCommit = true;
|
|
159
|
-
}
|
|
160
|
-
else if (stdout.includes("Already up to date")) {
|
|
161
|
-
result.fastForward = true;
|
|
162
|
-
}
|
|
163
|
-
logger.info("git merge executed successfully", {
|
|
164
|
-
...context,
|
|
165
|
-
operation,
|
|
166
|
-
path: targetPath,
|
|
167
|
-
result,
|
|
168
|
-
});
|
|
169
|
-
return result;
|
|
170
64
|
}
|
|
171
65
|
catch (error) {
|
|
172
|
-
const errorMessage = error.stderr || error.stdout || error.message || "";
|
|
173
|
-
logger.error(`Git merge command failed`, {
|
|
174
|
-
...context,
|
|
175
|
-
operation,
|
|
176
|
-
path: targetPath,
|
|
177
|
-
error: error.message,
|
|
178
|
-
output: errorMessage,
|
|
179
|
-
});
|
|
180
|
-
if (input.abort) {
|
|
181
|
-
// If abort failed, it's likely there was no merge in progress
|
|
182
|
-
if (errorMessage.includes("fatal: There is no merge to abort")) {
|
|
183
|
-
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `No merge in progress to abort.`, { context, operation, originalError: error });
|
|
184
|
-
}
|
|
185
|
-
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to abort merge: ${errorMessage}`, { context, operation, originalError: error });
|
|
186
|
-
}
|
|
187
|
-
// Check for specific failure scenarios
|
|
66
|
+
const errorMessage = error.stderr || error.stdout || error.message || "";
|
|
67
|
+
logger.error(`Git merge command failed`, { ...context, operation, errorMessage });
|
|
188
68
|
if (errorMessage.includes("CONFLICT")) {
|
|
189
|
-
throw new McpError(BaseErrorCode.CONFLICT,
|
|
69
|
+
throw new McpError(BaseErrorCode.CONFLICT, "Merge failed due to conflicts. Please resolve them and commit.");
|
|
190
70
|
}
|
|
191
|
-
if (errorMessage.includes("
|
|
192
|
-
throw new McpError(BaseErrorCode.VALIDATION_ERROR,
|
|
71
|
+
if (errorMessage.includes("unrelated histories")) {
|
|
72
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, "Merge failed: Refusing to merge unrelated histories.");
|
|
193
73
|
}
|
|
194
|
-
if (errorMessage.includes("
|
|
195
|
-
throw new McpError(BaseErrorCode.
|
|
74
|
+
if (errorMessage.includes("not a git repository")) {
|
|
75
|
+
throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`);
|
|
196
76
|
}
|
|
197
77
|
if (errorMessage.match(/fatal: '.*?' does not point to a commit/)) {
|
|
198
|
-
throw new McpError(BaseErrorCode.NOT_FOUND, `Merge failed: Branch '${
|
|
199
|
-
}
|
|
200
|
-
if (errorMessage.includes("fatal: You have not concluded your merge")) {
|
|
201
|
-
throw new McpError(BaseErrorCode.CONFLICT, `Merge failed: Conflicts still exist from a previous merge. Resolve conflicts or abort. Output: ${errorMessage}`, { context, operation, originalError: error });
|
|
202
|
-
}
|
|
203
|
-
if (errorMessage.includes("error: Your local changes to the following files would be overwritten by merge")) {
|
|
204
|
-
throw new McpError(BaseErrorCode.CONFLICT, `Merge failed: Local changes would be overwritten. Please commit or stash them.`, { context, operation, originalError: error });
|
|
78
|
+
throw new McpError(BaseErrorCode.NOT_FOUND, `Merge failed: Branch '${params.branch}' not found.`);
|
|
205
79
|
}
|
|
206
|
-
|
|
207
|
-
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Git merge command failed for path ${targetPath}: ${errorMessage}`, { context, operation, originalError: error });
|
|
80
|
+
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Git merge failed: ${errorMessage}`);
|
|
208
81
|
}
|
|
209
82
|
}
|
|
@@ -1,83 +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 { GitMergeInputSchema, gitMergeLogic, } 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_merge tool.
|
|
3
|
+
* @module src/mcp-server/tools/gitMerge/registration
|
|
15
4
|
*/
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
logger.info("State accessors initialized for git_merge tool registration.");
|
|
20
|
-
}
|
|
5
|
+
import { ErrorHandler, logger, requestContextService } from "../../../utils/index.js";
|
|
6
|
+
import { McpError, BaseErrorCode } from "../../../types-global/errors.js";
|
|
7
|
+
import { gitMergeLogic, GitMergeInputSchema, GitMergeOutputSchema, } from "./logic.js";
|
|
21
8
|
const TOOL_NAME = "git_merge";
|
|
22
9
|
const TOOL_DESCRIPTION = "Merges the specified branch into the current branch. Supports options like --no-ff, --squash, and --abort. Returns the merge result as a JSON object.";
|
|
23
10
|
/**
|
|
24
|
-
* Registers the git_merge 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_merge 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 registerGitMergeTool = async (server) => {
|
|
31
|
-
if (!_getWorkingDirectory || !_getSessionId) {
|
|
32
|
-
throw new Error("State accessors for git_merge must be initialized before registration.");
|
|
33
|
-
}
|
|
16
|
+
export const registerGitMergeTool = async (server, getWorkingDirectory, getSessionId) => {
|
|
34
17
|
const operation = "registerGitMergeTool";
|
|
35
18
|
const context = requestContextService.createRequestContext({ operation });
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
19
|
+
server.registerTool(TOOL_NAME, {
|
|
20
|
+
title: "Git Merge",
|
|
21
|
+
description: TOOL_DESCRIPTION,
|
|
22
|
+
inputSchema: GitMergeInputSchema.shape,
|
|
23
|
+
outputSchema: GitMergeOutputSchema.shape,
|
|
24
|
+
annotations: {
|
|
25
|
+
readOnlyHint: false,
|
|
26
|
+
destructiveHint: true, // Can create merge commits and change history
|
|
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 gitMergeLogic(params, {
|
|
38
|
+
...handlerContext,
|
|
39
|
+
getWorkingDirectory: () => getWorkingDirectory(sessionId),
|
|
43
40
|
});
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
};
|
|
48
|
-
const logicContext = {
|
|
49
|
-
...requestContext,
|
|
50
|
-
sessionId: sessionId,
|
|
51
|
-
getWorkingDirectory: getWorkingDirectoryForSession,
|
|
41
|
+
return {
|
|
42
|
+
structuredContent: result,
|
|
43
|
+
content: [{ type: "text", text: `Success: ${JSON.stringify(result, null, 2)}` }],
|
|
52
44
|
};
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
text: JSON.stringify(mergeResult, null, 2), // Pretty-print JSON
|
|
61
|
-
contentType: "application/json",
|
|
62
|
-
};
|
|
63
|
-
// Log based on the success flag in the result
|
|
64
|
-
if (mergeResult.success) {
|
|
65
|
-
logger.info(`Tool ${TOOL_NAME} executed successfully, returning JSON`, logicContext);
|
|
66
|
-
}
|
|
67
|
-
else {
|
|
68
|
-
// Log non-fatal conditions (like conflicts) differently from execution errors
|
|
69
|
-
logger.info(`Tool ${TOOL_NAME} completed with specific condition (e.g., conflict, no merge to abort), returning JSON`, logicContext);
|
|
70
|
-
}
|
|
71
|
-
// Even if success is false (e.g., conflicts), it's not a tool execution *error* in the MCP sense,
|
|
72
|
-
// the tool ran, but the git operation failed predictably. Return the structured result.
|
|
73
|
-
return { content: [resultContent] };
|
|
74
|
-
}, {
|
|
75
|
-
operation: toolOperation,
|
|
76
|
-
context: logicContext,
|
|
77
|
-
input: validatedArgs,
|
|
78
|
-
errorCode: BaseErrorCode.INTERNAL_ERROR, // Default for unexpected logic errors
|
|
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,
|
|
79
52
|
});
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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);
|
|
83
64
|
};
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Barrel file for the gitPull tool.
|
|
3
|
+
* @module src/mcp-server/tools/gitPull/index
|
|
3
4
|
*/
|
|
4
|
-
export { registerGitPullTool
|
|
5
|
-
// Export types if needed elsewhere, e.g.:
|
|
6
|
-
// export type { GitPullInput, GitPullResult } from './logic.js';
|
|
5
|
+
export { registerGitPullTool } from "./registration.js";
|
|
@@ -1,167 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Defines the core logic, schemas, and types for the git_pull tool.
|
|
3
|
+
* @module src/mcp-server/tools/gitPull/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 GitPullInputSchema = z.object({
|
|
13
|
-
path: z
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
.string()
|
|
25
|
-
.optional()
|
|
26
|
-
.describe("The remote branch to pull (e.g., 'main'). Defaults to the current branch's upstream."),
|
|
27
|
-
rebase: z
|
|
28
|
-
.boolean()
|
|
29
|
-
.optional()
|
|
30
|
-
.default(false)
|
|
31
|
-
.describe("Use 'git pull --rebase' instead of merge."),
|
|
32
|
-
ffOnly: z
|
|
33
|
-
.boolean()
|
|
34
|
-
.optional()
|
|
35
|
-
.default(false)
|
|
36
|
-
.describe("Use '--ff-only' to only allow fast-forward merges."),
|
|
37
|
-
// Add other relevant git pull options as needed (e.g., --prune, --tags, --depth)
|
|
13
|
+
path: z.string().default(".").describe("Path to the Git repository."),
|
|
14
|
+
remote: z.string().optional().describe("The remote repository to pull from (e.g., 'origin')."),
|
|
15
|
+
branch: z.string().optional().describe("The remote branch to pull."),
|
|
16
|
+
rebase: z.boolean().default(false).describe("Use 'git pull --rebase' instead of merge."),
|
|
17
|
+
ffOnly: z.boolean().default(false).describe("Only allow fast-forward merges."),
|
|
18
|
+
});
|
|
19
|
+
// 2. DEFINE the Zod response schema.
|
|
20
|
+
export const GitPullOutputSchema = 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
|
+
conflict: z.boolean().optional().describe("True if a merge conflict occurred."),
|
|
38
24
|
});
|
|
39
25
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* @param {GitPullInput} input - The validated input object.
|
|
43
|
-
* @param {RequestContext} context - The request context for logging and error handling, including session info and working dir getter.
|
|
44
|
-
* @returns {Promise<GitPullResult>} A promise that resolves with the structured pull result.
|
|
45
|
-
* @throws {McpError} Throws an McpError if path resolution, validation, or the git command fails unexpectedly.
|
|
26
|
+
* 4. IMPLEMENT the core logic function.
|
|
27
|
+
* @throws {McpError} If the logic encounters an unrecoverable issue.
|
|
46
28
|
*/
|
|
47
|
-
export async function pullGitChanges(
|
|
29
|
+
export async function pullGitChanges(params, context) {
|
|
48
30
|
const operation = "pullGitChanges";
|
|
49
|
-
logger.debug(`Executing ${operation}`, { ...context,
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (input.path && input.path !== ".") {
|
|
54
|
-
targetPath = input.path;
|
|
55
|
-
logger.debug(`Using provided path: ${targetPath}`, {
|
|
56
|
-
...context,
|
|
57
|
-
operation,
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
else {
|
|
61
|
-
const workingDir = context.getWorkingDirectory();
|
|
62
|
-
if (!workingDir) {
|
|
63
|
-
throw new McpError(BaseErrorCode.VALIDATION_ERROR, "No path provided and no working directory set for the session.", { context, operation });
|
|
64
|
-
}
|
|
65
|
-
targetPath = workingDir;
|
|
66
|
-
logger.debug(`Using session working directory: ${targetPath}`, {
|
|
67
|
-
...context,
|
|
68
|
-
operation,
|
|
69
|
-
sessionId: context.sessionId,
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
// Sanitize the resolved path
|
|
73
|
-
targetPath = sanitization.sanitizePath(targetPath, {
|
|
74
|
-
allowAbsolute: true,
|
|
75
|
-
}).sanitizedPath;
|
|
76
|
-
logger.debug("Sanitized path", {
|
|
77
|
-
...context,
|
|
78
|
-
operation,
|
|
79
|
-
sanitizedPath: targetPath,
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
catch (error) {
|
|
83
|
-
logger.error("Path resolution or sanitization failed", {
|
|
84
|
-
...context,
|
|
85
|
-
operation,
|
|
86
|
-
error,
|
|
87
|
-
});
|
|
88
|
-
if (error instanceof McpError)
|
|
89
|
-
throw error;
|
|
90
|
-
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
|
|
31
|
+
logger.debug(`Executing ${operation}`, { ...context, params });
|
|
32
|
+
const workingDir = context.getWorkingDirectory();
|
|
33
|
+
if (params.path === "." && !workingDir) {
|
|
34
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, "No session working directory set. Please specify a 'path' or use 'git_set_working_dir' first.");
|
|
91
35
|
}
|
|
36
|
+
const targetPath = sanitization.sanitizePath(params.path === "." ? workingDir : params.path, { allowAbsolute: true }).sanitizedPath;
|
|
37
|
+
const args = ["-C", targetPath, "pull"];
|
|
38
|
+
if (params.rebase)
|
|
39
|
+
args.push("--rebase");
|
|
40
|
+
if (params.ffOnly)
|
|
41
|
+
args.push("--ff-only");
|
|
42
|
+
if (params.remote)
|
|
43
|
+
args.push(params.remote);
|
|
44
|
+
if (params.branch)
|
|
45
|
+
args.push(params.branch);
|
|
92
46
|
try {
|
|
93
|
-
|
|
94
|
-
const args = ["-C", targetPath, "pull"];
|
|
95
|
-
if (input.rebase) {
|
|
96
|
-
args.push("--rebase");
|
|
97
|
-
}
|
|
98
|
-
if (input.ffOnly) {
|
|
99
|
-
args.push("--ff-only");
|
|
100
|
-
}
|
|
101
|
-
if (input.remote) {
|
|
102
|
-
args.push(input.remote);
|
|
103
|
-
if (input.branch) {
|
|
104
|
-
args.push(input.branch);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
else if (input.branch) {
|
|
108
|
-
// If only branch is specified, assume 'origin' or tracked remote
|
|
109
|
-
args.push("origin", input.branch); // Defaulting to origin if remote not specified but branch is
|
|
110
|
-
logger.warning(`Remote not specified, defaulting to 'origin' for branch pull`, { ...context, operation });
|
|
111
|
-
}
|
|
112
|
-
logger.debug(`Executing command: git ${args.join(" ")}`, {
|
|
113
|
-
...context,
|
|
114
|
-
operation,
|
|
115
|
-
});
|
|
47
|
+
logger.debug(`Executing command: git ${args.join(" ")}`, { ...context, operation });
|
|
116
48
|
const { stdout, stderr } = await execFileAsync("git", args);
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
logger.debug(`Git pull stderr: ${stderr}`, { ...context, operation });
|
|
120
|
-
}
|
|
121
|
-
// Analyze stdout/stderr to determine the outcome
|
|
122
|
-
const message = stdout.trim() || stderr.trim() || "Pull command executed.";
|
|
123
|
-
const summary = message;
|
|
124
|
-
const conflict = message.includes("CONFLICT");
|
|
125
|
-
logger.info("git pull executed successfully", {
|
|
126
|
-
...context,
|
|
127
|
-
operation,
|
|
128
|
-
path: targetPath,
|
|
129
|
-
summary,
|
|
130
|
-
conflict,
|
|
131
|
-
});
|
|
132
|
-
return { success: true, message, summary, conflict };
|
|
49
|
+
const message = stdout.trim() || stderr.trim() || "Pull command executed successfully.";
|
|
50
|
+
return { success: true, message, conflict: message.includes("CONFLICT") };
|
|
133
51
|
}
|
|
134
52
|
catch (error) {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
operation,
|
|
138
|
-
path: targetPath,
|
|
139
|
-
error: error.message,
|
|
140
|
-
stderr: error.stderr,
|
|
141
|
-
stdout: error.stdout,
|
|
142
|
-
});
|
|
143
|
-
const errorMessage = error.stderr || error.stdout || error.message || ""; // Check stdout too for errors
|
|
144
|
-
// Handle specific error cases
|
|
53
|
+
const errorMessage = error.stderr || error.stdout || error.message || "";
|
|
54
|
+
logger.error(`Failed to execute git pull command`, { ...context, operation, errorMessage });
|
|
145
55
|
if (errorMessage.toLowerCase().includes("not a git repository")) {
|
|
146
|
-
throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}
|
|
147
|
-
}
|
|
148
|
-
if (errorMessage.includes("resolve host") ||
|
|
149
|
-
errorMessage.includes("Could not read from remote repository")) {
|
|
150
|
-
throw new McpError(BaseErrorCode.SERVICE_UNAVAILABLE, `Failed to connect to remote repository. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
56
|
+
throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`);
|
|
151
57
|
}
|
|
152
|
-
if (errorMessage.includes("
|
|
153
|
-
|
|
154
|
-
// This might be caught here if execAsync throws due to non-zero exit code during conflict
|
|
155
|
-
throw new McpError(BaseErrorCode.CONFLICT, `Pull resulted in merge conflicts. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
58
|
+
if (errorMessage.includes("Could not read from remote repository")) {
|
|
59
|
+
throw new McpError(BaseErrorCode.SERVICE_UNAVAILABLE, "Failed to connect to remote repository.");
|
|
156
60
|
}
|
|
157
|
-
if (errorMessage.includes("
|
|
158
|
-
|
|
159
|
-
throw new McpError(BaseErrorCode.CONFLICT, `Pull failed due to uncommitted local changes. Please commit or stash them first. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
61
|
+
if (errorMessage.includes("merge conflict")) {
|
|
62
|
+
throw new McpError(BaseErrorCode.CONFLICT, "Pull resulted in merge conflicts.");
|
|
160
63
|
}
|
|
161
|
-
if (errorMessage.includes("
|
|
162
|
-
throw new McpError(BaseErrorCode.VALIDATION_ERROR,
|
|
64
|
+
if (errorMessage.includes("unrelated histories")) {
|
|
65
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, "Pull failed: Refusing to merge unrelated histories.");
|
|
163
66
|
}
|
|
164
|
-
|
|
165
|
-
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to pull changes for path: ${targetPath}. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
67
|
+
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Git pull failed: ${errorMessage}`);
|
|
166
68
|
}
|
|
167
69
|
}
|