@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,27 +1,49 @@
|
|
|
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 base input schema without refinement
|
|
12
12
|
const GitDiffInputBaseSchema = z.object({
|
|
13
|
-
path: z
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
+
commit1: z
|
|
20
|
+
.string()
|
|
21
|
+
.optional()
|
|
22
|
+
.describe("First commit, branch, or ref for comparison. If omitted, compares against the working tree or index (depending on 'staged')."),
|
|
23
|
+
commit2: z
|
|
24
|
+
.string()
|
|
25
|
+
.optional()
|
|
26
|
+
.describe("Second commit, branch, or ref for comparison. If omitted, compares commit1 against the working tree or index."),
|
|
27
|
+
staged: z
|
|
28
|
+
.boolean()
|
|
29
|
+
.optional()
|
|
30
|
+
.default(false)
|
|
31
|
+
.describe("Show diff of changes staged for the next commit (compares index against HEAD). Overrides commit1/commit2 if true."),
|
|
32
|
+
file: z
|
|
33
|
+
.string()
|
|
34
|
+
.optional()
|
|
35
|
+
.describe("Limit the diff output to a specific file path."),
|
|
36
|
+
includeUntracked: z
|
|
37
|
+
.boolean()
|
|
38
|
+
.optional()
|
|
39
|
+
.default(false)
|
|
40
|
+
.describe("Include untracked files in the diff output (shows their full content as new files). This is a non-standard extension."),
|
|
19
41
|
// Add options like --name-only, --stat, context lines (-U<n>) if needed
|
|
20
42
|
});
|
|
21
43
|
// Export the shape for registration
|
|
22
44
|
export const GitDiffInputShape = GitDiffInputBaseSchema.shape;
|
|
23
45
|
// Define the final schema with refinement for validation during execution
|
|
24
|
-
export const GitDiffInputSchema = GitDiffInputBaseSchema.refine(data => !(data.staged && (data.commit1 || data.commit2)), {
|
|
46
|
+
export const GitDiffInputSchema = GitDiffInputBaseSchema.refine((data) => !(data.staged && (data.commit1 || data.commit2)), {
|
|
25
47
|
message: "Cannot use 'staged' option with specific commit references (commit1 or commit2).",
|
|
26
48
|
path: ["staged", "commit1", "commit2"], // Indicate related fields
|
|
27
49
|
});
|
|
@@ -34,12 +56,12 @@ export const GitDiffInputSchema = GitDiffInputBaseSchema.refine(data => !(data.s
|
|
|
34
56
|
* @throws {McpError} Throws an McpError if path resolution, validation, or the git command fails unexpectedly.
|
|
35
57
|
*/
|
|
36
58
|
export async function diffGitChanges(input, context) {
|
|
37
|
-
const operation =
|
|
59
|
+
const operation = "diffGitChanges";
|
|
38
60
|
logger.debug(`Executing ${operation}`, { ...context, input });
|
|
39
61
|
let targetPath;
|
|
40
62
|
try {
|
|
41
63
|
// Resolve and sanitize the target path
|
|
42
|
-
if (input.path && input.path !==
|
|
64
|
+
if (input.path && input.path !== ".") {
|
|
43
65
|
targetPath = input.path;
|
|
44
66
|
}
|
|
45
67
|
else {
|
|
@@ -49,26 +71,36 @@ export async function diffGitChanges(input, context) {
|
|
|
49
71
|
}
|
|
50
72
|
targetPath = workingDir;
|
|
51
73
|
}
|
|
52
|
-
targetPath = sanitization.sanitizePath(targetPath, {
|
|
53
|
-
|
|
74
|
+
targetPath = sanitization.sanitizePath(targetPath, {
|
|
75
|
+
allowAbsolute: true,
|
|
76
|
+
}).sanitizedPath;
|
|
77
|
+
logger.debug("Sanitized path", {
|
|
78
|
+
...context,
|
|
79
|
+
operation,
|
|
80
|
+
sanitizedPath: targetPath,
|
|
81
|
+
});
|
|
54
82
|
}
|
|
55
83
|
catch (error) {
|
|
56
|
-
logger.error(
|
|
84
|
+
logger.error("Path resolution or sanitization failed", {
|
|
85
|
+
...context,
|
|
86
|
+
operation,
|
|
87
|
+
error,
|
|
88
|
+
});
|
|
57
89
|
if (error instanceof McpError)
|
|
58
90
|
throw error;
|
|
59
91
|
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
|
|
60
92
|
}
|
|
61
93
|
// Basic sanitization for refs and file path
|
|
62
|
-
const safeCommit1 = input.commit1?.replace(/[`$&;*()|<>]/g,
|
|
63
|
-
const safeCommit2 = input.commit2?.replace(/[`$&;*()|<>]/g,
|
|
64
|
-
const safeFile = input.file?.replace(/[`$&;*()|<>]/g,
|
|
65
|
-
let untrackedFilesDiff =
|
|
94
|
+
const safeCommit1 = input.commit1?.replace(/[`$&;*()|<>]/g, "");
|
|
95
|
+
const safeCommit2 = input.commit2?.replace(/[`$&;*()|<>]/g, "");
|
|
96
|
+
const safeFile = input.file?.replace(/[`$&;*()|<>]/g, "");
|
|
97
|
+
let untrackedFilesDiff = "";
|
|
66
98
|
let untrackedFilesCount = 0;
|
|
67
99
|
try {
|
|
68
100
|
// Construct the standard git diff command
|
|
69
101
|
let standardDiffCommand = `git -C "${targetPath}" diff`;
|
|
70
102
|
if (input.staged) {
|
|
71
|
-
standardDiffCommand +=
|
|
103
|
+
standardDiffCommand += " --staged"; // Or --cached
|
|
72
104
|
}
|
|
73
105
|
else {
|
|
74
106
|
// Add commit references if not doing staged diff
|
|
@@ -85,25 +117,38 @@ export async function diffGitChanges(input, context) {
|
|
|
85
117
|
if (safeFile) {
|
|
86
118
|
standardDiffCommand += ` -- "${safeFile}"`; // Use '--' to separate paths from revisions
|
|
87
119
|
}
|
|
88
|
-
logger.debug(`Executing standard diff command: ${standardDiffCommand}`, {
|
|
120
|
+
logger.debug(`Executing standard diff command: ${standardDiffCommand}`, {
|
|
121
|
+
...context,
|
|
122
|
+
operation,
|
|
123
|
+
});
|
|
89
124
|
const { stdout: standardStdout, stderr: standardStderr } = await execAsync(standardDiffCommand, { maxBuffer: 1024 * 1024 * 20 });
|
|
90
125
|
if (standardStderr) {
|
|
91
|
-
logger.warning(`Git diff (standard) stderr: ${standardStderr}`, {
|
|
126
|
+
logger.warning(`Git diff (standard) stderr: ${standardStderr}`, {
|
|
127
|
+
...context,
|
|
128
|
+
operation,
|
|
129
|
+
});
|
|
92
130
|
}
|
|
93
131
|
let combinedDiffOutput = standardStdout;
|
|
94
132
|
// Handle untracked files if requested
|
|
95
133
|
if (input.includeUntracked) {
|
|
96
|
-
logger.debug(
|
|
134
|
+
logger.debug("Including untracked files.", { ...context, operation });
|
|
97
135
|
const listUntrackedCommand = `git -C "${targetPath}" ls-files --others --exclude-standard`;
|
|
98
136
|
try {
|
|
99
137
|
const { stdout: untrackedFilesStdOut } = await execAsync(listUntrackedCommand);
|
|
100
|
-
const untrackedFiles = untrackedFilesStdOut
|
|
138
|
+
const untrackedFiles = untrackedFilesStdOut
|
|
139
|
+
.trim()
|
|
140
|
+
.split("\n")
|
|
141
|
+
.filter((f) => f); // Filter out empty lines
|
|
101
142
|
if (untrackedFiles.length > 0) {
|
|
102
|
-
logger.info(`Found ${untrackedFiles.length} untracked files.`, {
|
|
103
|
-
|
|
143
|
+
logger.info(`Found ${untrackedFiles.length} untracked files.`, {
|
|
144
|
+
...context,
|
|
145
|
+
operation,
|
|
146
|
+
untrackedFiles,
|
|
147
|
+
});
|
|
148
|
+
let individualUntrackedDiffs = "";
|
|
104
149
|
for (const untrackedFile of untrackedFiles) {
|
|
105
150
|
// Sanitize each untracked file path before using in command
|
|
106
|
-
const safeUntrackedFile = untrackedFile.replace(/[`$&;*()|<>]/g,
|
|
151
|
+
const safeUntrackedFile = untrackedFile.replace(/[`$&;*()|<>]/g, "");
|
|
107
152
|
// Skip if file path becomes empty after sanitization (unlikely but safe)
|
|
108
153
|
if (!safeUntrackedFile)
|
|
109
154
|
continue;
|
|
@@ -127,7 +172,16 @@ export async function diffGitChanges(input, context) {
|
|
|
127
172
|
}
|
|
128
173
|
else {
|
|
129
174
|
// If stdout is empty, then it's a more genuine failure.
|
|
130
|
-
logger.warning(`Failed to diff untracked file: ${safeUntrackedFile}. Error: ${untrackedError.message}`, {
|
|
175
|
+
logger.warning(`Failed to diff untracked file: ${safeUntrackedFile}. Error: ${untrackedError.message}`, {
|
|
176
|
+
...context,
|
|
177
|
+
operation,
|
|
178
|
+
file: safeUntrackedFile,
|
|
179
|
+
errorDetails: {
|
|
180
|
+
stderr: untrackedError.stderr,
|
|
181
|
+
stdout: untrackedError.stdout,
|
|
182
|
+
code: untrackedError.code,
|
|
183
|
+
},
|
|
184
|
+
});
|
|
131
185
|
individualUntrackedDiffs += `\n--- Diff for untracked file ${safeUntrackedFile} failed: ${untrackedError.message}\n`;
|
|
132
186
|
}
|
|
133
187
|
}
|
|
@@ -135,43 +189,69 @@ export async function diffGitChanges(input, context) {
|
|
|
135
189
|
if (individualUntrackedDiffs) {
|
|
136
190
|
// Add a separator if standard diff also had output
|
|
137
191
|
if (combinedDiffOutput.trim()) {
|
|
138
|
-
combinedDiffOutput +=
|
|
192
|
+
combinedDiffOutput += "\n";
|
|
139
193
|
}
|
|
140
194
|
combinedDiffOutput += individualUntrackedDiffs;
|
|
141
195
|
}
|
|
142
196
|
}
|
|
143
197
|
else {
|
|
144
|
-
logger.info(
|
|
198
|
+
logger.info("No untracked files found.", { ...context, operation });
|
|
145
199
|
}
|
|
146
200
|
}
|
|
147
201
|
catch (lsFilesError) {
|
|
148
|
-
logger.warning(`Failed to list untracked files. Error: ${lsFilesError.message}`, {
|
|
202
|
+
logger.warning(`Failed to list untracked files. Error: ${lsFilesError.message}`, {
|
|
203
|
+
...context,
|
|
204
|
+
operation,
|
|
205
|
+
error: lsFilesError.stderr || lsFilesError.stdout,
|
|
206
|
+
});
|
|
149
207
|
// Proceed without untracked files if listing fails
|
|
150
208
|
}
|
|
151
209
|
}
|
|
152
|
-
const isNoChanges = combinedDiffOutput.trim() ===
|
|
153
|
-
const finalDiffOutput = isNoChanges
|
|
154
|
-
|
|
210
|
+
const isNoChanges = combinedDiffOutput.trim() === "";
|
|
211
|
+
const finalDiffOutput = isNoChanges
|
|
212
|
+
? "No changes found."
|
|
213
|
+
: combinedDiffOutput;
|
|
214
|
+
let message = isNoChanges
|
|
215
|
+
? "No changes found."
|
|
216
|
+
: "Diff generated successfully.";
|
|
155
217
|
if (untrackedFilesCount > 0) {
|
|
156
218
|
message += ` Included ${untrackedFilesCount} untracked file(s).`;
|
|
157
219
|
}
|
|
158
|
-
logger.info(
|
|
159
|
-
|
|
220
|
+
logger.info(message, {
|
|
221
|
+
...context,
|
|
222
|
+
operation,
|
|
223
|
+
path: targetPath,
|
|
224
|
+
untrackedFilesProcessed: untrackedFilesCount,
|
|
225
|
+
});
|
|
226
|
+
return {
|
|
227
|
+
success: true,
|
|
228
|
+
diff: finalDiffOutput,
|
|
229
|
+
message,
|
|
230
|
+
untrackedFilesProcessed: untrackedFilesCount,
|
|
231
|
+
};
|
|
160
232
|
}
|
|
161
233
|
catch (error) {
|
|
162
234
|
// This catch block now primarily handles errors from the *standard* diff command
|
|
163
235
|
// or catastrophic failures before/after untracked file processing.
|
|
164
|
-
logger.error(`Failed to execute git diff operation`, {
|
|
165
|
-
|
|
236
|
+
logger.error(`Failed to execute git diff operation`, {
|
|
237
|
+
...context,
|
|
238
|
+
operation,
|
|
239
|
+
path: targetPath,
|
|
240
|
+
error: error.message,
|
|
241
|
+
stderr: error.stderr,
|
|
242
|
+
stdout: error.stdout,
|
|
243
|
+
});
|
|
244
|
+
const errorMessage = error.stderr || error.stdout || error.message || "";
|
|
166
245
|
// Handle specific error cases
|
|
167
|
-
if (errorMessage.toLowerCase().includes(
|
|
246
|
+
if (errorMessage.toLowerCase().includes("not a git repository")) {
|
|
168
247
|
throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`, { context, operation, originalError: error });
|
|
169
248
|
}
|
|
170
|
-
if (errorMessage.includes(
|
|
249
|
+
if (errorMessage.includes("fatal: bad object") ||
|
|
250
|
+
errorMessage.includes("unknown revision or path not in the working tree")) {
|
|
171
251
|
const invalidRef = input.commit1 || input.commit2 || input.file;
|
|
172
252
|
throw new McpError(BaseErrorCode.NOT_FOUND, `Invalid commit reference or file path specified: '${invalidRef}'. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
173
253
|
}
|
|
174
|
-
if (errorMessage.includes(
|
|
254
|
+
if (errorMessage.includes("ambiguous argument")) {
|
|
175
255
|
const ambiguousArg = input.commit1 || input.commit2 || input.file;
|
|
176
256
|
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Ambiguous argument provided: '${ambiguousArg}'. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
177
257
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
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 { requestContextService } from
|
|
6
|
+
import { requestContextService } from "../../../utils/index.js";
|
|
7
7
|
// Import the shape and the final schema/types
|
|
8
|
-
import { BaseErrorCode } from
|
|
9
|
-
import { diffGitChanges, GitDiffInputShape } from
|
|
8
|
+
import { BaseErrorCode } from "../../../types-global/errors.js"; // Keep direct import for types-global
|
|
9
|
+
import { diffGitChanges, GitDiffInputShape, } from "./logic.js";
|
|
10
10
|
let _getWorkingDirectory;
|
|
11
11
|
let _getSessionId;
|
|
12
12
|
/**
|
|
@@ -18,9 +18,9 @@ let _getSessionId;
|
|
|
18
18
|
export function initializeGitDiffStateAccessors(getWdFn, getSidFn) {
|
|
19
19
|
_getWorkingDirectory = getWdFn;
|
|
20
20
|
_getSessionId = getSidFn;
|
|
21
|
-
logger.info(
|
|
21
|
+
logger.info("State accessors initialized for git_diff tool registration.");
|
|
22
22
|
}
|
|
23
|
-
const TOOL_NAME =
|
|
23
|
+
const TOOL_NAME = "git_diff";
|
|
24
24
|
const TOOL_DESCRIPTION = "Shows changes between commits, commit and working tree, etc. Can show staged changes or diff specific files. An optional 'includeUntracked' parameter (boolean) can be used to also show the content of untracked files. Returns the diff output as plain text.";
|
|
25
25
|
/**
|
|
26
26
|
* Registers the git_diff tool with the MCP server.
|
|
@@ -30,16 +30,19 @@ const TOOL_DESCRIPTION = "Shows changes between commits, commit and working tree
|
|
|
30
30
|
*/
|
|
31
31
|
export async function registerGitDiffTool(server) {
|
|
32
32
|
if (!_getWorkingDirectory || !_getSessionId) {
|
|
33
|
-
throw new Error(
|
|
33
|
+
throw new Error("State accessors for git_diff must be initialized before registration.");
|
|
34
34
|
}
|
|
35
|
-
const operation =
|
|
35
|
+
const operation = "registerGitDiffTool";
|
|
36
36
|
const context = requestContextService.createRequestContext({ operation });
|
|
37
37
|
await ErrorHandler.tryCatch(async () => {
|
|
38
38
|
// Use the exported shape for registration
|
|
39
39
|
server.tool(TOOL_NAME, TOOL_DESCRIPTION, GitDiffInputShape, // Provide the Zod base schema shape
|
|
40
40
|
async (validatedArgs, callContext) => {
|
|
41
|
-
const toolOperation =
|
|
42
|
-
const requestContext = requestContextService.createRequestContext({
|
|
41
|
+
const toolOperation = "tool:git_diff";
|
|
42
|
+
const requestContext = requestContextService.createRequestContext({
|
|
43
|
+
operation: toolOperation,
|
|
44
|
+
parentContext: callContext,
|
|
45
|
+
});
|
|
43
46
|
const sessionId = _getSessionId(requestContext);
|
|
44
47
|
const getWorkingDirectoryForSession = () => {
|
|
45
48
|
return _getWorkingDirectory(sessionId);
|
|
@@ -55,11 +58,11 @@ export async function registerGitDiffTool(server) {
|
|
|
55
58
|
const diffResult = await diffGitChanges(validatedArgs, logicContext);
|
|
56
59
|
// Format the result (the diff string) as plain text within TextContent
|
|
57
60
|
const resultContent = {
|
|
58
|
-
type:
|
|
61
|
+
type: "text",
|
|
59
62
|
// Return the raw diff output directly
|
|
60
63
|
text: diffResult.diff,
|
|
61
64
|
// Indicate the content type is plain text diff
|
|
62
|
-
contentType:
|
|
65
|
+
contentType: "text/plain; charset=utf-8", // Or 'text/x-diff'
|
|
63
66
|
};
|
|
64
67
|
logger.info(`Tool ${TOOL_NAME} executed successfully: ${diffResult.message}`, logicContext);
|
|
65
68
|
// Success is determined by the logic function
|
|
@@ -74,4 +77,3 @@ export async function registerGitDiffTool(server) {
|
|
|
74
77
|
logger.info(`Tool registered: ${TOOL_NAME}`, context);
|
|
75
78
|
}, { operation, context, critical: true });
|
|
76
79
|
}
|
|
77
|
-
;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Barrel file for the gitFetch tool.
|
|
3
3
|
*/
|
|
4
|
-
export { registerGitFetchTool, initializeGitFetchStateAccessors } from
|
|
4
|
+
export { registerGitFetchTool, initializeGitFetchStateAccessors, } from "./registration.js";
|
|
5
5
|
// Export types if needed elsewhere, e.g.:
|
|
6
6
|
// export type { GitFetchInput, GitFetchResult } from './logic.js';
|
|
@@ -1,19 +1,35 @@
|
|
|
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_fetch tool using Zod
|
|
12
12
|
export const GitFetchInputSchema = z.object({
|
|
13
|
-
path: z
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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 fetch from (e.g., 'origin'). If omitted, fetches from 'origin' or the default configured remote."),
|
|
23
|
+
prune: z
|
|
24
|
+
.boolean()
|
|
25
|
+
.optional()
|
|
26
|
+
.default(false)
|
|
27
|
+
.describe("Before fetching, remove any remote-tracking references that no longer exist on the remote."),
|
|
28
|
+
tags: z
|
|
29
|
+
.boolean()
|
|
30
|
+
.optional()
|
|
31
|
+
.default(false)
|
|
32
|
+
.describe("Fetch all tags from the remote (in addition to whatever else is fetched)."),
|
|
17
33
|
all: z.boolean().optional().default(false).describe("Fetch all remotes."),
|
|
18
34
|
// Add options like --depth, specific refspecs if needed
|
|
19
35
|
});
|
|
@@ -26,12 +42,12 @@ export const GitFetchInputSchema = z.object({
|
|
|
26
42
|
* @throws {McpError} Throws an McpError if path resolution, validation, or the git command fails unexpectedly.
|
|
27
43
|
*/
|
|
28
44
|
export async function fetchGitRemote(input, context) {
|
|
29
|
-
const operation =
|
|
45
|
+
const operation = "fetchGitRemote";
|
|
30
46
|
logger.debug(`Executing ${operation}`, { ...context, input });
|
|
31
47
|
let targetPath;
|
|
32
48
|
try {
|
|
33
49
|
// Resolve and sanitize the target path
|
|
34
|
-
if (input.path && input.path !==
|
|
50
|
+
if (input.path && input.path !== ".") {
|
|
35
51
|
targetPath = input.path;
|
|
36
52
|
}
|
|
37
53
|
else {
|
|
@@ -41,28 +57,38 @@ export async function fetchGitRemote(input, context) {
|
|
|
41
57
|
}
|
|
42
58
|
targetPath = workingDir;
|
|
43
59
|
}
|
|
44
|
-
targetPath = sanitization.sanitizePath(targetPath, {
|
|
45
|
-
|
|
60
|
+
targetPath = sanitization.sanitizePath(targetPath, {
|
|
61
|
+
allowAbsolute: true,
|
|
62
|
+
}).sanitizedPath;
|
|
63
|
+
logger.debug("Sanitized path", {
|
|
64
|
+
...context,
|
|
65
|
+
operation,
|
|
66
|
+
sanitizedPath: targetPath,
|
|
67
|
+
});
|
|
46
68
|
}
|
|
47
69
|
catch (error) {
|
|
48
|
-
logger.error(
|
|
70
|
+
logger.error("Path resolution or sanitization failed", {
|
|
71
|
+
...context,
|
|
72
|
+
operation,
|
|
73
|
+
error,
|
|
74
|
+
});
|
|
49
75
|
if (error instanceof McpError)
|
|
50
76
|
throw error;
|
|
51
77
|
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
|
|
52
78
|
}
|
|
53
79
|
// Basic sanitization for remote name
|
|
54
|
-
const safeRemote = input.remote?.replace(/[^a-zA-Z0-9_.\-/]/g,
|
|
80
|
+
const safeRemote = input.remote?.replace(/[^a-zA-Z0-9_.\-/]/g, "");
|
|
55
81
|
try {
|
|
56
82
|
// Construct the git fetch command
|
|
57
83
|
let command = `git -C "${targetPath}" fetch`;
|
|
58
84
|
if (input.prune) {
|
|
59
|
-
command +=
|
|
85
|
+
command += " --prune";
|
|
60
86
|
}
|
|
61
87
|
if (input.tags) {
|
|
62
|
-
command +=
|
|
88
|
+
command += " --tags";
|
|
63
89
|
}
|
|
64
90
|
if (input.all) {
|
|
65
|
-
command +=
|
|
91
|
+
command += " --all";
|
|
66
92
|
}
|
|
67
93
|
else if (safeRemote) {
|
|
68
94
|
command += ` ${safeRemote}`; // Fetch specific remote if 'all' is not used
|
|
@@ -71,47 +97,50 @@ export async function fetchGitRemote(input, context) {
|
|
|
71
97
|
logger.debug(`Executing command: ${command}`, { ...context, operation });
|
|
72
98
|
// Execute command. Fetch output is primarily on stderr.
|
|
73
99
|
const { stdout, stderr } = await execAsync(command);
|
|
74
|
-
logger.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
let message = stderr.trim() || 'Fetch command executed.'; // Use stderr as the primary message
|
|
78
|
-
let summary = undefined;
|
|
79
|
-
// Check for common patterns in stderr
|
|
80
|
-
if (stderr.includes('Updating') || stderr.includes('->') || stderr.includes('new tag') || stderr.includes('new branch')) {
|
|
81
|
-
message = 'Fetch successful.';
|
|
82
|
-
summary = stderr.trim(); // Use the full stderr as summary
|
|
83
|
-
}
|
|
84
|
-
else if (stderr.trim() === '') {
|
|
85
|
-
// Sometimes fetch completes successfully with no output if nothing changed
|
|
86
|
-
message = 'Fetch successful (no changes detected).';
|
|
100
|
+
logger.debug(`Git fetch stdout: ${stdout}`, { ...context, operation }); // stdout is usually empty
|
|
101
|
+
if (stderr) {
|
|
102
|
+
logger.debug(`Git fetch stderr: ${stderr}`, { ...context, operation }); // stderr contains fetch details
|
|
87
103
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
104
|
+
// Analyze stderr for success/summary
|
|
105
|
+
const message = "Fetch successful.";
|
|
106
|
+
const summary = stderr.trim() || "No changes detected.";
|
|
107
|
+
logger.info(message, {
|
|
108
|
+
...context,
|
|
109
|
+
operation,
|
|
110
|
+
path: targetPath,
|
|
111
|
+
summary,
|
|
112
|
+
});
|
|
95
113
|
return { success: true, message, summary };
|
|
96
114
|
}
|
|
97
115
|
catch (error) {
|
|
98
|
-
logger.error(`Failed to execute git fetch command`, {
|
|
99
|
-
|
|
116
|
+
logger.error(`Failed to execute git fetch command`, {
|
|
117
|
+
...context,
|
|
118
|
+
operation,
|
|
119
|
+
path: targetPath,
|
|
120
|
+
error: error.message,
|
|
121
|
+
stderr: error.stderr,
|
|
122
|
+
stdout: error.stdout,
|
|
123
|
+
});
|
|
124
|
+
const errorMessage = error.stderr || error.stdout || error.message || "";
|
|
100
125
|
// Handle specific error cases
|
|
101
|
-
if (errorMessage.toLowerCase().includes(
|
|
126
|
+
if (errorMessage.toLowerCase().includes("not a git repository")) {
|
|
102
127
|
throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`, { context, operation, originalError: error });
|
|
103
128
|
}
|
|
104
|
-
if (errorMessage.includes(
|
|
105
|
-
|
|
129
|
+
if (errorMessage.includes("resolve host") ||
|
|
130
|
+
errorMessage.includes("Could not read from remote repository") ||
|
|
131
|
+
errorMessage.includes("Connection timed out")) {
|
|
132
|
+
throw new McpError(BaseErrorCode.SERVICE_UNAVAILABLE, `Failed to connect to remote repository '${input.remote || "default"}'. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
106
133
|
}
|
|
107
|
-
if (errorMessage.includes(
|
|
134
|
+
if (errorMessage.includes("fatal: ") &&
|
|
135
|
+
errorMessage.includes("couldn't find remote ref")) {
|
|
108
136
|
throw new McpError(BaseErrorCode.NOT_FOUND, `Remote ref not found. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
109
137
|
}
|
|
110
|
-
if (errorMessage.includes(
|
|
111
|
-
|
|
138
|
+
if (errorMessage.includes("Authentication failed") ||
|
|
139
|
+
errorMessage.includes("Permission denied")) {
|
|
140
|
+
throw new McpError(BaseErrorCode.UNAUTHORIZED, `Authentication failed for remote repository '${input.remote || "default"}'. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
112
141
|
}
|
|
113
|
-
if (errorMessage.includes(
|
|
114
|
-
throw new McpError(BaseErrorCode.NOT_FOUND, `Remote '${input.remote ||
|
|
142
|
+
if (errorMessage.includes("does not appear to be a git repository")) {
|
|
143
|
+
throw new McpError(BaseErrorCode.NOT_FOUND, `Remote '${input.remote || "default"}' does not appear to be a git repository. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
115
144
|
}
|
|
116
145
|
// Generic internal error for other failures
|
|
117
146
|
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to git fetch for path: ${targetPath}. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
@@ -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 { fetchGitRemote, GitFetchInputSchema } from
|
|
6
|
+
import { BaseErrorCode } from "../../../types-global/errors.js"; // Keep direct import for types-global
|
|
7
|
+
import { requestContextService } from "../../../utils/index.js";
|
|
8
|
+
import { fetchGitRemote, GitFetchInputSchema, } from "./logic.js";
|
|
9
9
|
let _getWorkingDirectory;
|
|
10
10
|
let _getSessionId;
|
|
11
11
|
/**
|
|
@@ -17,9 +17,9 @@ let _getSessionId;
|
|
|
17
17
|
export function initializeGitFetchStateAccessors(getWdFn, getSidFn) {
|
|
18
18
|
_getWorkingDirectory = getWdFn;
|
|
19
19
|
_getSessionId = getSidFn;
|
|
20
|
-
logger.info(
|
|
20
|
+
logger.info("State accessors initialized for git_fetch tool registration.");
|
|
21
21
|
}
|
|
22
|
-
const TOOL_NAME =
|
|
22
|
+
const TOOL_NAME = "git_fetch";
|
|
23
23
|
const TOOL_DESCRIPTION = "Downloads objects and refs from one or more other repositories. Can fetch specific remotes or all, prune stale branches, and fetch tags.";
|
|
24
24
|
/**
|
|
25
25
|
* Registers the git_fetch tool with the MCP server.
|
|
@@ -29,15 +29,18 @@ const TOOL_DESCRIPTION = "Downloads objects and refs from one or more other repo
|
|
|
29
29
|
*/
|
|
30
30
|
export async function registerGitFetchTool(server) {
|
|
31
31
|
if (!_getWorkingDirectory || !_getSessionId) {
|
|
32
|
-
throw new Error(
|
|
32
|
+
throw new Error("State accessors for git_fetch must be initialized before registration.");
|
|
33
33
|
}
|
|
34
|
-
const operation =
|
|
34
|
+
const operation = "registerGitFetchTool";
|
|
35
35
|
const context = requestContextService.createRequestContext({ operation });
|
|
36
36
|
await ErrorHandler.tryCatch(async () => {
|
|
37
37
|
server.tool(TOOL_NAME, TOOL_DESCRIPTION, GitFetchInputSchema.shape, // Provide the Zod schema shape
|
|
38
38
|
async (validatedArgs, callContext) => {
|
|
39
|
-
const toolOperation =
|
|
40
|
-
const requestContext = requestContextService.createRequestContext({
|
|
39
|
+
const toolOperation = "tool:git_fetch";
|
|
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 registerGitFetchTool(server) {
|
|
|
53
56
|
const fetchResult = await fetchGitRemote(validatedArgs, logicContext);
|
|
54
57
|
// Format the result as a JSON string within TextContent
|
|
55
58
|
const resultContent = {
|
|
56
|
-
type:
|
|
59
|
+
type: "text",
|
|
57
60
|
// Stringify the entire GitFetchResult object
|
|
58
61
|
text: JSON.stringify(fetchResult, null, 2), // Pretty-print JSON
|
|
59
|
-
contentType:
|
|
62
|
+
contentType: "application/json",
|
|
60
63
|
};
|
|
61
64
|
logger.info(`Tool ${TOOL_NAME} executed successfully: ${fetchResult.message}`, logicContext);
|
|
62
65
|
// Success is determined by the logic function and included in the result object
|
|
@@ -71,4 +74,3 @@ export async function registerGitFetchTool(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_init tool.
|
|
3
3
|
* Exports the registration function and the state accessor initializer.
|
|
4
4
|
*/
|
|
5
|
-
export { registerGitInitTool, initializeGitInitStateAccessors } from
|
|
5
|
+
export { registerGitInitTool, initializeGitInitStateAccessors, } from "./registration.js";
|
|
6
6
|
// Export types if needed elsewhere, e.g.:
|
|
7
7
|
// export type { GitInitInput, GitInitResult } from './logic.js';
|