@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,213 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Defines the core logic, schemas, and types for the git_remote tool.
|
|
3
|
+
* @module src/mcp-server/tools/gitRemote/logic
|
|
4
|
+
*/
|
|
1
5
|
import { execFile } from "child_process";
|
|
2
6
|
import { promisify } from "util";
|
|
3
7
|
import { z } from "zod";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
8
|
+
import { logger, sanitization } from "../../../utils/index.js";
|
|
9
|
+
import { McpError, BaseErrorCode } from "../../../types-global/errors.js";
|
|
6
10
|
const execFileAsync = promisify(execFile);
|
|
7
|
-
//
|
|
8
|
-
export const
|
|
9
|
-
path: z
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
11
|
+
// 1. DEFINE the Zod input schema.
|
|
12
|
+
export const GitRemoteBaseSchema = z.object({
|
|
13
|
+
path: z.string().default(".").describe("Path to the Git repository."),
|
|
14
|
+
mode: z.enum(["list", "add", "remove", "show"]).describe("Operation mode."),
|
|
15
|
+
name: z.string().optional().describe("Remote name (required for 'add', 'remove', 'show')."),
|
|
16
|
+
url: z.string().optional().describe("Remote URL (required for 'add')."),
|
|
17
|
+
});
|
|
18
|
+
export const GitRemoteInputSchema = GitRemoteBaseSchema.refine(data => !(data.mode === 'add' && (!data.name || !data.url)), {
|
|
19
|
+
message: "Remote 'name' and 'url' are required for 'add' mode.",
|
|
20
|
+
path: ["name", "url"],
|
|
21
|
+
}).refine(data => !((data.mode === 'remove' || data.mode === 'show') && !data.name), {
|
|
22
|
+
message: "Remote 'name' is required for 'remove' or 'show' mode.",
|
|
23
|
+
path: ["name"],
|
|
24
|
+
});
|
|
25
|
+
// 2. DEFINE the Zod response schema.
|
|
26
|
+
const RemoteInfoSchema = z.object({
|
|
27
|
+
name: z.string(),
|
|
28
|
+
fetchUrl: z.string(),
|
|
29
|
+
pushUrl: z.string(),
|
|
30
|
+
});
|
|
31
|
+
export const GitRemoteOutputSchema = z.object({
|
|
32
|
+
success: z.boolean().describe("Indicates if the command was successful."),
|
|
33
|
+
mode: z.string().describe("The mode of operation that was performed."),
|
|
34
|
+
message: z.string().optional().describe("A summary message of the result."),
|
|
35
|
+
remotes: z.array(RemoteInfoSchema).optional().describe("A list of remotes for the 'list' mode."),
|
|
36
|
+
details: z.string().optional().describe("Details for the 'show' mode."),
|
|
24
37
|
});
|
|
25
38
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* @param {GitRemoteInput} input - The validated input object.
|
|
29
|
-
* @param {RequestContext} context - The request context for logging and error handling.
|
|
30
|
-
* @returns {Promise<GitRemoteResult>} A promise that resolves with the structured result.
|
|
31
|
-
* @throws {McpError} Throws an McpError for path resolution/validation failures or unexpected errors.
|
|
39
|
+
* 4. IMPLEMENT the core logic function.
|
|
40
|
+
* @throws {McpError} If the logic encounters an unrecoverable issue.
|
|
32
41
|
*/
|
|
33
|
-
export async function gitRemoteLogic(
|
|
34
|
-
const operation = `gitRemoteLogic:${
|
|
35
|
-
logger.debug(`Executing ${operation}`, { ...context,
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const workingDir = context.getWorkingDirectory();
|
|
40
|
-
targetPath =
|
|
41
|
-
input.path && input.path !== "." ? input.path : (workingDir ?? "."); // Default to '.' if no working dir set and no path provided
|
|
42
|
-
if (targetPath === "." && !workingDir) {
|
|
43
|
-
logger.warning("Executing git remote in server's CWD as no path provided and no session WD set.", { ...context, operation });
|
|
44
|
-
// Allow execution in CWD but log it clearly. Consider if an error is more appropriate.
|
|
45
|
-
// For now, let's proceed but be aware.
|
|
46
|
-
targetPath = process.cwd(); // Use actual CWD if '.' was the default
|
|
47
|
-
}
|
|
48
|
-
else if (targetPath === "." && workingDir) {
|
|
49
|
-
targetPath = workingDir;
|
|
50
|
-
logger.debug(`Using session working directory: ${targetPath}`, {
|
|
51
|
-
...context,
|
|
52
|
-
operation,
|
|
53
|
-
sessionId: context.sessionId,
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
else {
|
|
57
|
-
logger.debug(`Using provided path: ${targetPath}`, {
|
|
58
|
-
...context,
|
|
59
|
-
operation,
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
targetPath = sanitization.sanitizePath(targetPath, {
|
|
63
|
-
allowAbsolute: true,
|
|
64
|
-
}).sanitizedPath; // Sanitize the final resolved path
|
|
65
|
-
logger.debug("Sanitized path", {
|
|
66
|
-
...context,
|
|
67
|
-
operation,
|
|
68
|
-
sanitizedPath: targetPath,
|
|
69
|
-
});
|
|
42
|
+
export async function gitRemoteLogic(params, context) {
|
|
43
|
+
const operation = `gitRemoteLogic:${params.mode}`;
|
|
44
|
+
logger.debug(`Executing ${operation}`, { ...context, params });
|
|
45
|
+
const workingDir = context.getWorkingDirectory();
|
|
46
|
+
if (params.path === "." && !workingDir) {
|
|
47
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, "No session working directory set. Please specify a 'path' or use 'git_set_working_dir' first.");
|
|
70
48
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
49
|
+
const targetPath = sanitization.sanitizePath(params.path === "." ? workingDir : params.path, { allowAbsolute: true }).sanitizedPath;
|
|
50
|
+
const args = ["-C", targetPath, "remote"];
|
|
51
|
+
switch (params.mode) {
|
|
52
|
+
case "list":
|
|
53
|
+
args.push("-v");
|
|
54
|
+
break;
|
|
55
|
+
case "add":
|
|
56
|
+
args.push("add", params.name, params.url);
|
|
57
|
+
break;
|
|
58
|
+
case "remove":
|
|
59
|
+
args.push("remove", params.name);
|
|
60
|
+
break;
|
|
61
|
+
case "show":
|
|
62
|
+
args.push("show", params.name);
|
|
63
|
+
break;
|
|
80
64
|
}
|
|
81
65
|
try {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if (type === "fetch") {
|
|
105
|
-
remoteMap.get(name).fetchUrl = url;
|
|
106
|
-
}
|
|
107
|
-
else if (type === "push") {
|
|
108
|
-
remoteMap.get(name).pushUrl = url;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
});
|
|
112
|
-
remoteMap.forEach((urls, name) => {
|
|
113
|
-
// Ensure both URLs are present, defaulting to fetch URL if push is missing (common case)
|
|
114
|
-
remotes.push({
|
|
115
|
-
name,
|
|
116
|
-
fetchUrl: urls.fetchUrl || "N/A",
|
|
117
|
-
pushUrl: urls.pushUrl || urls.fetchUrl || "N/A",
|
|
118
|
-
});
|
|
119
|
-
});
|
|
120
|
-
result = { success: true, mode: "list", remotes };
|
|
121
|
-
break;
|
|
122
|
-
case "add":
|
|
123
|
-
if (!input.name || !input.url) {
|
|
124
|
-
throw new McpError(BaseErrorCode.VALIDATION_ERROR, "Remote 'name' and 'url' are required for 'add' mode.", { context, operation });
|
|
125
|
-
}
|
|
126
|
-
// Basic validation for remote name (avoiding shell injection characters)
|
|
127
|
-
if (!/^[a-zA-Z0-9_.-]+$/.test(input.name)) {
|
|
128
|
-
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid remote name: ${input.name}`, { context, operation });
|
|
129
|
-
}
|
|
130
|
-
args = ["-C", targetPath, "remote", "add", input.name, input.url];
|
|
131
|
-
logger.debug(`Executing command: git ${args.join(" ")}`, {
|
|
132
|
-
...context,
|
|
133
|
-
operation,
|
|
134
|
-
});
|
|
135
|
-
await execFileAsync("git", args);
|
|
136
|
-
result = {
|
|
137
|
-
success: true,
|
|
138
|
-
mode: "add",
|
|
139
|
-
message: `Remote '${input.name}' added successfully.`,
|
|
140
|
-
};
|
|
141
|
-
break;
|
|
142
|
-
case "remove":
|
|
143
|
-
if (!input.name) {
|
|
144
|
-
throw new McpError(BaseErrorCode.VALIDATION_ERROR, "Remote 'name' is required for 'remove' mode.", { context, operation });
|
|
145
|
-
}
|
|
146
|
-
if (!/^[a-zA-Z0-9_.-]+$/.test(input.name)) {
|
|
147
|
-
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid remote name: ${input.name}`, { context, operation });
|
|
148
|
-
}
|
|
149
|
-
args = ["-C", targetPath, "remote", "remove", input.name];
|
|
150
|
-
logger.debug(`Executing command: git ${args.join(" ")}`, {
|
|
151
|
-
...context,
|
|
152
|
-
operation,
|
|
153
|
-
});
|
|
154
|
-
await execFileAsync("git", args);
|
|
155
|
-
result = {
|
|
156
|
-
success: true,
|
|
157
|
-
mode: "remove",
|
|
158
|
-
message: `Remote '${input.name}' removed successfully.`,
|
|
159
|
-
};
|
|
160
|
-
break;
|
|
161
|
-
case "show":
|
|
162
|
-
if (!input.name) {
|
|
163
|
-
throw new McpError(BaseErrorCode.VALIDATION_ERROR, "Remote 'name' is required for 'show' mode.", { context, operation });
|
|
164
|
-
}
|
|
165
|
-
if (!/^[a-zA-Z0-9_.-]+$/.test(input.name)) {
|
|
166
|
-
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid remote name: ${input.name}`, { context, operation });
|
|
167
|
-
}
|
|
168
|
-
args = ["-C", targetPath, "remote", "show", input.name];
|
|
169
|
-
logger.debug(`Executing command: git ${args.join(" ")}`, {
|
|
170
|
-
...context,
|
|
171
|
-
operation,
|
|
172
|
-
});
|
|
173
|
-
const { stdout: showStdout } = await execFileAsync("git", args);
|
|
174
|
-
result = { success: true, mode: "show", details: showStdout.trim() };
|
|
175
|
-
break;
|
|
176
|
-
default:
|
|
177
|
-
// Should not happen due to Zod validation, but good practice
|
|
178
|
-
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid mode: ${input.mode}`, { context, operation });
|
|
66
|
+
logger.debug(`Executing command: git ${args.join(" ")}`, { ...context, operation });
|
|
67
|
+
const { stdout } = await execFileAsync("git", args);
|
|
68
|
+
if (params.mode === 'list') {
|
|
69
|
+
const remoteMap = new Map();
|
|
70
|
+
stdout.trim().split("\n").forEach(line => {
|
|
71
|
+
const parts = line.split(/\s+/);
|
|
72
|
+
if (parts.length < 3)
|
|
73
|
+
return;
|
|
74
|
+
const [name, url, type] = parts;
|
|
75
|
+
const cleanType = type.replace(/[()]/g, "");
|
|
76
|
+
if (!remoteMap.has(name))
|
|
77
|
+
remoteMap.set(name, {});
|
|
78
|
+
if (cleanType === 'fetch')
|
|
79
|
+
remoteMap.get(name).fetchUrl = url;
|
|
80
|
+
if (cleanType === 'push')
|
|
81
|
+
remoteMap.get(name).pushUrl = url;
|
|
82
|
+
});
|
|
83
|
+
const remotes = Array.from(remoteMap.entries()).map(([name, urls]) => ({ name, fetchUrl: urls.fetchUrl || 'N/A', pushUrl: urls.pushUrl || urls.fetchUrl || 'N/A' }));
|
|
84
|
+
return { success: true, mode: params.mode, remotes };
|
|
85
|
+
}
|
|
86
|
+
if (params.mode === 'show') {
|
|
87
|
+
return { success: true, mode: params.mode, details: stdout.trim() };
|
|
179
88
|
}
|
|
180
|
-
|
|
181
|
-
...context,
|
|
182
|
-
operation,
|
|
183
|
-
path: targetPath,
|
|
184
|
-
result,
|
|
185
|
-
});
|
|
186
|
-
return result;
|
|
89
|
+
return { success: true, mode: params.mode, message: `Remote '${params.name}' ${params.mode === 'add' ? 'added' : 'removed'} successfully.` };
|
|
187
90
|
}
|
|
188
91
|
catch (error) {
|
|
189
92
|
const errorMessage = error.stderr || error.message || "";
|
|
190
|
-
logger.error(`Failed to execute git remote command`, {
|
|
191
|
-
...context,
|
|
192
|
-
operation,
|
|
193
|
-
path: targetPath,
|
|
194
|
-
error: errorMessage,
|
|
195
|
-
stderr: error.stderr,
|
|
196
|
-
stdout: error.stdout,
|
|
197
|
-
});
|
|
198
|
-
// Specific error handling
|
|
93
|
+
logger.error(`Failed to execute git remote command`, { ...context, operation, errorMessage });
|
|
199
94
|
if (errorMessage.toLowerCase().includes("not a git repository")) {
|
|
200
|
-
throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}
|
|
95
|
+
throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`);
|
|
201
96
|
}
|
|
202
|
-
if (
|
|
203
|
-
|
|
204
|
-
throw new McpError(BaseErrorCode.CONFLICT, `Failed to add remote: Remote '${input.name}' already exists. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
97
|
+
if (params.mode === "add" && errorMessage.toLowerCase().includes("already exists")) {
|
|
98
|
+
throw new McpError(BaseErrorCode.CONFLICT, `Remote '${params.name}' already exists.`);
|
|
205
99
|
}
|
|
206
|
-
if ((
|
|
207
|
-
|
|
208
|
-
throw new McpError(BaseErrorCode.NOT_FOUND, `Failed to ${input.mode} remote: Remote '${input.name}' does not exist. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
100
|
+
if ((params.mode === "remove" || params.mode === "show") && errorMessage.toLowerCase().includes("no such remote")) {
|
|
101
|
+
throw new McpError(BaseErrorCode.NOT_FOUND, `Remote '${params.name}' does not exist.`);
|
|
209
102
|
}
|
|
210
|
-
|
|
211
|
-
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Git remote ${input.mode} failed for path: ${targetPath}. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
103
|
+
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Git remote ${params.mode} failed: ${errorMessage}`);
|
|
212
104
|
}
|
|
213
105
|
}
|
|
@@ -1,77 +1,64 @@
|
|
|
1
|
-
import { BaseErrorCode } from "../../../types-global/errors.js"; // Direct import for types-global
|
|
2
|
-
import { ErrorHandler, logger, requestContextService, } from "../../../utils/index.js"; // ErrorHandler (./utils/internal/errorHandler.js), logger (./utils/internal/logger.js), requestContextService (./utils/internal/requestContext.js)
|
|
3
|
-
import { GitRemoteInputSchema, gitRemoteLogic, } from "./logic.js";
|
|
4
|
-
let _getWorkingDirectory;
|
|
5
|
-
let _getSessionId;
|
|
6
1
|
/**
|
|
7
|
-
*
|
|
8
|
-
* @
|
|
9
|
-
* @param getSidFn - Function to get the session ID from context.
|
|
2
|
+
* @fileoverview Handles registration and error handling for the git_remote tool.
|
|
3
|
+
* @module src/mcp-server/tools/gitRemote/registration
|
|
10
4
|
*/
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
logger.info("State accessors initialized for git_remote tool registration.");
|
|
15
|
-
}
|
|
5
|
+
import { ErrorHandler, logger, requestContextService } from "../../../utils/index.js";
|
|
6
|
+
import { McpError, BaseErrorCode } from "../../../types-global/errors.js";
|
|
7
|
+
import { gitRemoteLogic, GitRemoteOutputSchema, GitRemoteBaseSchema, } from "./logic.js";
|
|
16
8
|
const TOOL_NAME = "git_remote";
|
|
17
9
|
const TOOL_DESCRIPTION = "Manages remote repositories (list, add, remove, show).";
|
|
18
10
|
/**
|
|
19
|
-
* Registers the git_remote tool with the MCP server.
|
|
20
|
-
*
|
|
21
|
-
* @param
|
|
22
|
-
* @
|
|
23
|
-
* @throws {Error} If registration fails or state accessors are not initialized.
|
|
11
|
+
* Registers the git_remote 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.
|
|
24
15
|
*/
|
|
25
|
-
export const registerGitRemoteTool = async (server) => {
|
|
26
|
-
if (!_getWorkingDirectory || !_getSessionId) {
|
|
27
|
-
throw new Error("State accessors for git_remote must be initialized before registration.");
|
|
28
|
-
}
|
|
16
|
+
export const registerGitRemoteTool = async (server, getWorkingDirectory, getSessionId) => {
|
|
29
17
|
const operation = "registerGitRemoteTool";
|
|
30
18
|
const context = requestContextService.createRequestContext({ operation });
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
19
|
+
server.registerTool(TOOL_NAME, {
|
|
20
|
+
title: "Git Remote",
|
|
21
|
+
description: TOOL_DESCRIPTION,
|
|
22
|
+
inputSchema: GitRemoteBaseSchema.shape,
|
|
23
|
+
outputSchema: GitRemoteOutputSchema.shape,
|
|
24
|
+
annotations: {
|
|
25
|
+
readOnlyHint: false, // Can add/remove remotes
|
|
26
|
+
destructiveHint: true,
|
|
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 gitRemoteLogic(params, {
|
|
38
|
+
...handlerContext,
|
|
39
|
+
getWorkingDirectory: () => getWorkingDirectory(sessionId),
|
|
38
40
|
});
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
};
|
|
43
|
-
const logicContext = {
|
|
44
|
-
...requestContext,
|
|
45
|
-
sessionId: sessionId,
|
|
46
|
-
getWorkingDirectory: getWorkingDirectoryForSession,
|
|
41
|
+
return {
|
|
42
|
+
structuredContent: result,
|
|
43
|
+
content: [{ type: "text", text: `Success: ${JSON.stringify(result, null, 2)}` }],
|
|
47
44
|
};
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
text: JSON.stringify(remoteResult, null, 2), // Pretty-print JSON
|
|
56
|
-
contentType: "application/json",
|
|
57
|
-
};
|
|
58
|
-
// Log based on the success flag in the result
|
|
59
|
-
if (remoteResult.success) {
|
|
60
|
-
logger.info(`Tool ${TOOL_NAME} (mode: ${validatedArgs.mode}) executed successfully, returning JSON`, logicContext);
|
|
61
|
-
}
|
|
62
|
-
else {
|
|
63
|
-
// Log specific failure message from the result
|
|
64
|
-
logger.warning(`Tool ${TOOL_NAME} (mode: ${validatedArgs.mode}) failed: ${remoteResult.message}`, { ...logicContext, errorDetails: remoteResult.error });
|
|
65
|
-
}
|
|
66
|
-
// Return the result, whether success or structured failure
|
|
67
|
-
return { content: [resultContent] };
|
|
68
|
-
}, {
|
|
69
|
-
operation: toolOperation,
|
|
70
|
-
context: logicContext,
|
|
71
|
-
input: validatedArgs,
|
|
72
|
-
errorCode: BaseErrorCode.INTERNAL_ERROR, // Default if unexpected error occurs in logic/wrapper
|
|
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,
|
|
73
52
|
});
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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);
|
|
77
64
|
};
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Barrel file for the gitReset tool.
|
|
3
|
+
* @module src/mcp-server/tools/gitReset/index
|
|
3
4
|
*/
|
|
4
|
-
export { registerGitResetTool
|
|
5
|
-
// Export types if needed elsewhere, e.g.:
|
|
6
|
-
// export type { GitResetInput, GitResetResult } from './logic.js';
|
|
5
|
+
export { registerGitResetTool } from "./registration.js";
|
|
@@ -1,144 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Defines the core logic, schemas, and types for the git_reset tool.
|
|
3
|
+
* @module src/mcp-server/tools/gitReset/logic
|
|
4
|
+
*/
|
|
1
5
|
import { execFile } from "child_process";
|
|
2
6
|
import { promisify } from "util";
|
|
3
7
|
import { z } from "zod";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
8
|
+
import { logger, sanitization } from "../../../utils/index.js";
|
|
9
|
+
import { McpError, BaseErrorCode } from "../../../types-global/errors.js";
|
|
6
10
|
const execFileAsync = promisify(execFile);
|
|
7
|
-
//
|
|
8
|
-
const ResetModeEnum = z.enum(["soft", "mixed", "hard", "merge", "keep"]);
|
|
9
|
-
// Define the input schema for the git_reset tool using Zod
|
|
11
|
+
// 1. DEFINE the Zod input schema.
|
|
10
12
|
export const GitResetInputSchema = z.object({
|
|
11
|
-
path: z
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
commit: z
|
|
21
|
-
.string()
|
|
22
|
-
.optional()
|
|
23
|
-
.describe("Commit, branch, or ref to reset to. Defaults to HEAD (useful for unstaging with 'mixed' mode)."),
|
|
24
|
-
// file: z.string().optional().describe("If specified, reset only this file in the index (unstaging). Mode must be 'mixed' or omitted."), // Git reset [<mode>] [<tree-ish>] [--] <paths>… is complex, handle separately if needed
|
|
13
|
+
path: z.string().default(".").describe("Path to the Git repository."),
|
|
14
|
+
mode: z.enum(["soft", "mixed", "hard", "merge", "keep"]).default("mixed").describe("Reset mode."),
|
|
15
|
+
commit: z.string().optional().describe("Commit, branch, or ref to reset to. Defaults to HEAD."),
|
|
16
|
+
});
|
|
17
|
+
// 2. DEFINE the Zod response schema.
|
|
18
|
+
export const GitResetOutputSchema = z.object({
|
|
19
|
+
success: z.boolean().describe("Indicates if the command was successful."),
|
|
20
|
+
message: z.string().describe("A summary message of the result."),
|
|
21
|
+
changesSummary: z.string().optional().describe("Summary of changes, if any."),
|
|
25
22
|
});
|
|
26
23
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* @param {GitResetInput} input - The validated input object.
|
|
30
|
-
* @param {RequestContext} context - The request context for logging and error handling.
|
|
31
|
-
* @returns {Promise<GitResetResult>} A promise that resolves with the structured reset result.
|
|
32
|
-
* @throws {McpError} Throws an McpError if path resolution, validation, or the git command fails unexpectedly.
|
|
24
|
+
* 4. IMPLEMENT the core logic function.
|
|
25
|
+
* @throws {McpError} If the logic encounters an unrecoverable issue.
|
|
33
26
|
*/
|
|
34
|
-
export async function resetGitState(
|
|
27
|
+
export async function resetGitState(params, context) {
|
|
35
28
|
const operation = "resetGitState";
|
|
36
|
-
logger.debug(`Executing ${operation}`, { ...context,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
// }
|
|
41
|
-
// if (input.file && input.commit) {
|
|
42
|
-
// throw new McpError(BaseErrorCode.VALIDATION_ERROR, "Cannot specify both a commit and file paths for reset.", { context, operation });
|
|
43
|
-
// }
|
|
44
|
-
let targetPath;
|
|
45
|
-
try {
|
|
46
|
-
// Resolve and sanitize the target path
|
|
47
|
-
if (input.path && input.path !== ".") {
|
|
48
|
-
targetPath = input.path;
|
|
49
|
-
}
|
|
50
|
-
else {
|
|
51
|
-
const workingDir = context.getWorkingDirectory();
|
|
52
|
-
if (!workingDir) {
|
|
53
|
-
throw new McpError(BaseErrorCode.VALIDATION_ERROR, "No path provided and no working directory set for the session.", { context, operation });
|
|
54
|
-
}
|
|
55
|
-
targetPath = workingDir;
|
|
56
|
-
}
|
|
57
|
-
targetPath = sanitization.sanitizePath(targetPath, {
|
|
58
|
-
allowAbsolute: true,
|
|
59
|
-
}).sanitizedPath;
|
|
60
|
-
logger.debug("Sanitized path", {
|
|
61
|
-
...context,
|
|
62
|
-
operation,
|
|
63
|
-
sanitizedPath: targetPath,
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
catch (error) {
|
|
67
|
-
logger.error("Path resolution or sanitization failed", {
|
|
68
|
-
...context,
|
|
69
|
-
operation,
|
|
70
|
-
error,
|
|
71
|
-
});
|
|
72
|
-
if (error instanceof McpError)
|
|
73
|
-
throw error;
|
|
74
|
-
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
|
|
29
|
+
logger.debug(`Executing ${operation}`, { ...context, params });
|
|
30
|
+
const workingDir = context.getWorkingDirectory();
|
|
31
|
+
if (params.path === "." && !workingDir) {
|
|
32
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, "No session working directory set. Please specify a 'path' or use 'git_set_working_dir' first.");
|
|
75
33
|
}
|
|
34
|
+
const targetPath = sanitization.sanitizePath(params.path === "." ? workingDir : params.path, { allowAbsolute: true }).sanitizedPath;
|
|
35
|
+
const args = ["-C", targetPath, "reset"];
|
|
36
|
+
args.push(`--${params.mode}`);
|
|
37
|
+
if (params.commit)
|
|
38
|
+
args.push(params.commit);
|
|
76
39
|
try {
|
|
77
|
-
|
|
78
|
-
const args = ["-C", targetPath, "reset"];
|
|
79
|
-
if (input.mode) {
|
|
80
|
-
args.push(`--${input.mode}`);
|
|
81
|
-
}
|
|
82
|
-
if (input.commit) {
|
|
83
|
-
args.push(input.commit);
|
|
84
|
-
}
|
|
85
|
-
// Handling file paths requires careful command construction, often without a commit ref.
|
|
86
|
-
// Example: `git reset HEAD -- path/to/file` or `git reset -- path/to/file` (unstages)
|
|
87
|
-
// For simplicity, this initial version focuses on resetting the whole HEAD/index/tree.
|
|
88
|
-
// Add file path logic here if needed, adjusting command structure.
|
|
89
|
-
logger.debug(`Executing command: git ${args.join(" ")}`, {
|
|
90
|
-
...context,
|
|
91
|
-
operation,
|
|
92
|
-
});
|
|
93
|
-
// Execute command. Reset output is often minimal on success, but stderr might indicate issues.
|
|
40
|
+
logger.debug(`Executing command: git ${args.join(" ")}`, { ...context, operation });
|
|
94
41
|
const { stdout, stderr } = await execFileAsync("git", args);
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
// Log stderr as info, as it often contains the primary status message
|
|
98
|
-
logger.debug(`Git reset stderr: ${stderr}`, { ...context, operation });
|
|
99
|
-
}
|
|
100
|
-
// Analyze output (primarily stderr for reset)
|
|
101
|
-
const message = stderr.trim() ||
|
|
102
|
-
stdout.trim() ||
|
|
103
|
-
`Reset successful (mode: ${input.mode || "mixed"}).`; // Default success message
|
|
104
|
-
const changesSummary = stderr.includes("Unstaged changes after reset")
|
|
105
|
-
? stderr
|
|
106
|
-
: undefined;
|
|
107
|
-
logger.info("git reset executed successfully", {
|
|
108
|
-
...context,
|
|
109
|
-
operation,
|
|
110
|
-
path: targetPath,
|
|
111
|
-
message,
|
|
112
|
-
changesSummary,
|
|
113
|
-
});
|
|
42
|
+
const message = stderr.trim() || stdout.trim() || `Reset successful (mode: ${params.mode}).`;
|
|
43
|
+
const changesSummary = stderr.includes("Unstaged changes after reset") ? stderr : undefined;
|
|
114
44
|
return { success: true, message, changesSummary };
|
|
115
45
|
}
|
|
116
46
|
catch (error) {
|
|
117
|
-
logger.error(`Failed to execute git reset command`, {
|
|
118
|
-
...context,
|
|
119
|
-
operation,
|
|
120
|
-
path: targetPath,
|
|
121
|
-
error: error.message,
|
|
122
|
-
stderr: error.stderr,
|
|
123
|
-
stdout: error.stdout,
|
|
124
|
-
});
|
|
125
47
|
const errorMessage = error.stderr || error.stdout || error.message || "";
|
|
126
|
-
|
|
48
|
+
logger.error(`Failed to execute git reset command`, { ...context, operation, errorMessage });
|
|
127
49
|
if (errorMessage.toLowerCase().includes("not a git repository")) {
|
|
128
|
-
throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}
|
|
129
|
-
}
|
|
130
|
-
if (errorMessage.includes("fatal: bad revision") ||
|
|
131
|
-
errorMessage.includes("unknown revision")) {
|
|
132
|
-
throw new McpError(BaseErrorCode.NOT_FOUND, `Invalid commit reference specified: '${input.commit}'. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
50
|
+
throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`);
|
|
133
51
|
}
|
|
134
|
-
if (errorMessage.includes("
|
|
135
|
-
|
|
136
|
-
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid mode ('${input.mode}') used with file paths. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
52
|
+
if (errorMessage.includes("bad revision")) {
|
|
53
|
+
throw new McpError(BaseErrorCode.NOT_FOUND, `Invalid commit reference specified: '${params.commit}'.`);
|
|
137
54
|
}
|
|
138
55
|
if (errorMessage.includes("unmerged paths")) {
|
|
139
|
-
throw new McpError(BaseErrorCode.CONFLICT,
|
|
56
|
+
throw new McpError(BaseErrorCode.CONFLICT, "Cannot reset due to unmerged files. Please resolve conflicts first.");
|
|
140
57
|
}
|
|
141
|
-
|
|
142
|
-
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to git reset for path: ${targetPath}. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
58
|
+
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Git reset failed: ${errorMessage}`);
|
|
143
59
|
}
|
|
144
60
|
}
|