@cyanheads/git-mcp-server 2.1.8 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/mcp-server/server.js +69 -228
- package/dist/mcp-server/tools/gitAdd/index.js +2 -4
- package/dist/mcp-server/tools/gitAdd/logic.js +40 -116
- package/dist/mcp-server/tools/gitAdd/registration.js +39 -59
- package/dist/mcp-server/tools/gitBranch/index.js +3 -5
- package/dist/mcp-server/tools/gitBranch/logic.js +109 -304
- package/dist/mcp-server/tools/gitBranch/registration.js +52 -66
- package/dist/mcp-server/tools/gitCheckout/index.js +2 -3
- package/dist/mcp-server/tools/gitCheckout/logic.js +47 -144
- package/dist/mcp-server/tools/gitCheckout/registration.js +53 -72
- package/dist/mcp-server/tools/gitCherryPick/index.js +3 -5
- package/dist/mcp-server/tools/gitCherryPick/logic.js +47 -173
- package/dist/mcp-server/tools/gitCherryPick/registration.js +52 -67
- package/dist/mcp-server/tools/gitClean/index.js +3 -5
- package/dist/mcp-server/tools/gitClean/logic.js +45 -154
- package/dist/mcp-server/tools/gitClean/registration.js +52 -92
- package/dist/mcp-server/tools/gitClearWorkingDir/index.js +3 -5
- package/dist/mcp-server/tools/gitClearWorkingDir/logic.js +18 -32
- package/dist/mcp-server/tools/gitClearWorkingDir/registration.js +55 -73
- package/dist/mcp-server/tools/gitClone/index.js +2 -4
- package/dist/mcp-server/tools/gitClone/logic.js +47 -187
- package/dist/mcp-server/tools/gitClone/registration.js +51 -42
- package/dist/mcp-server/tools/gitCommit/index.js +2 -4
- package/dist/mcp-server/tools/gitCommit/logic.js +75 -310
- package/dist/mcp-server/tools/gitCommit/registration.js +52 -73
- package/dist/mcp-server/tools/gitDiff/index.js +2 -3
- package/dist/mcp-server/tools/gitDiff/logic.js +72 -264
- package/dist/mcp-server/tools/gitDiff/registration.js +53 -68
- package/dist/mcp-server/tools/gitFetch/index.js +2 -3
- package/dist/mcp-server/tools/gitFetch/logic.js +38 -136
- package/dist/mcp-server/tools/gitFetch/registration.js +54 -66
- package/dist/mcp-server/tools/gitInit/index.js +3 -5
- package/dist/mcp-server/tools/gitInit/logic.js +40 -162
- package/dist/mcp-server/tools/gitInit/registration.js +52 -104
- package/dist/mcp-server/tools/gitLog/index.js +2 -3
- package/dist/mcp-server/tools/gitLog/logic.js +71 -266
- package/dist/mcp-server/tools/gitLog/registration.js +54 -66
- package/dist/mcp-server/tools/gitMerge/index.js +3 -5
- package/dist/mcp-server/tools/gitMerge/logic.js +45 -191
- package/dist/mcp-server/tools/gitMerge/registration.js +52 -71
- package/dist/mcp-server/tools/gitPull/index.js +2 -3
- package/dist/mcp-server/tools/gitPull/logic.js +39 -156
- package/dist/mcp-server/tools/gitPull/registration.js +53 -75
- package/dist/mcp-server/tools/gitPush/index.js +2 -3
- package/dist/mcp-server/tools/gitPush/logic.js +65 -192
- package/dist/mcp-server/tools/gitPush/registration.js +53 -75
- package/dist/mcp-server/tools/gitRebase/index.js +3 -5
- package/dist/mcp-server/tools/gitRebase/logic.js +59 -207
- package/dist/mcp-server/tools/gitRebase/registration.js +52 -70
- package/dist/mcp-server/tools/gitRemote/index.js +3 -5
- package/dist/mcp-server/tools/gitRemote/logic.js +76 -200
- package/dist/mcp-server/tools/gitRemote/registration.js +52 -65
- package/dist/mcp-server/tools/gitReset/index.js +2 -3
- package/dist/mcp-server/tools/gitReset/logic.js +33 -133
- package/dist/mcp-server/tools/gitReset/registration.js +53 -60
- package/dist/mcp-server/tools/gitSetWorkingDir/index.js +3 -5
- package/dist/mcp-server/tools/gitSetWorkingDir/logic.js +39 -144
- package/dist/mcp-server/tools/gitSetWorkingDir/registration.js +55 -85
- package/dist/mcp-server/tools/gitShow/index.js +3 -5
- package/dist/mcp-server/tools/gitShow/logic.js +28 -133
- package/dist/mcp-server/tools/gitShow/registration.js +52 -74
- package/dist/mcp-server/tools/gitStash/index.js +3 -5
- package/dist/mcp-server/tools/gitStash/logic.js +59 -219
- package/dist/mcp-server/tools/gitStash/registration.js +52 -77
- package/dist/mcp-server/tools/gitStatus/index.js +2 -4
- package/dist/mcp-server/tools/gitStatus/logic.js +79 -236
- package/dist/mcp-server/tools/gitStatus/registration.js +52 -66
- package/dist/mcp-server/tools/gitTag/index.js +3 -5
- package/dist/mcp-server/tools/gitTag/logic.js +57 -198
- package/dist/mcp-server/tools/gitTag/registration.js +54 -73
- package/dist/mcp-server/tools/gitWorktree/index.js +3 -5
- package/dist/mcp-server/tools/gitWorktree/logic.js +102 -328
- package/dist/mcp-server/tools/gitWorktree/registration.js +54 -58
- package/dist/mcp-server/tools/gitWrapupInstructions/index.js +5 -3
- package/dist/mcp-server/tools/gitWrapupInstructions/logic.js +25 -43
- package/dist/mcp-server/tools/gitWrapupInstructions/registration.js +54 -70
- package/dist/mcp-server/transports/httpTransport.js +2 -3
- package/package.json +8 -8
|
@@ -1,167 +1,50 @@
|
|
|
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 });
|
|
91
|
-
}
|
|
92
|
-
try {
|
|
93
|
-
// Construct the git pull command
|
|
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
|
-
});
|
|
116
|
-
const { stdout, stderr } = await execFileAsync("git", args);
|
|
117
|
-
logger.debug(`Git pull stdout: ${stdout}`, { ...context, operation });
|
|
118
|
-
if (stderr) {
|
|
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 };
|
|
133
|
-
}
|
|
134
|
-
catch (error) {
|
|
135
|
-
logger.error(`Failed to execute git pull command`, {
|
|
136
|
-
...context,
|
|
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
|
|
145
|
-
if (errorMessage.toLowerCase().includes("not a git repository")) {
|
|
146
|
-
throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`, { context, operation, originalError: error });
|
|
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 });
|
|
151
|
-
}
|
|
152
|
-
if (errorMessage.includes("merge conflict") ||
|
|
153
|
-
errorMessage.includes("fix conflicts")) {
|
|
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 });
|
|
156
|
-
}
|
|
157
|
-
if (errorMessage.includes("You have unstaged changes") ||
|
|
158
|
-
errorMessage.includes("Your local changes to the following files would be overwritten by merge")) {
|
|
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 });
|
|
160
|
-
}
|
|
161
|
-
if (errorMessage.includes("refusing to merge unrelated histories")) {
|
|
162
|
-
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Pull failed: Refusing to merge unrelated histories. Use '--allow-unrelated-histories' if intended.`, { context, operation, originalError: error });
|
|
163
|
-
}
|
|
164
|
-
// Generic internal error for other failures
|
|
165
|
-
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to pull changes for path: ${targetPath}. Error: ${errorMessage}`, { 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.");
|
|
166
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);
|
|
46
|
+
logger.debug(`Executing command: git ${args.join(" ")}`, { ...context, operation });
|
|
47
|
+
const { stdout, stderr } = await execFileAsync("git", args);
|
|
48
|
+
const message = stdout.trim() || stderr.trim() || "Pull command executed successfully.";
|
|
49
|
+
return { success: true, message, conflict: message.includes("CONFLICT") };
|
|
167
50
|
}
|
|
@@ -1,86 +1,64 @@
|
|
|
1
|
-
// Import utils from barrel (ErrorHandler from ../utils/internal/errorHandler.js)
|
|
2
|
-
import { ErrorHandler } from "../../../utils/index.js";
|
|
3
|
-
// Import utils from barrel (logger from ../utils/internal/logger.js)
|
|
4
|
-
import { logger } from "../../../utils/index.js";
|
|
5
|
-
// Import utils from barrel (requestContextService, RequestContext from ../utils/internal/requestContext.js)
|
|
6
|
-
import { BaseErrorCode } from "../../../types-global/errors.js"; // Keep direct import for types-global
|
|
7
|
-
import { requestContextService } from "../../../utils/index.js";
|
|
8
|
-
import { GitPullInputSchema, pullGitChanges, } from "./logic.js";
|
|
9
|
-
let _getWorkingDirectory;
|
|
10
|
-
let _getSessionId;
|
|
11
1
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* @param getWdFn - Function to get the working directory for a session.
|
|
15
|
-
* @param getSidFn - Function to get the session ID from context.
|
|
2
|
+
* @fileoverview Handles registration and error handling for the git_pull tool.
|
|
3
|
+
* @module src/mcp-server/tools/gitPull/registration
|
|
16
4
|
*/
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
_getSessionId = getSidFn;
|
|
20
|
-
logger.info("State accessors initialized for git_pull tool registration.");
|
|
21
|
-
}
|
|
5
|
+
import { ErrorHandler, logger, requestContextService } from "../../../utils/index.js";
|
|
6
|
+
import { pullGitChanges, GitPullInputSchema, GitPullOutputSchema, } from "./logic.js";
|
|
22
7
|
const TOOL_NAME = "git_pull";
|
|
23
8
|
const TOOL_DESCRIPTION = "Fetches from and integrates with another repository or a local branch (e.g., 'git pull origin main'). Supports rebase and fast-forward only options. Returns the pull result as a JSON object.";
|
|
24
9
|
/**
|
|
25
|
-
* Registers the git_pull tool with the MCP server.
|
|
26
|
-
*
|
|
27
|
-
* @param
|
|
28
|
-
* @
|
|
10
|
+
* Registers the git_pull tool with the MCP server instance.
|
|
11
|
+
* @param server The MCP server instance.
|
|
12
|
+
* @param getWorkingDirectory Function to get the session's working directory.
|
|
13
|
+
* @param getSessionId Function to get the session ID from context.
|
|
29
14
|
*/
|
|
30
|
-
export async
|
|
31
|
-
if (!_getWorkingDirectory || !_getSessionId) {
|
|
32
|
-
throw new Error("State accessors for git_pull must be initialized before registration.");
|
|
33
|
-
}
|
|
15
|
+
export const registerGitPullTool = async (server, getWorkingDirectory, getSessionId) => {
|
|
34
16
|
const operation = "registerGitPullTool";
|
|
35
17
|
const context = requestContextService.createRequestContext({ operation });
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
18
|
+
server.registerTool(TOOL_NAME, {
|
|
19
|
+
title: "Git Pull",
|
|
20
|
+
description: TOOL_DESCRIPTION,
|
|
21
|
+
inputSchema: GitPullInputSchema.shape,
|
|
22
|
+
outputSchema: GitPullOutputSchema.shape,
|
|
23
|
+
annotations: {
|
|
24
|
+
readOnlyHint: false,
|
|
25
|
+
destructiveHint: true, // Can change local files and history
|
|
26
|
+
idempotentHint: false,
|
|
27
|
+
openWorldHint: true, // Interacts with remote repositories
|
|
28
|
+
},
|
|
29
|
+
}, async (params, callContext) => {
|
|
30
|
+
const handlerContext = requestContextService.createRequestContext({
|
|
31
|
+
toolName: TOOL_NAME,
|
|
32
|
+
parentContext: callContext,
|
|
33
|
+
});
|
|
34
|
+
try {
|
|
35
|
+
const sessionId = getSessionId(handlerContext);
|
|
36
|
+
const result = await pullGitChanges(params, {
|
|
37
|
+
...handlerContext,
|
|
38
|
+
getWorkingDirectory: () => getWorkingDirectory(sessionId),
|
|
43
39
|
});
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
};
|
|
48
|
-
const logicContext = {
|
|
49
|
-
...requestContext,
|
|
50
|
-
sessionId: sessionId,
|
|
51
|
-
getWorkingDirectory: getWorkingDirectoryForSession,
|
|
40
|
+
return {
|
|
41
|
+
structuredContent: result,
|
|
42
|
+
content: [{ type: "text", text: `Success: ${JSON.stringify(result, null, 2)}` }],
|
|
52
43
|
};
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
text: JSON.stringify(pullResult, null, 2), // Pretty-print JSON
|
|
61
|
-
contentType: "application/json",
|
|
62
|
-
};
|
|
63
|
-
// Log based on the success flag in the result
|
|
64
|
-
if (pullResult.success) {
|
|
65
|
-
logger.info(`Tool ${TOOL_NAME} executed successfully, returning JSON`, logicContext);
|
|
66
|
-
}
|
|
67
|
-
else {
|
|
68
|
-
// Log non-fatal conditions like conflicts or already up-to-date differently
|
|
69
|
-
const logMessage = pullResult.conflict
|
|
70
|
-
? `Tool ${TOOL_NAME} completed with merge conflicts, returning JSON`
|
|
71
|
-
: `Tool ${TOOL_NAME} completed with status: ${pullResult.message}, returning JSON`;
|
|
72
|
-
logger.info(logMessage, logicContext);
|
|
73
|
-
}
|
|
74
|
-
// Even if success is false (e.g., conflicts), it's not necessarily a tool execution *error*
|
|
75
|
-
// unless the logic threw an McpError. The success flag in the JSON indicates the Git outcome.
|
|
76
|
-
return { content: [resultContent] };
|
|
77
|
-
}, {
|
|
78
|
-
operation: toolOperation,
|
|
79
|
-
context: logicContext,
|
|
80
|
-
input: validatedArgs,
|
|
81
|
-
errorCode: BaseErrorCode.INTERNAL_ERROR, // Default if unexpected error in logic
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
logger.error(`Error in ${TOOL_NAME} handler`, { error, ...handlerContext });
|
|
47
|
+
const mcpError = ErrorHandler.handleError(error, {
|
|
48
|
+
operation: `tool:${TOOL_NAME}`,
|
|
49
|
+
context: handlerContext,
|
|
50
|
+
input: params,
|
|
82
51
|
});
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
52
|
+
return {
|
|
53
|
+
isError: true,
|
|
54
|
+
content: [{ type: "text", text: `Error: ${mcpError.message}` }],
|
|
55
|
+
structuredContent: {
|
|
56
|
+
code: mcpError.code,
|
|
57
|
+
message: mcpError.message,
|
|
58
|
+
details: mcpError.details,
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
logger.info(`Tool '${TOOL_NAME}' registered successfully.`, context);
|
|
64
|
+
};
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Barrel file for the gitPush tool.
|
|
3
|
+
* @module src/mcp-server/tools/gitPush/index
|
|
3
4
|
*/
|
|
4
|
-
export { registerGitPushTool
|
|
5
|
-
// Export types if needed elsewhere, e.g.:
|
|
6
|
-
// export type { GitPushInput, GitPushResult } from './logic.js';
|
|
5
|
+
export { registerGitPushTool } from "./registration.js";
|
|
@@ -1,205 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Defines the core logic, schemas, and types for the git_push tool.
|
|
3
|
+
* @module src/mcp-server/tools/gitPush/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
|
-
export const
|
|
13
|
-
path: z
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
forceWithLease: z
|
|
37
|
-
.boolean()
|
|
38
|
-
.optional()
|
|
39
|
-
.default(false)
|
|
40
|
-
.describe("Force the push only if the remote ref is the expected value (`--force-with-lease`). Safer than --force."),
|
|
41
|
-
setUpstream: z
|
|
42
|
-
.boolean()
|
|
43
|
-
.optional()
|
|
44
|
-
.default(false)
|
|
45
|
-
.describe("Set the upstream tracking configuration (`-u` or `--set-upstream`)."),
|
|
46
|
-
tags: z
|
|
47
|
-
.boolean()
|
|
48
|
-
.optional()
|
|
49
|
-
.default(false)
|
|
50
|
-
.describe("Push all tags (`--tags`)."),
|
|
51
|
-
delete: z
|
|
52
|
-
.boolean()
|
|
53
|
-
.optional()
|
|
54
|
-
.default(false)
|
|
55
|
-
.describe("Delete the remote branch (`--delete`). Requires `branch` to be specified. Use with caution, as deleting remote branches can affect collaborators."),
|
|
56
|
-
// Add other relevant git push options as needed (e.g., --prune, --all)
|
|
11
|
+
// 1. DEFINE the Zod input schema.
|
|
12
|
+
export const GitPushBaseSchema = z.object({
|
|
13
|
+
path: z.string().default(".").describe("Path to the Git repository."),
|
|
14
|
+
remote: z.string().optional().describe("The remote repository to push to (e.g., 'origin')."),
|
|
15
|
+
branch: z.string().optional().describe("The local branch to push."),
|
|
16
|
+
remoteBranch: z.string().optional().describe("The remote branch to push to."),
|
|
17
|
+
force: z.boolean().default(false).describe("Force the push (use with caution)."),
|
|
18
|
+
forceWithLease: z.boolean().default(false).describe("Force the push only if the remote ref is as expected."),
|
|
19
|
+
setUpstream: z.boolean().default(false).describe("Set the upstream tracking configuration."),
|
|
20
|
+
tags: z.boolean().default(false).describe("Push all tags."),
|
|
21
|
+
delete: z.boolean().default(false).describe("Delete the remote branch."),
|
|
22
|
+
});
|
|
23
|
+
export const GitPushInputSchema = GitPushBaseSchema.refine(data => !(data.delete && !data.branch), {
|
|
24
|
+
message: "Cannot use --delete without specifying a branch to delete.",
|
|
25
|
+
path: ["delete", "branch"],
|
|
26
|
+
}).refine(data => !(data.force && data.forceWithLease), {
|
|
27
|
+
message: "Cannot use --force and --force-with-lease together.",
|
|
28
|
+
path: ["force", "forceWithLease"],
|
|
29
|
+
});
|
|
30
|
+
// 2. DEFINE the Zod response schema.
|
|
31
|
+
export const GitPushOutputSchema = z.object({
|
|
32
|
+
success: z.boolean().describe("Indicates if the command was successful."),
|
|
33
|
+
message: z.string().describe("A summary message of the result."),
|
|
34
|
+
rejected: z.boolean().optional().describe("True if the push was rejected."),
|
|
35
|
+
deleted: z.boolean().optional().describe("True if a remote branch was deleted."),
|
|
57
36
|
});
|
|
58
37
|
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* @param {GitPushInput} input - The validated input object.
|
|
62
|
-
* @param {RequestContext} context - The request context for logging and error handling.
|
|
63
|
-
* @returns {Promise<GitPushResult>} A promise that resolves with the structured push result.
|
|
64
|
-
* @throws {McpError} Throws an McpError if path resolution, validation, or the git command fails unexpectedly.
|
|
38
|
+
* 4. IMPLEMENT the core logic function.
|
|
39
|
+
* @throws {McpError} If the logic encounters an unrecoverable issue.
|
|
65
40
|
*/
|
|
66
|
-
export async function pushGitChanges(
|
|
41
|
+
export async function pushGitChanges(params, context) {
|
|
67
42
|
const operation = "pushGitChanges";
|
|
68
|
-
logger.debug(`Executing ${operation}`, { ...context,
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (input.path && input.path !== ".") {
|
|
73
|
-
targetPath = input.path;
|
|
74
|
-
}
|
|
75
|
-
else {
|
|
76
|
-
const workingDir = context.getWorkingDirectory();
|
|
77
|
-
if (!workingDir) {
|
|
78
|
-
throw new McpError(BaseErrorCode.VALIDATION_ERROR, "No path provided and no working directory set for the session.", { context, operation });
|
|
79
|
-
}
|
|
80
|
-
targetPath = workingDir;
|
|
81
|
-
}
|
|
82
|
-
targetPath = sanitization.sanitizePath(targetPath, {
|
|
83
|
-
allowAbsolute: true,
|
|
84
|
-
}).sanitizedPath;
|
|
85
|
-
logger.debug("Sanitized path", {
|
|
86
|
-
...context,
|
|
87
|
-
operation,
|
|
88
|
-
sanitizedPath: targetPath,
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
catch (error) {
|
|
92
|
-
logger.error("Path resolution or sanitization failed", {
|
|
93
|
-
...context,
|
|
94
|
-
operation,
|
|
95
|
-
error,
|
|
96
|
-
});
|
|
97
|
-
if (error instanceof McpError)
|
|
98
|
-
throw error;
|
|
99
|
-
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
|
|
100
|
-
}
|
|
101
|
-
// Validate specific input combinations
|
|
102
|
-
if (input.delete && !input.branch) {
|
|
103
|
-
throw new McpError(BaseErrorCode.VALIDATION_ERROR, "Cannot use --delete without specifying a branch to delete.", { context, operation });
|
|
104
|
-
}
|
|
105
|
-
if (input.force && input.forceWithLease) {
|
|
106
|
-
throw new McpError(BaseErrorCode.VALIDATION_ERROR, "Cannot use --force and --force-with-lease together.", { context, operation });
|
|
107
|
-
}
|
|
108
|
-
if (input.delete &&
|
|
109
|
-
(input.force || input.forceWithLease || input.setUpstream || input.tags)) {
|
|
110
|
-
throw new McpError(BaseErrorCode.VALIDATION_ERROR, "Cannot combine --delete with --force, --force-with-lease, --set-upstream, or --tags.", { context, operation });
|
|
111
|
-
}
|
|
112
|
-
try {
|
|
113
|
-
// Construct the git push command
|
|
114
|
-
const args = ["-C", targetPath, "push"];
|
|
115
|
-
if (input.force) {
|
|
116
|
-
args.push("--force");
|
|
117
|
-
}
|
|
118
|
-
else if (input.forceWithLease) {
|
|
119
|
-
args.push("--force-with-lease");
|
|
120
|
-
}
|
|
121
|
-
if (input.setUpstream) {
|
|
122
|
-
args.push("--set-upstream");
|
|
123
|
-
}
|
|
124
|
-
if (input.tags) {
|
|
125
|
-
args.push("--tags");
|
|
126
|
-
}
|
|
127
|
-
if (input.delete) {
|
|
128
|
-
args.push("--delete");
|
|
129
|
-
}
|
|
130
|
-
// Add remote and branch specification
|
|
131
|
-
const remote = input.remote || "origin"; // Default to origin
|
|
132
|
-
args.push(remote);
|
|
133
|
-
if (input.branch) {
|
|
134
|
-
if (input.remoteBranch && !input.delete) {
|
|
135
|
-
args.push(`${input.branch}:${input.remoteBranch}`);
|
|
136
|
-
}
|
|
137
|
-
else {
|
|
138
|
-
args.push(input.branch);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
else if (!input.tags && !input.delete) {
|
|
142
|
-
// If no branch, tags, or delete specified, push the current branch by default
|
|
143
|
-
logger.debug("No specific branch, tags, or delete specified. Relying on default git push behavior for current branch.", { ...context, operation });
|
|
144
|
-
}
|
|
145
|
-
logger.debug(`Executing command: git ${args.join(" ")}`, {
|
|
146
|
-
...context,
|
|
147
|
-
operation,
|
|
148
|
-
});
|
|
149
|
-
// Execute command. Note: Git push often uses stderr for progress and success messages.
|
|
150
|
-
const { stdout, stderr } = await execFileAsync("git", args);
|
|
151
|
-
logger.debug(`Git push stdout: ${stdout}`, { ...context, operation });
|
|
152
|
-
if (stderr) {
|
|
153
|
-
logger.debug(`Git push stderr: ${stderr}`, { ...context, operation });
|
|
154
|
-
}
|
|
155
|
-
// Analyze stderr primarily, fallback to stdout
|
|
156
|
-
const message = stderr.trim() || stdout.trim() || "Push command executed.";
|
|
157
|
-
const summary = message;
|
|
158
|
-
const rejected = message.includes("[rejected]");
|
|
159
|
-
const deleted = message.includes("[deleted]");
|
|
160
|
-
logger.info("git push executed successfully", {
|
|
161
|
-
...context,
|
|
162
|
-
operation,
|
|
163
|
-
path: targetPath,
|
|
164
|
-
summary,
|
|
165
|
-
rejected,
|
|
166
|
-
deleted,
|
|
167
|
-
});
|
|
168
|
-
return { success: true, message, summary, rejected, deleted };
|
|
43
|
+
logger.debug(`Executing ${operation}`, { ...context, params });
|
|
44
|
+
const workingDir = context.getWorkingDirectory();
|
|
45
|
+
if (params.path === "." && !workingDir) {
|
|
46
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, "No session working directory set. Please specify a 'path' or use 'git_set_working_dir' first.");
|
|
169
47
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
if (
|
|
185
|
-
|
|
186
|
-
errorMessage.includes("Connection timed out")) {
|
|
187
|
-
throw new McpError(BaseErrorCode.SERVICE_UNAVAILABLE, `Failed to connect to remote repository. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
48
|
+
const targetPath = sanitization.sanitizePath(params.path === "." ? workingDir : params.path, { allowAbsolute: true }).sanitizedPath;
|
|
49
|
+
const args = ["-C", targetPath, "push"];
|
|
50
|
+
if (params.force)
|
|
51
|
+
args.push("--force");
|
|
52
|
+
else if (params.forceWithLease)
|
|
53
|
+
args.push("--force-with-lease");
|
|
54
|
+
if (params.setUpstream)
|
|
55
|
+
args.push("--set-upstream");
|
|
56
|
+
if (params.tags)
|
|
57
|
+
args.push("--tags");
|
|
58
|
+
if (params.delete)
|
|
59
|
+
args.push("--delete");
|
|
60
|
+
args.push(params.remote || "origin");
|
|
61
|
+
if (params.branch) {
|
|
62
|
+
if (params.remoteBranch && !params.delete) {
|
|
63
|
+
args.push(`${params.branch}:${params.remoteBranch}`);
|
|
188
64
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
// This might be caught here if execAsync throws due to non-zero exit code on rejection
|
|
192
|
-
throw new McpError(BaseErrorCode.CONFLICT, `Push rejected: ${errorMessage}`, { context, operation, originalError: error });
|
|
193
|
-
}
|
|
194
|
-
if (errorMessage.includes("Authentication failed") ||
|
|
195
|
-
errorMessage.includes("Permission denied")) {
|
|
196
|
-
throw new McpError(BaseErrorCode.UNAUTHORIZED, `Authentication failed for remote repository. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
197
|
-
}
|
|
198
|
-
if (errorMessage.includes("src refspec") &&
|
|
199
|
-
errorMessage.includes("does not match any")) {
|
|
200
|
-
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Push failed: Source branch/refspec does not exist locally. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
65
|
+
else {
|
|
66
|
+
args.push(params.branch);
|
|
201
67
|
}
|
|
202
|
-
// Generic internal error for other failures
|
|
203
|
-
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to push changes for path: ${targetPath}. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
204
68
|
}
|
|
69
|
+
logger.debug(`Executing command: git ${args.join(" ")}`, { ...context, operation });
|
|
70
|
+
const { stdout, stderr } = await execFileAsync("git", args);
|
|
71
|
+
const message = stderr.trim() || stdout.trim() || "Push command executed successfully.";
|
|
72
|
+
return {
|
|
73
|
+
success: true,
|
|
74
|
+
message,
|
|
75
|
+
rejected: message.includes("[rejected]"),
|
|
76
|
+
deleted: message.includes("[deleted]"),
|
|
77
|
+
};
|
|
205
78
|
}
|