@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,12 +1,17 @@
|
|
|
1
|
-
import { exec } from
|
|
2
|
-
import { promisify } from
|
|
3
|
-
import { z } from
|
|
4
|
-
import { BaseErrorCode, McpError } from
|
|
5
|
-
import { logger, sanitization } from
|
|
1
|
+
import { exec } from "child_process";
|
|
2
|
+
import { promisify } from "util";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; // Direct import for types-global
|
|
5
|
+
import { logger, sanitization } from "../../../utils/index.js"; // logger (./utils/internal/logger.js), RequestContext (./utils/internal/requestContext.js), sanitization (./utils/security/sanitization.js)
|
|
6
6
|
const execAsync = promisify(exec);
|
|
7
7
|
// Define the input schema for the git_status tool using Zod
|
|
8
8
|
export const GitStatusInputSchema = z.object({
|
|
9
|
-
path: z
|
|
9
|
+
path: z
|
|
10
|
+
.string()
|
|
11
|
+
.min(1)
|
|
12
|
+
.optional()
|
|
13
|
+
.default(".")
|
|
14
|
+
.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."),
|
|
10
15
|
});
|
|
11
16
|
/**
|
|
12
17
|
* Parses the output of 'git status --porcelain=v1 -b'.
|
|
@@ -16,7 +21,7 @@ export const GitStatusInputSchema = z.object({
|
|
|
16
21
|
* @returns {GitStatusResult} - Structured status information.
|
|
17
22
|
*/
|
|
18
23
|
function parseGitStatusPorcelainV1(porcelainOutput) {
|
|
19
|
-
const lines = porcelainOutput.trim().split(
|
|
24
|
+
const lines = porcelainOutput.trim().split("\n");
|
|
20
25
|
const result = {
|
|
21
26
|
current_branch: null,
|
|
22
27
|
staged_changes: {},
|
|
@@ -25,10 +30,10 @@ function parseGitStatusPorcelainV1(porcelainOutput) {
|
|
|
25
30
|
conflicted_files: [],
|
|
26
31
|
is_clean: true, // Assume clean initially
|
|
27
32
|
};
|
|
28
|
-
if (lines.length === 0 || (lines.length === 1 && lines[0] ===
|
|
33
|
+
if (lines.length === 0 || (lines.length === 1 && lines[0] === "")) {
|
|
29
34
|
return result;
|
|
30
35
|
}
|
|
31
|
-
if (lines[0].startsWith(
|
|
36
|
+
if (lines[0].startsWith("## ")) {
|
|
32
37
|
const branchLine = lines.shift();
|
|
33
38
|
const standardBranchMatch = branchLine.match(/^## ([^ ]+?)(?:\.\.\.| \[.*\]|$)/);
|
|
34
39
|
const noCommitsMatch = branchLine.match(/^## No commits yet on (.+)/);
|
|
@@ -40,11 +45,13 @@ function parseGitStatusPorcelainV1(porcelainOutput) {
|
|
|
40
45
|
result.current_branch = `${noCommitsMatch[1]} (no commits yet)`;
|
|
41
46
|
}
|
|
42
47
|
else if (detachedMatch) {
|
|
43
|
-
result.current_branch =
|
|
48
|
+
result.current_branch = "HEAD (detached)";
|
|
44
49
|
}
|
|
45
50
|
else {
|
|
46
|
-
logger.warning(
|
|
47
|
-
|
|
51
|
+
logger.warning("Could not parse branch information from line:", {
|
|
52
|
+
branchLine,
|
|
53
|
+
});
|
|
54
|
+
result.current_branch = "(unknown)";
|
|
48
55
|
}
|
|
49
56
|
}
|
|
50
57
|
for (const line of lines) {
|
|
@@ -56,40 +63,41 @@ function parseGitStatusPorcelainV1(porcelainOutput) {
|
|
|
56
63
|
const stagedStatusChar = xy[0];
|
|
57
64
|
const unstagedStatusChar = xy[1];
|
|
58
65
|
// Handle untracked files
|
|
59
|
-
if (xy ===
|
|
66
|
+
if (xy === "??") {
|
|
60
67
|
result.untracked_files.push(file);
|
|
61
68
|
continue;
|
|
62
69
|
}
|
|
63
70
|
// Handle conflicted files (unmerged paths)
|
|
64
71
|
// DD = both deleted, AU = added by us, UD = deleted by them, UA = added by them, DU = deleted by us
|
|
65
72
|
// AA = both added, UU = both modified
|
|
66
|
-
if (stagedStatusChar ===
|
|
67
|
-
|
|
68
|
-
(stagedStatusChar ===
|
|
73
|
+
if (stagedStatusChar === "U" ||
|
|
74
|
+
unstagedStatusChar === "U" ||
|
|
75
|
+
(stagedStatusChar === "D" && unstagedStatusChar === "D") ||
|
|
76
|
+
(stagedStatusChar === "A" && unstagedStatusChar === "A")) {
|
|
69
77
|
result.conflicted_files.push(file);
|
|
70
78
|
continue; // Conflicted files are handled separately and not in staged/unstaged
|
|
71
79
|
}
|
|
72
80
|
// Handle staged changes (index status)
|
|
73
|
-
if (stagedStatusChar !==
|
|
81
|
+
if (stagedStatusChar !== " " && stagedStatusChar !== "?") {
|
|
74
82
|
let statusDesc = undefined;
|
|
75
83
|
switch (stagedStatusChar) {
|
|
76
|
-
case
|
|
77
|
-
statusDesc =
|
|
84
|
+
case "M":
|
|
85
|
+
statusDesc = "Modified";
|
|
78
86
|
break;
|
|
79
|
-
case
|
|
80
|
-
statusDesc =
|
|
87
|
+
case "A":
|
|
88
|
+
statusDesc = "Added";
|
|
81
89
|
break;
|
|
82
|
-
case
|
|
83
|
-
statusDesc =
|
|
90
|
+
case "D":
|
|
91
|
+
statusDesc = "Deleted";
|
|
84
92
|
break;
|
|
85
|
-
case
|
|
86
|
-
statusDesc =
|
|
93
|
+
case "R":
|
|
94
|
+
statusDesc = "Renamed";
|
|
87
95
|
break;
|
|
88
|
-
case
|
|
89
|
-
statusDesc =
|
|
96
|
+
case "C":
|
|
97
|
+
statusDesc = "Copied";
|
|
90
98
|
break;
|
|
91
|
-
case
|
|
92
|
-
statusDesc =
|
|
99
|
+
case "T":
|
|
100
|
+
statusDesc = "TypeChanged";
|
|
93
101
|
break;
|
|
94
102
|
}
|
|
95
103
|
if (statusDesc) {
|
|
@@ -100,17 +108,17 @@ function parseGitStatusPorcelainV1(porcelainOutput) {
|
|
|
100
108
|
}
|
|
101
109
|
}
|
|
102
110
|
// Handle unstaged changes (worktree status)
|
|
103
|
-
if (unstagedStatusChar !==
|
|
111
|
+
if (unstagedStatusChar !== " " && unstagedStatusChar !== "?") {
|
|
104
112
|
let statusDesc = undefined;
|
|
105
113
|
switch (unstagedStatusChar) {
|
|
106
|
-
case
|
|
107
|
-
statusDesc =
|
|
114
|
+
case "M":
|
|
115
|
+
statusDesc = "Modified";
|
|
108
116
|
break;
|
|
109
|
-
case
|
|
110
|
-
statusDesc =
|
|
117
|
+
case "D":
|
|
118
|
+
statusDesc = "Deleted";
|
|
111
119
|
break;
|
|
112
|
-
case
|
|
113
|
-
statusDesc =
|
|
120
|
+
case "T":
|
|
121
|
+
statusDesc = "TypeChanged";
|
|
114
122
|
break;
|
|
115
123
|
// 'A' (Added but not committed) is handled by '??' (untracked)
|
|
116
124
|
// 'R' and 'C' in worktree without being staged are complex, often appear as deleted + untracked
|
|
@@ -123,10 +131,11 @@ function parseGitStatusPorcelainV1(porcelainOutput) {
|
|
|
123
131
|
}
|
|
124
132
|
}
|
|
125
133
|
}
|
|
126
|
-
result.is_clean =
|
|
127
|
-
Object.keys(result.
|
|
128
|
-
|
|
129
|
-
|
|
134
|
+
result.is_clean =
|
|
135
|
+
Object.keys(result.staged_changes).length === 0 &&
|
|
136
|
+
Object.keys(result.unstaged_changes).length === 0 &&
|
|
137
|
+
result.untracked_files.length === 0 &&
|
|
138
|
+
result.conflicted_files.length === 0;
|
|
130
139
|
return result;
|
|
131
140
|
}
|
|
132
141
|
/**
|
|
@@ -137,17 +146,19 @@ function parseGitStatusPorcelainV1(porcelainOutput) {
|
|
|
137
146
|
* @returns {Promise<GitStatusResult>} A promise that resolves with the structured git status.
|
|
138
147
|
* @throws {McpError} Throws an McpError if path resolution or validation fails, or if the git command fails.
|
|
139
148
|
*/
|
|
140
|
-
export async function getGitStatus(input, context
|
|
141
|
-
|
|
142
|
-
const operation = 'getGitStatus';
|
|
149
|
+
export async function getGitStatus(input, context) {
|
|
150
|
+
const operation = "getGitStatus";
|
|
143
151
|
logger.debug(`Executing ${operation}`, { ...context, input });
|
|
144
152
|
let targetPath;
|
|
145
153
|
try {
|
|
146
154
|
// Resolve the target path
|
|
147
|
-
if (input.path && input.path !==
|
|
155
|
+
if (input.path && input.path !== ".") {
|
|
148
156
|
// Use the provided path directly
|
|
149
157
|
targetPath = input.path;
|
|
150
|
-
logger.debug(`Using provided path: ${targetPath}`, {
|
|
158
|
+
logger.debug(`Using provided path: ${targetPath}`, {
|
|
159
|
+
...context,
|
|
160
|
+
operation,
|
|
161
|
+
});
|
|
151
162
|
}
|
|
152
163
|
else {
|
|
153
164
|
// Path is '.' or undefined, try to get the session's working directory
|
|
@@ -156,15 +167,29 @@ export async function getGitStatus(input, context // Add getter to context
|
|
|
156
167
|
throw new McpError(BaseErrorCode.VALIDATION_ERROR, "No path provided and no working directory set for the session.", { context, operation });
|
|
157
168
|
}
|
|
158
169
|
targetPath = workingDir;
|
|
159
|
-
logger.debug(`Using session working directory: ${targetPath}`, {
|
|
170
|
+
logger.debug(`Using session working directory: ${targetPath}`, {
|
|
171
|
+
...context,
|
|
172
|
+
operation,
|
|
173
|
+
sessionId: context.sessionId,
|
|
174
|
+
});
|
|
160
175
|
}
|
|
161
176
|
// Sanitize the resolved path
|
|
162
|
-
const sanitizedPathInfo = sanitization.sanitizePath(targetPath, {
|
|
163
|
-
|
|
177
|
+
const sanitizedPathInfo = sanitization.sanitizePath(targetPath, {
|
|
178
|
+
allowAbsolute: true,
|
|
179
|
+
});
|
|
180
|
+
logger.debug("Sanitized path", {
|
|
181
|
+
...context,
|
|
182
|
+
operation,
|
|
183
|
+
sanitizedPathInfo,
|
|
184
|
+
});
|
|
164
185
|
targetPath = sanitizedPathInfo.sanitizedPath; // Use the sanitized path going forward
|
|
165
186
|
}
|
|
166
187
|
catch (error) {
|
|
167
|
-
logger.error(
|
|
188
|
+
logger.error("Path resolution or sanitization failed", {
|
|
189
|
+
...context,
|
|
190
|
+
operation,
|
|
191
|
+
error,
|
|
192
|
+
});
|
|
168
193
|
if (error instanceof McpError) {
|
|
169
194
|
throw error;
|
|
170
195
|
}
|
|
@@ -180,7 +205,11 @@ export async function getGitStatus(input, context // Add getter to context
|
|
|
180
205
|
// Log stderr as warning but proceed to parse stdout
|
|
181
206
|
logger.warning(`Git status command produced stderr (may be informational)`, { ...context, operation, stderr });
|
|
182
207
|
}
|
|
183
|
-
logger.
|
|
208
|
+
logger.debug(`${operation} command executed, parsing output...`, {
|
|
209
|
+
...context,
|
|
210
|
+
operation,
|
|
211
|
+
path: targetPath,
|
|
212
|
+
});
|
|
184
213
|
// Parse the porcelain output
|
|
185
214
|
const structuredResult = parseGitStatusPorcelainV1(stdout);
|
|
186
215
|
// If parsing resulted in clean state but no branch, re-check branch explicitly
|
|
@@ -190,26 +219,39 @@ export async function getGitStatus(input, context // Add getter to context
|
|
|
190
219
|
const branchCommand = `git -C "${targetPath}" rev-parse --abbrev-ref HEAD`;
|
|
191
220
|
const { stdout: branchStdout } = await execAsync(branchCommand);
|
|
192
221
|
const currentBranchName = branchStdout.trim(); // Renamed variable for clarity
|
|
193
|
-
if (currentBranchName && currentBranchName !==
|
|
222
|
+
if (currentBranchName && currentBranchName !== "HEAD") {
|
|
194
223
|
structuredResult.current_branch = currentBranchName;
|
|
195
224
|
}
|
|
196
|
-
else if (currentBranchName ===
|
|
225
|
+
else if (currentBranchName === "HEAD" &&
|
|
226
|
+
!structuredResult.current_branch) {
|
|
197
227
|
// If rev-parse returns HEAD and we still don't have a branch (e.g. detached from no-commits branch)
|
|
198
|
-
structuredResult.current_branch =
|
|
228
|
+
structuredResult.current_branch = "HEAD (detached)";
|
|
199
229
|
}
|
|
200
230
|
}
|
|
201
231
|
catch (branchError) {
|
|
202
232
|
// Ignore error if rev-parse fails (e.g., still no commits)
|
|
203
|
-
logger.debug(
|
|
233
|
+
logger.debug("Could not determine branch via rev-parse, likely no commits yet.", { ...context, operation, branchError });
|
|
204
234
|
}
|
|
205
235
|
}
|
|
206
|
-
logger.info(
|
|
236
|
+
logger.info("git status parsed successfully", {
|
|
237
|
+
...context,
|
|
238
|
+
operation,
|
|
239
|
+
path: targetPath,
|
|
240
|
+
isClean: structuredResult.is_clean,
|
|
241
|
+
currentBranch: structuredResult.current_branch,
|
|
242
|
+
});
|
|
207
243
|
return structuredResult; // Return the structured JSON object
|
|
208
244
|
}
|
|
209
245
|
catch (error) {
|
|
210
|
-
logger.error(`Failed to execute or parse git status command`, {
|
|
211
|
-
|
|
212
|
-
|
|
246
|
+
logger.error(`Failed to execute or parse git status command`, {
|
|
247
|
+
...context,
|
|
248
|
+
operation,
|
|
249
|
+
path: targetPath,
|
|
250
|
+
error: error.message,
|
|
251
|
+
stderr: error.stderr,
|
|
252
|
+
});
|
|
253
|
+
const errorMessage = error.stderr || error.message || "";
|
|
254
|
+
if (errorMessage.toLowerCase().includes("not a git repository")) {
|
|
213
255
|
throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`, { context, operation, originalError: error });
|
|
214
256
|
}
|
|
215
257
|
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to get git status for path: ${targetPath}. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { BaseErrorCode } from
|
|
2
|
-
import { ErrorHandler, logger, requestContextService } from
|
|
1
|
+
import { BaseErrorCode } from "../../../types-global/errors.js"; // Direct import for types-global
|
|
2
|
+
import { ErrorHandler, logger, requestContextService, } from "../../../utils/index.js"; // logger (./utils/internal/logger.js), ErrorHandler (./utils/internal/errorHandler.js), requestContextService (./utils/internal/requestContext.js)
|
|
3
3
|
// Import the result type along with the function and input schema
|
|
4
|
-
import { getGitStatus, GitStatusInputSchema } from
|
|
4
|
+
import { getGitStatus, GitStatusInputSchema, } from "./logic.js";
|
|
5
5
|
let _getWorkingDirectory;
|
|
6
6
|
let _getSessionId;
|
|
7
7
|
/**
|
|
@@ -13,10 +13,10 @@ let _getSessionId;
|
|
|
13
13
|
export function initializeGitStatusStateAccessors(getWdFn, getSidFn) {
|
|
14
14
|
_getWorkingDirectory = getWdFn;
|
|
15
15
|
_getSessionId = getSidFn;
|
|
16
|
-
logger.info(
|
|
16
|
+
logger.info("State accessors initialized for git_status tool registration.");
|
|
17
17
|
}
|
|
18
|
-
const TOOL_NAME =
|
|
19
|
-
const TOOL_DESCRIPTION =
|
|
18
|
+
const TOOL_NAME = "git_status";
|
|
19
|
+
const TOOL_DESCRIPTION = "Retrieves the status of a Git repository. Returns a JSON object detailing the current branch, cleanliness, and changes. Staged and unstaged changes are grouped by status (e.g., Added, Modified), alongside lists of untracked and conflicted files.";
|
|
20
20
|
/**
|
|
21
21
|
* Registers the git_status tool with the MCP server.
|
|
22
22
|
* Uses the high-level server.tool() method for registration, schema validation, and routing.
|
|
@@ -27,16 +27,19 @@ const TOOL_DESCRIPTION = 'Retrieves the status of a Git repository. Returns a JS
|
|
|
27
27
|
*/
|
|
28
28
|
export const registerGitStatusTool = async (server) => {
|
|
29
29
|
if (!_getWorkingDirectory || !_getSessionId) {
|
|
30
|
-
throw new Error(
|
|
30
|
+
throw new Error("State accessors for git_status must be initialized before registration.");
|
|
31
31
|
}
|
|
32
|
-
const operation =
|
|
32
|
+
const operation = "registerGitStatusTool";
|
|
33
33
|
const context = requestContextService.createRequestContext({ operation });
|
|
34
34
|
await ErrorHandler.tryCatch(async () => {
|
|
35
35
|
server.tool(TOOL_NAME, TOOL_DESCRIPTION, GitStatusInputSchema.shape, // Provide the Zod schema shape
|
|
36
36
|
async (validatedArgs, callContext) => {
|
|
37
|
-
const toolOperation =
|
|
37
|
+
const toolOperation = "tool:git_status";
|
|
38
38
|
// Create context, potentially inheriting from callContext
|
|
39
|
-
const requestContext = requestContextService.createRequestContext({
|
|
39
|
+
const requestContext = requestContextService.createRequestContext({
|
|
40
|
+
operation: toolOperation,
|
|
41
|
+
parentContext: callContext,
|
|
42
|
+
});
|
|
40
43
|
// Get session ID
|
|
41
44
|
const sessionId = _getSessionId(requestContext);
|
|
42
45
|
// Define the session-specific getter function
|
|
@@ -56,10 +59,10 @@ export const registerGitStatusTool = async (server) => {
|
|
|
56
59
|
const statusResult = await getGitStatus(validatedArgs, logicContext);
|
|
57
60
|
// Format the successful result as a JSON string within TextContent
|
|
58
61
|
const resultContent = {
|
|
59
|
-
type:
|
|
62
|
+
type: "text",
|
|
60
63
|
// Stringify the JSON object for the response content
|
|
61
64
|
text: JSON.stringify(statusResult, null, 2), // Pretty-print JSON
|
|
62
|
-
contentType:
|
|
65
|
+
contentType: "application/json", // Specify content type
|
|
63
66
|
};
|
|
64
67
|
logger.info(`Tool ${TOOL_NAME} executed successfully, returning JSON`, logicContext);
|
|
65
68
|
return { content: [resultContent] }; // isError defaults to false
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
* @fileoverview Barrel file for the git_tag tool.
|
|
3
3
|
* Exports the registration function and state accessor initialization function.
|
|
4
4
|
*/
|
|
5
|
-
export { registerGitTagTool, initializeGitTagStateAccessors } from
|
|
5
|
+
export { registerGitTagTool, initializeGitTagStateAccessors, } from "./registration.js";
|
|
6
6
|
// Export types if needed elsewhere, e.g.:
|
|
7
7
|
// export type { GitTagInput, GitTagResult } from './logic.js';
|
|
@@ -1,28 +1,50 @@
|
|
|
1
|
-
import { exec } from
|
|
2
|
-
import { promisify } from
|
|
3
|
-
import { z } from
|
|
4
|
-
import { BaseErrorCode, McpError } from
|
|
5
|
-
import { logger, sanitization } from
|
|
1
|
+
import { exec } from "child_process";
|
|
2
|
+
import { promisify } from "util";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; // Direct import for types-global
|
|
5
|
+
import { logger, sanitization } from "../../../utils/index.js"; // logger (./utils/internal/logger.js), RequestContext (./utils/internal/requestContext.js), sanitization (./utils/security/sanitization.js)
|
|
6
6
|
const execAsync = promisify(exec);
|
|
7
7
|
// Define the base input schema for the git_tag tool using Zod
|
|
8
8
|
// We export this separately to access its .shape for registration
|
|
9
9
|
export const GitTagBaseSchema = z.object({
|
|
10
|
-
path: z
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
path: z
|
|
11
|
+
.string()
|
|
12
|
+
.min(1)
|
|
13
|
+
.optional()
|
|
14
|
+
.default(".")
|
|
15
|
+
.describe("Path to the local Git repository. Defaults to the directory set via `git_set_working_dir` for the session; set 'git_set_working_dir' if not set."),
|
|
16
|
+
mode: z
|
|
17
|
+
.enum(["list", "create", "delete"])
|
|
18
|
+
.describe("The tag operation to perform: 'list' (show all tags), 'create' (add a new tag), 'delete' (remove a local tag)."),
|
|
19
|
+
tagName: z
|
|
20
|
+
.string()
|
|
21
|
+
.min(1)
|
|
22
|
+
.optional()
|
|
23
|
+
.describe("The name for the tag. Required for 'create' and 'delete' modes. e.g., 'v2.3.0'."),
|
|
24
|
+
message: z
|
|
25
|
+
.string()
|
|
26
|
+
.optional()
|
|
27
|
+
.describe("The annotation message for the tag. Required and used only when 'mode' is 'create' and 'annotate' is true."),
|
|
28
|
+
commitRef: z
|
|
29
|
+
.string()
|
|
30
|
+
.optional()
|
|
31
|
+
.describe("The commit hash, branch name, or other reference to tag. Used only in 'create' mode. Defaults to the current HEAD if omitted."),
|
|
32
|
+
annotate: z
|
|
33
|
+
.boolean()
|
|
34
|
+
.default(false)
|
|
35
|
+
.describe("If true, creates an annotated tag (-a flag) instead of a lightweight tag. Requires 'message' to be provided. Used only in 'create' mode."),
|
|
16
36
|
// force: z.boolean().default(false).describe("Force tag creation/update (-f flag). Use with caution as it can overwrite existing tags."), // Consider adding later
|
|
17
37
|
});
|
|
18
38
|
// Apply refinements for conditional validation and export the final schema
|
|
19
|
-
export const GitTagInputSchema = GitTagBaseSchema.refine(data => !(data.mode ===
|
|
39
|
+
export const GitTagInputSchema = GitTagBaseSchema.refine((data) => !(data.mode === "create" && data.annotate && !data.message), {
|
|
20
40
|
message: "An annotation 'message' is required when creating an annotated tag (annotate=true).",
|
|
21
41
|
path: ["message"], // Point Zod error to the message field
|
|
22
|
-
})
|
|
42
|
+
})
|
|
43
|
+
.refine((data) => !(data.mode === "create" && !data.tagName), {
|
|
23
44
|
message: "A 'tagName' is required for 'create' mode.",
|
|
24
45
|
path: ["tagName"], // Point Zod error to the tagName field
|
|
25
|
-
})
|
|
46
|
+
})
|
|
47
|
+
.refine((data) => !(data.mode === "delete" && !data.tagName), {
|
|
26
48
|
message: "A 'tagName' is required for 'delete' mode.",
|
|
27
49
|
path: ["tagName"], // Point Zod error to the tagName field
|
|
28
50
|
});
|
|
@@ -41,25 +63,41 @@ export async function gitTagLogic(input, context) {
|
|
|
41
63
|
try {
|
|
42
64
|
// Resolve and sanitize the target path
|
|
43
65
|
const workingDir = context.getWorkingDirectory();
|
|
44
|
-
targetPath =
|
|
45
|
-
? input.path
|
|
46
|
-
|
|
47
|
-
if (targetPath === '.' && !workingDir) {
|
|
66
|
+
targetPath =
|
|
67
|
+
input.path && input.path !== "." ? input.path : (workingDir ?? ".");
|
|
68
|
+
if (targetPath === "." && !workingDir) {
|
|
48
69
|
logger.warning("Executing git tag in server's CWD as no path provided and no session WD set.", { ...context, operation });
|
|
49
70
|
targetPath = process.cwd();
|
|
50
71
|
}
|
|
51
|
-
else if (targetPath ===
|
|
72
|
+
else if (targetPath === "." && workingDir) {
|
|
52
73
|
targetPath = workingDir;
|
|
53
|
-
logger.debug(`Using session working directory: ${targetPath}`, {
|
|
74
|
+
logger.debug(`Using session working directory: ${targetPath}`, {
|
|
75
|
+
...context,
|
|
76
|
+
operation,
|
|
77
|
+
sessionId: context.sessionId,
|
|
78
|
+
});
|
|
54
79
|
}
|
|
55
80
|
else {
|
|
56
|
-
logger.debug(`Using provided path: ${targetPath}`, {
|
|
81
|
+
logger.debug(`Using provided path: ${targetPath}`, {
|
|
82
|
+
...context,
|
|
83
|
+
operation,
|
|
84
|
+
});
|
|
57
85
|
}
|
|
58
|
-
targetPath = sanitization.sanitizePath(targetPath, {
|
|
59
|
-
|
|
86
|
+
targetPath = sanitization.sanitizePath(targetPath, {
|
|
87
|
+
allowAbsolute: true,
|
|
88
|
+
}).sanitizedPath;
|
|
89
|
+
logger.debug("Sanitized path", {
|
|
90
|
+
...context,
|
|
91
|
+
operation,
|
|
92
|
+
sanitizedPath: targetPath,
|
|
93
|
+
});
|
|
60
94
|
}
|
|
61
95
|
catch (error) {
|
|
62
|
-
logger.error(
|
|
96
|
+
logger.error("Path resolution or sanitization failed", {
|
|
97
|
+
...context,
|
|
98
|
+
operation,
|
|
99
|
+
error,
|
|
100
|
+
});
|
|
63
101
|
if (error instanceof McpError)
|
|
64
102
|
throw error;
|
|
65
103
|
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
|
|
@@ -76,14 +114,20 @@ export async function gitTagLogic(input, context) {
|
|
|
76
114
|
let command;
|
|
77
115
|
let result;
|
|
78
116
|
switch (input.mode) {
|
|
79
|
-
case
|
|
117
|
+
case "list":
|
|
80
118
|
command = `git -C "${targetPath}" tag --list`;
|
|
81
|
-
logger.debug(`Executing command: ${command}`, {
|
|
119
|
+
logger.debug(`Executing command: ${command}`, {
|
|
120
|
+
...context,
|
|
121
|
+
operation,
|
|
122
|
+
});
|
|
82
123
|
const { stdout: listStdout } = await execAsync(command);
|
|
83
|
-
const tags = listStdout
|
|
84
|
-
|
|
124
|
+
const tags = listStdout
|
|
125
|
+
.trim()
|
|
126
|
+
.split("\n")
|
|
127
|
+
.filter((tag) => tag); // Filter out empty lines
|
|
128
|
+
result = { success: true, mode: "list", tags };
|
|
85
129
|
break;
|
|
86
|
-
case
|
|
130
|
+
case "create":
|
|
87
131
|
// TagName is validated by Zod refine
|
|
88
132
|
const tagNameCreate = input.tagName;
|
|
89
133
|
command = `git -C "${targetPath}" tag`;
|
|
@@ -95,47 +139,76 @@ export async function gitTagLogic(input, context) {
|
|
|
95
139
|
if (input.commitRef) {
|
|
96
140
|
command += ` "${input.commitRef}"`;
|
|
97
141
|
}
|
|
98
|
-
logger.debug(`Executing command: ${command}`, {
|
|
142
|
+
logger.debug(`Executing command: ${command}`, {
|
|
143
|
+
...context,
|
|
144
|
+
operation,
|
|
145
|
+
});
|
|
99
146
|
await execAsync(command);
|
|
100
|
-
result = {
|
|
147
|
+
result = {
|
|
148
|
+
success: true,
|
|
149
|
+
mode: "create",
|
|
150
|
+
message: `Tag '${tagNameCreate}' created successfully.`,
|
|
151
|
+
tagName: tagNameCreate,
|
|
152
|
+
};
|
|
101
153
|
break;
|
|
102
|
-
case
|
|
154
|
+
case "delete":
|
|
103
155
|
// TagName is validated by Zod refine
|
|
104
156
|
const tagNameDelete = input.tagName;
|
|
105
157
|
command = `git -C "${targetPath}" tag -d "${tagNameDelete}"`;
|
|
106
|
-
logger.debug(`Executing command: ${command}`, {
|
|
158
|
+
logger.debug(`Executing command: ${command}`, {
|
|
159
|
+
...context,
|
|
160
|
+
operation,
|
|
161
|
+
});
|
|
107
162
|
await execAsync(command);
|
|
108
|
-
result = {
|
|
163
|
+
result = {
|
|
164
|
+
success: true,
|
|
165
|
+
mode: "delete",
|
|
166
|
+
message: `Tag '${tagNameDelete}' deleted successfully.`,
|
|
167
|
+
tagName: tagNameDelete,
|
|
168
|
+
};
|
|
109
169
|
break;
|
|
110
170
|
default:
|
|
111
171
|
// Should not happen due to Zod validation
|
|
112
172
|
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid mode: ${input.mode}`, { context, operation });
|
|
113
173
|
}
|
|
114
|
-
logger.info(
|
|
174
|
+
logger.info(`git tag ${input.mode} executed successfully`, {
|
|
175
|
+
...context,
|
|
176
|
+
operation,
|
|
177
|
+
path: targetPath,
|
|
178
|
+
result,
|
|
179
|
+
});
|
|
115
180
|
return result;
|
|
116
181
|
}
|
|
117
182
|
catch (error) {
|
|
118
|
-
const errorMessage = error.stderr || error.message ||
|
|
119
|
-
logger.error(`Failed to execute git tag command`, {
|
|
183
|
+
const errorMessage = error.stderr || error.message || "";
|
|
184
|
+
logger.error(`Failed to execute git tag command`, {
|
|
185
|
+
...context,
|
|
186
|
+
operation,
|
|
187
|
+
path: targetPath,
|
|
188
|
+
error: errorMessage,
|
|
189
|
+
stderr: error.stderr,
|
|
190
|
+
stdout: error.stdout,
|
|
191
|
+
});
|
|
120
192
|
// Specific error handling
|
|
121
|
-
if (errorMessage.toLowerCase().includes(
|
|
193
|
+
if (errorMessage.toLowerCase().includes("not a git repository")) {
|
|
122
194
|
throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`, { context, operation, originalError: error });
|
|
123
195
|
}
|
|
124
|
-
if (input.mode ===
|
|
125
|
-
|
|
196
|
+
if (input.mode === "create" &&
|
|
197
|
+
errorMessage.toLowerCase().includes("already exists")) {
|
|
198
|
+
throw new McpError(BaseErrorCode.CONFLICT, `Failed to create tag: Tag '${input.tagName}' already exists. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
126
199
|
}
|
|
127
|
-
if (input.mode ===
|
|
128
|
-
|
|
200
|
+
if (input.mode === "delete" &&
|
|
201
|
+
errorMessage.toLowerCase().includes("not found")) {
|
|
202
|
+
throw new McpError(BaseErrorCode.NOT_FOUND, `Failed to delete tag: Tag '${input.tagName}' not found. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
129
203
|
}
|
|
130
|
-
if (input.mode ===
|
|
131
|
-
|
|
204
|
+
if (input.mode === "create" &&
|
|
205
|
+
input.commitRef &&
|
|
206
|
+
errorMessage
|
|
207
|
+
.toLowerCase()
|
|
208
|
+
.includes("unknown revision or path not in the working tree")) {
|
|
209
|
+
throw new McpError(BaseErrorCode.NOT_FOUND, `Failed to create tag: Commit reference '${input.commitRef}' not found. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
132
210
|
}
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
success: false,
|
|
136
|
-
mode: input.mode,
|
|
137
|
-
message: `Git tag ${input.mode} failed for path: ${targetPath}.`,
|
|
138
|
-
error: errorMessage
|
|
139
|
-
};
|
|
211
|
+
// Throw a generic McpError for other failures
|
|
212
|
+
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Git tag ${input.mode} failed for path: ${targetPath}. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
140
213
|
}
|
|
141
214
|
}
|