@cyanheads/git-mcp-server 2.1.0 → 2.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -11
- package/dist/config/index.js +7 -7
- package/dist/index.js +35 -21
- package/dist/mcp-server/server.js +72 -56
- package/dist/mcp-server/tools/gitAdd/index.js +1 -1
- package/dist/mcp-server/tools/gitAdd/logic.js +88 -39
- package/dist/mcp-server/tools/gitAdd/registration.js +17 -14
- package/dist/mcp-server/tools/gitBranch/index.js +1 -1
- package/dist/mcp-server/tools/gitBranch/logic.js +213 -85
- package/dist/mcp-server/tools/gitBranch/registration.js +16 -13
- package/dist/mcp-server/tools/gitCheckout/index.js +1 -1
- package/dist/mcp-server/tools/gitCheckout/logic.js +85 -145
- package/dist/mcp-server/tools/gitCheckout/registration.js +16 -14
- package/dist/mcp-server/tools/gitCherryPick/index.js +1 -1
- package/dist/mcp-server/tools/gitCherryPick/logic.js +100 -41
- package/dist/mcp-server/tools/gitCherryPick/registration.js +21 -14
- package/dist/mcp-server/tools/gitClean/index.js +1 -1
- package/dist/mcp-server/tools/gitClean/logic.js +93 -41
- package/dist/mcp-server/tools/gitClean/registration.js +19 -16
- package/dist/mcp-server/tools/gitClearWorkingDir/index.js +1 -1
- package/dist/mcp-server/tools/gitClearWorkingDir/logic.js +14 -11
- package/dist/mcp-server/tools/gitClearWorkingDir/registration.js +19 -13
- package/dist/mcp-server/tools/gitClone/index.js +1 -1
- package/dist/mcp-server/tools/gitClone/logic.js +89 -30
- package/dist/mcp-server/tools/gitClone/registration.js +15 -12
- package/dist/mcp-server/tools/gitCommit/index.js +1 -1
- package/dist/mcp-server/tools/gitCommit/logic.js +198 -76
- package/dist/mcp-server/tools/gitCommit/registration.js +23 -20
- package/dist/mcp-server/tools/gitDiff/index.js +1 -1
- package/dist/mcp-server/tools/gitDiff/logic.js +124 -44
- package/dist/mcp-server/tools/gitDiff/registration.js +16 -14
- package/dist/mcp-server/tools/gitFetch/index.js +1 -1
- package/dist/mcp-server/tools/gitFetch/logic.js +78 -49
- package/dist/mcp-server/tools/gitFetch/registration.js +16 -14
- package/dist/mcp-server/tools/gitInit/index.js +1 -1
- package/dist/mcp-server/tools/gitInit/logic.js +88 -34
- package/dist/mcp-server/tools/gitInit/registration.js +32 -18
- package/dist/mcp-server/tools/gitLog/index.js +1 -1
- package/dist/mcp-server/tools/gitLog/logic.js +133 -47
- package/dist/mcp-server/tools/gitLog/registration.js +16 -14
- package/dist/mcp-server/tools/gitMerge/index.js +1 -1
- package/dist/mcp-server/tools/gitMerge/logic.js +102 -61
- package/dist/mcp-server/tools/gitMerge/registration.js +17 -14
- package/dist/mcp-server/tools/gitPull/index.js +1 -1
- package/dist/mcp-server/tools/gitPull/logic.js +90 -69
- package/dist/mcp-server/tools/gitPull/registration.js +16 -14
- package/dist/mcp-server/tools/gitPush/index.js +1 -1
- package/dist/mcp-server/tools/gitPush/logic.js +116 -100
- package/dist/mcp-server/tools/gitPush/registration.js +16 -14
- package/dist/mcp-server/tools/gitRebase/index.js +1 -1
- package/dist/mcp-server/tools/gitRebase/logic.js +121 -82
- package/dist/mcp-server/tools/gitRebase/registration.js +21 -14
- package/dist/mcp-server/tools/gitRemote/index.js +1 -1
- package/dist/mcp-server/tools/gitRemote/logic.js +108 -52
- package/dist/mcp-server/tools/gitRemote/registration.js +14 -11
- package/dist/mcp-server/tools/gitReset/index.js +1 -1
- package/dist/mcp-server/tools/gitReset/logic.js +65 -37
- package/dist/mcp-server/tools/gitReset/registration.js +14 -12
- package/dist/mcp-server/tools/gitSetWorkingDir/index.js +1 -1
- package/dist/mcp-server/tools/gitSetWorkingDir/logic.js +74 -34
- package/dist/mcp-server/tools/gitSetWorkingDir/registration.js +18 -11
- package/dist/mcp-server/tools/gitShow/index.js +1 -1
- package/dist/mcp-server/tools/gitShow/logic.js +78 -35
- package/dist/mcp-server/tools/gitShow/registration.js +17 -12
- package/dist/mcp-server/tools/gitStash/index.js +1 -1
- package/dist/mcp-server/tools/gitStash/logic.js +143 -58
- package/dist/mcp-server/tools/gitStash/registration.js +19 -12
- package/dist/mcp-server/tools/gitStatus/index.js +1 -1
- package/dist/mcp-server/tools/gitStatus/logic.js +100 -58
- package/dist/mcp-server/tools/gitStatus/registration.js +15 -12
- package/dist/mcp-server/tools/gitTag/index.js +1 -1
- package/dist/mcp-server/tools/gitTag/logic.js +124 -51
- package/dist/mcp-server/tools/gitTag/registration.js +14 -11
- package/dist/mcp-server/tools/gitWorktree/index.js +1 -1
- package/dist/mcp-server/tools/gitWorktree/logic.js +204 -95
- package/dist/mcp-server/tools/gitWorktree/registration.js +14 -11
- package/dist/mcp-server/tools/gitWrapupInstructions/index.js +1 -1
- package/dist/mcp-server/tools/gitWrapupInstructions/logic.js +23 -11
- package/dist/mcp-server/tools/gitWrapupInstructions/registration.js +14 -12
- package/dist/mcp-server/transports/httpTransport.js +187 -79
- package/dist/mcp-server/transports/stdioTransport.js +14 -8
- package/dist/types-global/errors.js +9 -4
- package/dist/utils/index.js +4 -4
- package/dist/utils/internal/errorHandler.js +62 -40
- package/dist/utils/internal/index.js +3 -3
- package/dist/utils/internal/logger.js +97 -54
- package/dist/utils/internal/requestContext.js +7 -5
- package/dist/utils/metrics/index.js +1 -1
- package/dist/utils/metrics/tokenCounter.js +18 -14
- package/dist/utils/parsing/dateParser.js +5 -5
- package/dist/utils/parsing/index.js +2 -2
- package/dist/utils/parsing/jsonParser.js +20 -11
- package/dist/utils/security/idGenerator.js +8 -10
- package/dist/utils/security/index.js +3 -3
- package/dist/utils/security/rateLimiter.js +16 -14
- package/dist/utils/security/sanitization.js +139 -82
- package/package.json +45 -23
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// Import utils from barrel (ErrorHandler from ../utils/internal/errorHandler.js)
|
|
2
|
-
import { ErrorHandler } from
|
|
2
|
+
import { ErrorHandler } from "../../../utils/index.js";
|
|
3
3
|
// Import utils from barrel (logger from ../utils/internal/logger.js)
|
|
4
|
-
import { logger } from
|
|
4
|
+
import { logger } from "../../../utils/index.js";
|
|
5
5
|
// Import utils from barrel (requestContextService, RequestContext from ../utils/internal/requestContext.js)
|
|
6
|
-
import { BaseErrorCode } from
|
|
7
|
-
import { requestContextService } from
|
|
8
|
-
import { GitLogInputSchema, logGitHistory } from
|
|
6
|
+
import { BaseErrorCode } from "../../../types-global/errors.js"; // Keep direct import for types-global
|
|
7
|
+
import { requestContextService } from "../../../utils/index.js";
|
|
8
|
+
import { GitLogInputSchema, logGitHistory, } from "./logic.js";
|
|
9
9
|
let _getWorkingDirectory;
|
|
10
10
|
let _getSessionId;
|
|
11
11
|
/**
|
|
@@ -17,9 +17,9 @@ let _getSessionId;
|
|
|
17
17
|
export function initializeGitLogStateAccessors(getWdFn, getSidFn) {
|
|
18
18
|
_getWorkingDirectory = getWdFn;
|
|
19
19
|
_getSessionId = getSidFn;
|
|
20
|
-
logger.info(
|
|
20
|
+
logger.info("State accessors initialized for git_log tool registration.");
|
|
21
21
|
}
|
|
22
|
-
const TOOL_NAME =
|
|
22
|
+
const TOOL_NAME = "git_log";
|
|
23
23
|
const TOOL_DESCRIPTION = "Shows commit logs for the repository. Supports limiting count, filtering by author, date range, and specific branch/file. Returns a JSON object containing a list of commit objects (`commits` array) by default. If `showSignature: true` is used, it returns a JSON object where the `commits` array is empty and the raw signature verification output is included in the `message` field.";
|
|
24
24
|
/**
|
|
25
25
|
* Registers the git_log tool with the MCP server.
|
|
@@ -29,15 +29,18 @@ const TOOL_DESCRIPTION = "Shows commit logs for the repository. Supports limitin
|
|
|
29
29
|
*/
|
|
30
30
|
export async function registerGitLogTool(server) {
|
|
31
31
|
if (!_getWorkingDirectory || !_getSessionId) {
|
|
32
|
-
throw new Error(
|
|
32
|
+
throw new Error("State accessors for git_log must be initialized before registration.");
|
|
33
33
|
}
|
|
34
|
-
const operation =
|
|
34
|
+
const operation = "registerGitLogTool";
|
|
35
35
|
const context = requestContextService.createRequestContext({ operation });
|
|
36
36
|
await ErrorHandler.tryCatch(async () => {
|
|
37
37
|
server.tool(TOOL_NAME, TOOL_DESCRIPTION, GitLogInputSchema.shape, // Provide the Zod schema shape
|
|
38
38
|
async (validatedArgs, callContext) => {
|
|
39
|
-
const toolOperation =
|
|
40
|
-
const requestContext = requestContextService.createRequestContext({
|
|
39
|
+
const toolOperation = "tool:git_log";
|
|
40
|
+
const requestContext = requestContextService.createRequestContext({
|
|
41
|
+
operation: toolOperation,
|
|
42
|
+
parentContext: callContext,
|
|
43
|
+
});
|
|
41
44
|
const sessionId = _getSessionId(requestContext);
|
|
42
45
|
const getWorkingDirectoryForSession = () => {
|
|
43
46
|
return _getWorkingDirectory(sessionId);
|
|
@@ -53,10 +56,10 @@ export async function registerGitLogTool(server) {
|
|
|
53
56
|
const logResult = await logGitHistory(validatedArgs, logicContext);
|
|
54
57
|
// Format the result (array of commits) as a JSON string within TextContent
|
|
55
58
|
const resultContent = {
|
|
56
|
-
type:
|
|
59
|
+
type: "text",
|
|
57
60
|
// Stringify the entire GitLogResult object which includes the commits array and success flag
|
|
58
61
|
text: JSON.stringify(logResult, null, 2), // Pretty-print JSON
|
|
59
|
-
contentType:
|
|
62
|
+
contentType: "application/json",
|
|
60
63
|
};
|
|
61
64
|
logger.info(`Tool ${TOOL_NAME} executed successfully, returning JSON`, logicContext);
|
|
62
65
|
// Success is determined by the logic function and included in the result object
|
|
@@ -71,4 +74,3 @@ export async function registerGitLogTool(server) {
|
|
|
71
74
|
logger.info(`Tool registered: ${TOOL_NAME}`, context);
|
|
72
75
|
}, { operation, context, critical: true });
|
|
73
76
|
}
|
|
74
|
-
;
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
* @fileoverview Barrel file for the git_merge tool.
|
|
3
3
|
* Exports the registration function and state accessor initialization function.
|
|
4
4
|
*/
|
|
5
|
-
export { registerGitMergeTool, initializeGitMergeStateAccessors } from
|
|
5
|
+
export { registerGitMergeTool, initializeGitMergeStateAccessors, } from "./registration.js";
|
|
6
6
|
// Export types if needed elsewhere, e.g.:
|
|
7
7
|
// export type { GitMergeInput, GitMergeResult } from './logic.js';
|
|
@@ -1,22 +1,42 @@
|
|
|
1
|
-
import { exec } from
|
|
2
|
-
import { promisify } from
|
|
3
|
-
import { z } from
|
|
1
|
+
import { exec } from "child_process";
|
|
2
|
+
import { promisify } from "util";
|
|
3
|
+
import { z } from "zod";
|
|
4
4
|
// Import utils from barrel (logger from ../utils/internal/logger.js)
|
|
5
|
-
import { logger } from
|
|
5
|
+
import { logger } from "../../../utils/index.js";
|
|
6
6
|
// Import utils from barrel (RequestContext from ../utils/internal/requestContext.js)
|
|
7
|
-
import { BaseErrorCode, McpError } from
|
|
7
|
+
import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; // Keep direct import for types-global
|
|
8
8
|
// Import utils from barrel (sanitization from ../utils/security/sanitization.js)
|
|
9
|
-
import path from
|
|
10
|
-
import { sanitization } from
|
|
9
|
+
import path from "path"; // Import path module
|
|
10
|
+
import { sanitization } from "../../../utils/index.js";
|
|
11
11
|
const execAsync = promisify(exec);
|
|
12
12
|
// Define the input schema for the git_merge tool
|
|
13
13
|
export const GitMergeInputSchema = z.object({
|
|
14
|
-
path: z
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
path: z
|
|
15
|
+
.string()
|
|
16
|
+
.min(1)
|
|
17
|
+
.optional()
|
|
18
|
+
.default(".")
|
|
19
|
+
.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."),
|
|
20
|
+
branch: z
|
|
21
|
+
.string()
|
|
22
|
+
.min(1)
|
|
23
|
+
.describe("The name of the branch to merge into the current branch."),
|
|
24
|
+
commitMessage: z
|
|
25
|
+
.string()
|
|
26
|
+
.optional()
|
|
27
|
+
.describe("Commit message to use for the merge commit (if required, e.g., not fast-forward)."),
|
|
28
|
+
noFf: z
|
|
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)."),
|
|
20
40
|
// 'continue' might be too complex for initial implementation due to requiring index manipulation
|
|
21
41
|
});
|
|
22
42
|
/**
|
|
@@ -28,13 +48,13 @@ export const GitMergeInputSchema = z.object({
|
|
|
28
48
|
* @throws {McpError} Throws an McpError for path issues, command failures, or unexpected errors.
|
|
29
49
|
*/
|
|
30
50
|
export async function gitMergeLogic(input, context) {
|
|
31
|
-
const operation =
|
|
51
|
+
const operation = "gitMergeLogic";
|
|
32
52
|
logger.debug(`Executing ${operation}`, { ...context, input });
|
|
33
53
|
let targetPath;
|
|
34
54
|
try {
|
|
35
55
|
// Resolve the target path
|
|
36
56
|
let resolvedPath;
|
|
37
|
-
if (input.path && input.path !==
|
|
57
|
+
if (input.path && input.path !== ".") {
|
|
38
58
|
// If a specific path is given, resolve it absolutely first
|
|
39
59
|
// Assuming input.path could be relative *to the server's CWD* if no session WD is set,
|
|
40
60
|
// but it's safer to require absolute paths or rely on session WD.
|
|
@@ -50,7 +70,10 @@ export async function gitMergeLogic(input, context) {
|
|
|
50
70
|
// If relative path given without session WD, it's ambiguous. Error out.
|
|
51
71
|
throw new McpError(BaseErrorCode.VALIDATION_ERROR, "Relative path provided but no working directory set for the session.", { context, operation });
|
|
52
72
|
}
|
|
53
|
-
logger.debug(`Resolved provided path: ${resolvedPath}`, {
|
|
73
|
+
logger.debug(`Resolved provided path: ${resolvedPath}`, {
|
|
74
|
+
...context,
|
|
75
|
+
operation,
|
|
76
|
+
});
|
|
54
77
|
}
|
|
55
78
|
else {
|
|
56
79
|
const workingDir = context.getWorkingDirectory();
|
|
@@ -58,16 +81,26 @@ export async function gitMergeLogic(input, context) {
|
|
|
58
81
|
throw new McpError(BaseErrorCode.VALIDATION_ERROR, "No path provided and no working directory set for the session.", { context, operation });
|
|
59
82
|
}
|
|
60
83
|
resolvedPath = workingDir; // Use session working directory
|
|
61
|
-
logger.debug(`Using session working directory: ${resolvedPath}`, {
|
|
84
|
+
logger.debug(`Using session working directory: ${resolvedPath}`, {
|
|
85
|
+
...context,
|
|
86
|
+
operation,
|
|
87
|
+
sessionId: context.sessionId,
|
|
88
|
+
});
|
|
62
89
|
}
|
|
63
90
|
// Sanitize the resolved path
|
|
64
91
|
// We assume the resolved path should be absolute for git commands.
|
|
65
92
|
// sanitizePath checks for traversal and normalizes.
|
|
66
|
-
targetPath = sanitization.sanitizePath(resolvedPath, {
|
|
93
|
+
targetPath = sanitization.sanitizePath(resolvedPath, {
|
|
94
|
+
allowAbsolute: true,
|
|
95
|
+
}).sanitizedPath;
|
|
67
96
|
logger.debug(`Sanitized path: ${targetPath}`, { ...context, operation });
|
|
68
97
|
}
|
|
69
98
|
catch (error) {
|
|
70
|
-
logger.error(
|
|
99
|
+
logger.error("Path resolution or sanitization failed", {
|
|
100
|
+
...context,
|
|
101
|
+
operation,
|
|
102
|
+
error,
|
|
103
|
+
});
|
|
71
104
|
if (error instanceof McpError) {
|
|
72
105
|
throw error;
|
|
73
106
|
}
|
|
@@ -76,15 +109,16 @@ export async function gitMergeLogic(input, context) {
|
|
|
76
109
|
// --- Construct the git merge command ---
|
|
77
110
|
let command = `git -C "${targetPath}" merge`;
|
|
78
111
|
if (input.abort) {
|
|
79
|
-
command +=
|
|
112
|
+
command += " --abort";
|
|
80
113
|
}
|
|
81
114
|
else {
|
|
82
115
|
// Standard merge options
|
|
83
116
|
if (input.noFf)
|
|
84
|
-
command +=
|
|
117
|
+
command += " --no-ff";
|
|
85
118
|
if (input.squash)
|
|
86
|
-
command +=
|
|
87
|
-
if (input.commitMessage && !input.squash) {
|
|
119
|
+
command += " --squash";
|
|
120
|
+
if (input.commitMessage && !input.squash) {
|
|
121
|
+
// Commit message only relevant if not squashing (squash requires separate commit)
|
|
88
122
|
command += ` -m "${input.commitMessage.replace(/"/g, '\\"')}"`;
|
|
89
123
|
}
|
|
90
124
|
else if (input.squash && input.commitMessage) {
|
|
@@ -99,62 +133,69 @@ export async function gitMergeLogic(input, context) {
|
|
|
99
133
|
logger.debug(`Command stdout: ${stdout}`, { ...context, operation });
|
|
100
134
|
if (stderr)
|
|
101
135
|
logger.debug(`Command stderr: ${stderr}`, { ...context, operation }); // Log stderr even on success
|
|
136
|
+
const result = {
|
|
137
|
+
success: true,
|
|
138
|
+
message: stdout.trim() || stderr.trim() || "Merge command executed.",
|
|
139
|
+
};
|
|
102
140
|
if (input.abort) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (stdout.includes(
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
if (stdout.includes(
|
|
110
|
-
const match = stdout.match(/Merge commit '([a-f0-9]+)'/);
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
// If none of the above, return generic success based on exit code 0
|
|
129
|
-
return { success: true, message: `Merge command completed: ${stdout.trim()}` };
|
|
141
|
+
result.aborted = true;
|
|
142
|
+
result.message = "Merge aborted successfully.";
|
|
143
|
+
}
|
|
144
|
+
else if (stdout.includes("Fast-forward")) {
|
|
145
|
+
result.fastForward = true;
|
|
146
|
+
}
|
|
147
|
+
else if (stdout.includes("Merge made by") || stdout.includes("merging")) {
|
|
148
|
+
const match = stdout.match(/Merge commit '([a-f0-9]+)'/);
|
|
149
|
+
result.mergedCommitHash = match ? match[1] : undefined;
|
|
150
|
+
result.fastForward = false;
|
|
151
|
+
}
|
|
152
|
+
else if (stdout.includes("Squash commit -- not updating HEAD")) {
|
|
153
|
+
result.needsManualCommit = true;
|
|
154
|
+
}
|
|
155
|
+
else if (stdout.includes("Already up to date")) {
|
|
156
|
+
result.fastForward = true;
|
|
157
|
+
}
|
|
158
|
+
logger.info("git merge executed successfully", {
|
|
159
|
+
...context,
|
|
160
|
+
operation,
|
|
161
|
+
path: targetPath,
|
|
162
|
+
result,
|
|
163
|
+
});
|
|
164
|
+
return result;
|
|
130
165
|
}
|
|
131
166
|
catch (error) {
|
|
132
|
-
const errorMessage = error.stderr || error.stdout || error.message ||
|
|
133
|
-
logger.error(`Git merge command failed`, {
|
|
167
|
+
const errorMessage = error.stderr || error.stdout || error.message || ""; // Git often puts errors in stdout/stderr
|
|
168
|
+
logger.error(`Git merge command failed`, {
|
|
169
|
+
...context,
|
|
170
|
+
operation,
|
|
171
|
+
path: targetPath,
|
|
172
|
+
error: error.message,
|
|
173
|
+
output: errorMessage,
|
|
174
|
+
});
|
|
134
175
|
if (input.abort) {
|
|
135
176
|
// If abort failed, it's likely there was no merge in progress
|
|
136
|
-
if (errorMessage.includes(
|
|
137
|
-
|
|
177
|
+
if (errorMessage.includes("fatal: There is no merge to abort")) {
|
|
178
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `No merge in progress to abort.`, { context, operation, originalError: error });
|
|
138
179
|
}
|
|
139
180
|
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to abort merge: ${errorMessage}`, { context, operation, originalError: error });
|
|
140
181
|
}
|
|
141
182
|
// Check for specific failure scenarios
|
|
142
|
-
if (errorMessage.includes(
|
|
143
|
-
|
|
183
|
+
if (errorMessage.includes("CONFLICT")) {
|
|
184
|
+
throw new McpError(BaseErrorCode.CONFLICT, `Merge failed due to conflicts. Please resolve conflicts and commit. Output: ${errorMessage}`, { context, operation, originalError: error });
|
|
144
185
|
}
|
|
145
|
-
if (errorMessage.includes(
|
|
186
|
+
if (errorMessage.includes("refusing to merge unrelated histories")) {
|
|
146
187
|
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Merge failed: Refusing to merge unrelated histories. Consider using '--allow-unrelated-histories'.`, { context, operation, originalError: error });
|
|
147
188
|
}
|
|
148
|
-
if (errorMessage.includes(
|
|
189
|
+
if (errorMessage.includes("fatal: Not possible to fast-forward, aborting.")) {
|
|
149
190
|
throw new McpError(BaseErrorCode.CONFLICT, `Merge failed: Not possible to fast-forward. Merge required.`, { context, operation, originalError: error });
|
|
150
191
|
}
|
|
151
192
|
if (errorMessage.match(/fatal: '.*?' does not point to a commit/)) {
|
|
152
193
|
throw new McpError(BaseErrorCode.NOT_FOUND, `Merge failed: Branch '${input.branch}' not found or does not point to a commit.`, { context, operation, originalError: error });
|
|
153
194
|
}
|
|
154
|
-
if (errorMessage.includes(
|
|
155
|
-
|
|
195
|
+
if (errorMessage.includes("fatal: You have not concluded your merge")) {
|
|
196
|
+
throw new McpError(BaseErrorCode.CONFLICT, `Merge failed: Conflicts still exist from a previous merge. Resolve conflicts or abort. Output: ${errorMessage}`, { context, operation, originalError: error });
|
|
156
197
|
}
|
|
157
|
-
if (errorMessage.includes(
|
|
198
|
+
if (errorMessage.includes("error: Your local changes to the following files would be overwritten by merge")) {
|
|
158
199
|
throw new McpError(BaseErrorCode.CONFLICT, `Merge failed: Local changes would be overwritten. Please commit or stash them.`, { context, operation, originalError: error });
|
|
159
200
|
}
|
|
160
201
|
// Generic error
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// Import utils from barrel (logger from ../utils/internal/logger.js)
|
|
2
|
-
import { logger } from
|
|
2
|
+
import { logger } from "../../../utils/index.js";
|
|
3
3
|
// Import utils from barrel (ErrorHandler from ../utils/internal/errorHandler.js)
|
|
4
|
-
import { ErrorHandler } from
|
|
4
|
+
import { ErrorHandler } from "../../../utils/index.js";
|
|
5
5
|
// Import utils from barrel (requestContextService from ../utils/internal/requestContext.js)
|
|
6
|
-
import { BaseErrorCode } from
|
|
7
|
-
import { requestContextService } from
|
|
8
|
-
import { GitMergeInputSchema, gitMergeLogic } from
|
|
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
9
|
let _getWorkingDirectory;
|
|
10
10
|
let _getSessionId;
|
|
11
11
|
/**
|
|
@@ -16,10 +16,10 @@ let _getSessionId;
|
|
|
16
16
|
export function initializeGitMergeStateAccessors(getWdFn, getSidFn) {
|
|
17
17
|
_getWorkingDirectory = getWdFn;
|
|
18
18
|
_getSessionId = getSidFn;
|
|
19
|
-
logger.info(
|
|
19
|
+
logger.info("State accessors initialized for git_merge tool registration.");
|
|
20
20
|
}
|
|
21
|
-
const TOOL_NAME =
|
|
22
|
-
const TOOL_DESCRIPTION =
|
|
21
|
+
const TOOL_NAME = "git_merge";
|
|
22
|
+
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
23
|
/**
|
|
24
24
|
* Registers the git_merge tool with the MCP server.
|
|
25
25
|
*
|
|
@@ -29,15 +29,18 @@ const TOOL_DESCRIPTION = 'Merges the specified branch into the current branch. S
|
|
|
29
29
|
*/
|
|
30
30
|
export const registerGitMergeTool = async (server) => {
|
|
31
31
|
if (!_getWorkingDirectory || !_getSessionId) {
|
|
32
|
-
throw new Error(
|
|
32
|
+
throw new Error("State accessors for git_merge must be initialized before registration.");
|
|
33
33
|
}
|
|
34
|
-
const operation =
|
|
34
|
+
const operation = "registerGitMergeTool";
|
|
35
35
|
const context = requestContextService.createRequestContext({ operation });
|
|
36
36
|
await ErrorHandler.tryCatch(async () => {
|
|
37
37
|
server.tool(TOOL_NAME, TOOL_DESCRIPTION, GitMergeInputSchema.shape, // Provide the Zod schema shape
|
|
38
38
|
async (validatedArgs, callContext) => {
|
|
39
|
-
const toolOperation =
|
|
40
|
-
const requestContext = requestContextService.createRequestContext({
|
|
39
|
+
const toolOperation = "tool:git_merge";
|
|
40
|
+
const requestContext = requestContextService.createRequestContext({
|
|
41
|
+
operation: toolOperation,
|
|
42
|
+
parentContext: callContext,
|
|
43
|
+
});
|
|
41
44
|
const sessionId = _getSessionId(requestContext);
|
|
42
45
|
const getWorkingDirectoryForSession = () => {
|
|
43
46
|
return _getWorkingDirectory(sessionId);
|
|
@@ -53,9 +56,9 @@ export const registerGitMergeTool = async (server) => {
|
|
|
53
56
|
const mergeResult = await gitMergeLogic(validatedArgs, logicContext);
|
|
54
57
|
// Format the result as a JSON string within TextContent
|
|
55
58
|
const resultContent = {
|
|
56
|
-
type:
|
|
59
|
+
type: "text",
|
|
57
60
|
text: JSON.stringify(mergeResult, null, 2), // Pretty-print JSON
|
|
58
|
-
contentType:
|
|
61
|
+
contentType: "application/json",
|
|
59
62
|
};
|
|
60
63
|
// Log based on the success flag in the result
|
|
61
64
|
if (mergeResult.success) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Barrel file for the gitPull tool.
|
|
3
3
|
*/
|
|
4
|
-
export { registerGitPullTool, initializeGitPullStateAccessors } from
|
|
4
|
+
export { registerGitPullTool, initializeGitPullStateAccessors, } from "./registration.js";
|
|
5
5
|
// Export types if needed elsewhere, e.g.:
|
|
6
6
|
// export type { GitPullInput, GitPullResult } from './logic.js';
|
|
@@ -1,20 +1,39 @@
|
|
|
1
|
-
import { exec } from
|
|
2
|
-
import { promisify } from
|
|
3
|
-
import { z } from
|
|
1
|
+
import { exec } from "child_process";
|
|
2
|
+
import { promisify } from "util";
|
|
3
|
+
import { z } from "zod";
|
|
4
4
|
// Import utils from barrel (logger from ../utils/internal/logger.js)
|
|
5
|
-
import { logger } from
|
|
5
|
+
import { logger } from "../../../utils/index.js";
|
|
6
6
|
// Import utils from barrel (RequestContext from ../utils/internal/requestContext.js)
|
|
7
|
-
import { BaseErrorCode, McpError } from
|
|
7
|
+
import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; // Keep direct import for types-global
|
|
8
8
|
// Import utils from barrel (sanitization from ../utils/security/sanitization.js)
|
|
9
|
-
import { sanitization } from
|
|
9
|
+
import { sanitization } from "../../../utils/index.js";
|
|
10
10
|
const execAsync = promisify(exec);
|
|
11
11
|
// Define the input schema for the git_pull tool using Zod
|
|
12
12
|
export const GitPullInputSchema = z.object({
|
|
13
|
-
path: z
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
path: z
|
|
14
|
+
.string()
|
|
15
|
+
.min(1)
|
|
16
|
+
.optional()
|
|
17
|
+
.default(".")
|
|
18
|
+
.describe("Path to the Git repository. Defaults to the directory set via `git_set_working_dir` for the session; set 'git_set_working_dir' if not set."),
|
|
19
|
+
remote: z
|
|
20
|
+
.string()
|
|
21
|
+
.optional()
|
|
22
|
+
.describe("The remote repository to pull from (e.g., 'origin'). Defaults to the tracked upstream or 'origin'."),
|
|
23
|
+
branch: z
|
|
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."),
|
|
18
37
|
// Add other relevant git pull options as needed (e.g., --prune, --tags, --depth)
|
|
19
38
|
});
|
|
20
39
|
/**
|
|
@@ -26,14 +45,17 @@ export const GitPullInputSchema = z.object({
|
|
|
26
45
|
* @throws {McpError} Throws an McpError if path resolution, validation, or the git command fails unexpectedly.
|
|
27
46
|
*/
|
|
28
47
|
export async function pullGitChanges(input, context) {
|
|
29
|
-
const operation =
|
|
48
|
+
const operation = "pullGitChanges";
|
|
30
49
|
logger.debug(`Executing ${operation}`, { ...context, input });
|
|
31
50
|
let targetPath;
|
|
32
51
|
try {
|
|
33
52
|
// Resolve the target path
|
|
34
|
-
if (input.path && input.path !==
|
|
53
|
+
if (input.path && input.path !== ".") {
|
|
35
54
|
targetPath = input.path;
|
|
36
|
-
logger.debug(`Using provided path: ${targetPath}`, {
|
|
55
|
+
logger.debug(`Using provided path: ${targetPath}`, {
|
|
56
|
+
...context,
|
|
57
|
+
operation,
|
|
58
|
+
});
|
|
37
59
|
}
|
|
38
60
|
else {
|
|
39
61
|
const workingDir = context.getWorkingDirectory();
|
|
@@ -41,14 +63,28 @@ export async function pullGitChanges(input, context) {
|
|
|
41
63
|
throw new McpError(BaseErrorCode.VALIDATION_ERROR, "No path provided and no working directory set for the session.", { context, operation });
|
|
42
64
|
}
|
|
43
65
|
targetPath = workingDir;
|
|
44
|
-
logger.debug(`Using session working directory: ${targetPath}`, {
|
|
66
|
+
logger.debug(`Using session working directory: ${targetPath}`, {
|
|
67
|
+
...context,
|
|
68
|
+
operation,
|
|
69
|
+
sessionId: context.sessionId,
|
|
70
|
+
});
|
|
45
71
|
}
|
|
46
72
|
// Sanitize the resolved path
|
|
47
|
-
targetPath = sanitization.sanitizePath(targetPath, {
|
|
48
|
-
|
|
73
|
+
targetPath = sanitization.sanitizePath(targetPath, {
|
|
74
|
+
allowAbsolute: true,
|
|
75
|
+
}).sanitizedPath;
|
|
76
|
+
logger.debug("Sanitized path", {
|
|
77
|
+
...context,
|
|
78
|
+
operation,
|
|
79
|
+
sanitizedPath: targetPath,
|
|
80
|
+
});
|
|
49
81
|
}
|
|
50
82
|
catch (error) {
|
|
51
|
-
logger.error(
|
|
83
|
+
logger.error("Path resolution or sanitization failed", {
|
|
84
|
+
...context,
|
|
85
|
+
operation,
|
|
86
|
+
error,
|
|
87
|
+
});
|
|
52
88
|
if (error instanceof McpError)
|
|
53
89
|
throw error;
|
|
54
90
|
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
|
|
@@ -57,88 +93,73 @@ export async function pullGitChanges(input, context) {
|
|
|
57
93
|
// Construct the git pull command
|
|
58
94
|
let command = `git -C "${targetPath}" pull`;
|
|
59
95
|
if (input.rebase) {
|
|
60
|
-
command +=
|
|
96
|
+
command += " --rebase";
|
|
61
97
|
}
|
|
62
98
|
if (input.ffOnly) {
|
|
63
|
-
command +=
|
|
99
|
+
command += " --ff-only";
|
|
64
100
|
}
|
|
65
101
|
if (input.remote) {
|
|
66
102
|
// Sanitize remote and branch names - basic alphanumeric + common chars
|
|
67
|
-
const safeRemote = input.remote.replace(/[^a-zA-Z0-9_.\-/]/g,
|
|
103
|
+
const safeRemote = input.remote.replace(/[^a-zA-Z0-9_.\-/]/g, "");
|
|
68
104
|
command += ` ${safeRemote}`;
|
|
69
105
|
if (input.branch) {
|
|
70
|
-
const safeBranch = input.branch.replace(/[^a-zA-Z0-9_.\-/]/g,
|
|
106
|
+
const safeBranch = input.branch.replace(/[^a-zA-Z0-9_.\-/]/g, "");
|
|
71
107
|
command += ` ${safeBranch}`;
|
|
72
108
|
}
|
|
73
109
|
}
|
|
74
110
|
else if (input.branch) {
|
|
75
111
|
// If only branch is specified, assume 'origin' or tracked remote
|
|
76
|
-
const safeBranch = input.branch.replace(/[^a-zA-Z0-9_.\-/]/g,
|
|
112
|
+
const safeBranch = input.branch.replace(/[^a-zA-Z0-9_.\-/]/g, "");
|
|
77
113
|
command += ` origin ${safeBranch}`; // Defaulting to origin if remote not specified but branch is
|
|
78
114
|
logger.warning(`Remote not specified, defaulting to 'origin' for branch pull`, { ...context, operation });
|
|
79
115
|
}
|
|
80
116
|
logger.debug(`Executing command: ${command}`, { ...context, operation });
|
|
81
117
|
const { stdout, stderr } = await execAsync(command);
|
|
82
|
-
logger.
|
|
118
|
+
logger.debug(`Git pull stdout: ${stdout}`, { ...context, operation });
|
|
83
119
|
if (stderr) {
|
|
84
|
-
|
|
85
|
-
logger.warning(`Git pull stderr: ${stderr}`, { ...context, operation });
|
|
120
|
+
logger.debug(`Git pull stderr: ${stderr}`, { ...context, operation });
|
|
86
121
|
}
|
|
87
122
|
// Analyze stdout/stderr to determine the outcome
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (summaryMatch)
|
|
100
|
-
summary = summaryMatch[1];
|
|
101
|
-
}
|
|
102
|
-
else if (message.includes('Merge made by the') || message.includes('merging')) { // Covers recursive and octopus
|
|
103
|
-
message = 'Pull successful (merge).';
|
|
104
|
-
const summaryMatch = stdout.match(/(\d+ files? changed.*)/);
|
|
105
|
-
if (summaryMatch)
|
|
106
|
-
summary = summaryMatch[1];
|
|
107
|
-
}
|
|
108
|
-
else if (message.includes('Automatic merge failed; fix conflicts and then commit the result.')) {
|
|
109
|
-
message = 'Pull resulted in merge conflicts.';
|
|
110
|
-
success = false; // Indicate failure due to conflicts
|
|
111
|
-
conflict = true;
|
|
112
|
-
}
|
|
113
|
-
else if (message.includes('fatal:')) {
|
|
114
|
-
// If a fatal error wasn't caught by the execAsync catch block but is in stdout/stderr
|
|
115
|
-
success = false;
|
|
116
|
-
message = `Pull failed: ${message}`;
|
|
117
|
-
logger.error(`Git pull command indicated failure: ${message}`, { ...context, operation, stdout, stderr });
|
|
118
|
-
}
|
|
119
|
-
// Add more specific checks based on git pull output variations if needed
|
|
120
|
-
logger.info(`${operation} completed`, { ...context, operation, path: targetPath, success, message, conflict });
|
|
121
|
-
return { success, message, summary, conflict };
|
|
123
|
+
const message = stdout.trim() || stderr.trim() || "Pull command executed.";
|
|
124
|
+
const summary = message;
|
|
125
|
+
const conflict = message.includes("CONFLICT");
|
|
126
|
+
logger.info("git pull executed successfully", {
|
|
127
|
+
...context,
|
|
128
|
+
operation,
|
|
129
|
+
path: targetPath,
|
|
130
|
+
summary,
|
|
131
|
+
conflict,
|
|
132
|
+
});
|
|
133
|
+
return { success: true, message, summary, conflict };
|
|
122
134
|
}
|
|
123
135
|
catch (error) {
|
|
124
|
-
logger.error(`Failed to execute git pull command`, {
|
|
125
|
-
|
|
136
|
+
logger.error(`Failed to execute git pull command`, {
|
|
137
|
+
...context,
|
|
138
|
+
operation,
|
|
139
|
+
path: targetPath,
|
|
140
|
+
error: error.message,
|
|
141
|
+
stderr: error.stderr,
|
|
142
|
+
stdout: error.stdout,
|
|
143
|
+
});
|
|
144
|
+
const errorMessage = error.stderr || error.stdout || error.message || ""; // Check stdout too for errors
|
|
126
145
|
// Handle specific error cases
|
|
127
|
-
if (errorMessage.toLowerCase().includes(
|
|
146
|
+
if (errorMessage.toLowerCase().includes("not a git repository")) {
|
|
128
147
|
throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`, { context, operation, originalError: error });
|
|
129
148
|
}
|
|
130
|
-
if (errorMessage.includes(
|
|
149
|
+
if (errorMessage.includes("resolve host") ||
|
|
150
|
+
errorMessage.includes("Could not read from remote repository")) {
|
|
131
151
|
throw new McpError(BaseErrorCode.SERVICE_UNAVAILABLE, `Failed to connect to remote repository. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
132
152
|
}
|
|
133
|
-
if (errorMessage.includes(
|
|
153
|
+
if (errorMessage.includes("merge conflict") ||
|
|
154
|
+
errorMessage.includes("fix conflicts")) {
|
|
134
155
|
// This might be caught here if execAsync throws due to non-zero exit code during conflict
|
|
135
|
-
|
|
136
|
-
return { success: false, message: 'Pull resulted in merge conflicts.', conflict: true };
|
|
156
|
+
throw new McpError(BaseErrorCode.CONFLICT, `Pull resulted in merge conflicts. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
137
157
|
}
|
|
138
|
-
if (errorMessage.includes(
|
|
158
|
+
if (errorMessage.includes("You have unstaged changes") ||
|
|
159
|
+
errorMessage.includes("Your local changes to the following files would be overwritten by merge")) {
|
|
139
160
|
throw new McpError(BaseErrorCode.CONFLICT, `Pull failed due to uncommitted local changes. Please commit or stash them first. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
140
161
|
}
|
|
141
|
-
if (errorMessage.includes(
|
|
162
|
+
if (errorMessage.includes("refusing to merge unrelated histories")) {
|
|
142
163
|
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Pull failed: Refusing to merge unrelated histories. Use '--allow-unrelated-histories' if intended.`, { context, operation, originalError: error });
|
|
143
164
|
}
|
|
144
165
|
// Generic internal error for other failures
|