@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.
Files changed (97) hide show
  1. package/README.md +8 -11
  2. package/dist/config/index.js +7 -7
  3. package/dist/index.js +35 -21
  4. package/dist/mcp-server/server.js +72 -56
  5. package/dist/mcp-server/tools/gitAdd/index.js +1 -1
  6. package/dist/mcp-server/tools/gitAdd/logic.js +88 -39
  7. package/dist/mcp-server/tools/gitAdd/registration.js +17 -14
  8. package/dist/mcp-server/tools/gitBranch/index.js +1 -1
  9. package/dist/mcp-server/tools/gitBranch/logic.js +213 -85
  10. package/dist/mcp-server/tools/gitBranch/registration.js +16 -13
  11. package/dist/mcp-server/tools/gitCheckout/index.js +1 -1
  12. package/dist/mcp-server/tools/gitCheckout/logic.js +85 -145
  13. package/dist/mcp-server/tools/gitCheckout/registration.js +16 -14
  14. package/dist/mcp-server/tools/gitCherryPick/index.js +1 -1
  15. package/dist/mcp-server/tools/gitCherryPick/logic.js +100 -41
  16. package/dist/mcp-server/tools/gitCherryPick/registration.js +21 -14
  17. package/dist/mcp-server/tools/gitClean/index.js +1 -1
  18. package/dist/mcp-server/tools/gitClean/logic.js +93 -41
  19. package/dist/mcp-server/tools/gitClean/registration.js +19 -16
  20. package/dist/mcp-server/tools/gitClearWorkingDir/index.js +1 -1
  21. package/dist/mcp-server/tools/gitClearWorkingDir/logic.js +14 -11
  22. package/dist/mcp-server/tools/gitClearWorkingDir/registration.js +19 -13
  23. package/dist/mcp-server/tools/gitClone/index.js +1 -1
  24. package/dist/mcp-server/tools/gitClone/logic.js +89 -30
  25. package/dist/mcp-server/tools/gitClone/registration.js +15 -12
  26. package/dist/mcp-server/tools/gitCommit/index.js +1 -1
  27. package/dist/mcp-server/tools/gitCommit/logic.js +198 -76
  28. package/dist/mcp-server/tools/gitCommit/registration.js +23 -20
  29. package/dist/mcp-server/tools/gitDiff/index.js +1 -1
  30. package/dist/mcp-server/tools/gitDiff/logic.js +124 -44
  31. package/dist/mcp-server/tools/gitDiff/registration.js +16 -14
  32. package/dist/mcp-server/tools/gitFetch/index.js +1 -1
  33. package/dist/mcp-server/tools/gitFetch/logic.js +78 -49
  34. package/dist/mcp-server/tools/gitFetch/registration.js +16 -14
  35. package/dist/mcp-server/tools/gitInit/index.js +1 -1
  36. package/dist/mcp-server/tools/gitInit/logic.js +88 -34
  37. package/dist/mcp-server/tools/gitInit/registration.js +32 -18
  38. package/dist/mcp-server/tools/gitLog/index.js +1 -1
  39. package/dist/mcp-server/tools/gitLog/logic.js +133 -47
  40. package/dist/mcp-server/tools/gitLog/registration.js +16 -14
  41. package/dist/mcp-server/tools/gitMerge/index.js +1 -1
  42. package/dist/mcp-server/tools/gitMerge/logic.js +102 -61
  43. package/dist/mcp-server/tools/gitMerge/registration.js +17 -14
  44. package/dist/mcp-server/tools/gitPull/index.js +1 -1
  45. package/dist/mcp-server/tools/gitPull/logic.js +90 -69
  46. package/dist/mcp-server/tools/gitPull/registration.js +16 -14
  47. package/dist/mcp-server/tools/gitPush/index.js +1 -1
  48. package/dist/mcp-server/tools/gitPush/logic.js +116 -100
  49. package/dist/mcp-server/tools/gitPush/registration.js +16 -14
  50. package/dist/mcp-server/tools/gitRebase/index.js +1 -1
  51. package/dist/mcp-server/tools/gitRebase/logic.js +121 -82
  52. package/dist/mcp-server/tools/gitRebase/registration.js +21 -14
  53. package/dist/mcp-server/tools/gitRemote/index.js +1 -1
  54. package/dist/mcp-server/tools/gitRemote/logic.js +108 -52
  55. package/dist/mcp-server/tools/gitRemote/registration.js +14 -11
  56. package/dist/mcp-server/tools/gitReset/index.js +1 -1
  57. package/dist/mcp-server/tools/gitReset/logic.js +65 -37
  58. package/dist/mcp-server/tools/gitReset/registration.js +14 -12
  59. package/dist/mcp-server/tools/gitSetWorkingDir/index.js +1 -1
  60. package/dist/mcp-server/tools/gitSetWorkingDir/logic.js +74 -34
  61. package/dist/mcp-server/tools/gitSetWorkingDir/registration.js +18 -11
  62. package/dist/mcp-server/tools/gitShow/index.js +1 -1
  63. package/dist/mcp-server/tools/gitShow/logic.js +78 -35
  64. package/dist/mcp-server/tools/gitShow/registration.js +17 -12
  65. package/dist/mcp-server/tools/gitStash/index.js +1 -1
  66. package/dist/mcp-server/tools/gitStash/logic.js +143 -58
  67. package/dist/mcp-server/tools/gitStash/registration.js +19 -12
  68. package/dist/mcp-server/tools/gitStatus/index.js +1 -1
  69. package/dist/mcp-server/tools/gitStatus/logic.js +100 -58
  70. package/dist/mcp-server/tools/gitStatus/registration.js +15 -12
  71. package/dist/mcp-server/tools/gitTag/index.js +1 -1
  72. package/dist/mcp-server/tools/gitTag/logic.js +124 -51
  73. package/dist/mcp-server/tools/gitTag/registration.js +14 -11
  74. package/dist/mcp-server/tools/gitWorktree/index.js +1 -1
  75. package/dist/mcp-server/tools/gitWorktree/logic.js +204 -95
  76. package/dist/mcp-server/tools/gitWorktree/registration.js +14 -11
  77. package/dist/mcp-server/tools/gitWrapupInstructions/index.js +1 -1
  78. package/dist/mcp-server/tools/gitWrapupInstructions/logic.js +23 -11
  79. package/dist/mcp-server/tools/gitWrapupInstructions/registration.js +14 -12
  80. package/dist/mcp-server/transports/httpTransport.js +187 -79
  81. package/dist/mcp-server/transports/stdioTransport.js +14 -8
  82. package/dist/types-global/errors.js +9 -4
  83. package/dist/utils/index.js +4 -4
  84. package/dist/utils/internal/errorHandler.js +62 -40
  85. package/dist/utils/internal/index.js +3 -3
  86. package/dist/utils/internal/logger.js +97 -54
  87. package/dist/utils/internal/requestContext.js +7 -5
  88. package/dist/utils/metrics/index.js +1 -1
  89. package/dist/utils/metrics/tokenCounter.js +18 -14
  90. package/dist/utils/parsing/dateParser.js +5 -5
  91. package/dist/utils/parsing/index.js +2 -2
  92. package/dist/utils/parsing/jsonParser.js +20 -11
  93. package/dist/utils/security/idGenerator.js +8 -10
  94. package/dist/utils/security/index.js +3 -3
  95. package/dist/utils/security/rateLimiter.js +16 -14
  96. package/dist/utils/security/sanitization.js +139 -82
  97. package/package.json +45 -23
@@ -1,12 +1,17 @@
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)
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.string().min(1).optional().default('.').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."),
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('\n');
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 = 'HEAD (detached)';
48
+ result.current_branch = "HEAD (detached)";
44
49
  }
45
50
  else {
46
- logger.warning('Could not parse branch information from line:', { branchLine });
47
- result.current_branch = '(unknown)';
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 === 'U' || unstagedStatusChar === 'U' ||
67
- (stagedStatusChar === 'D' && unstagedStatusChar === 'D') ||
68
- (stagedStatusChar === 'A' && unstagedStatusChar === 'A')) {
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 !== ' ' && stagedStatusChar !== '?') {
81
+ if (stagedStatusChar !== " " && stagedStatusChar !== "?") {
74
82
  let statusDesc = undefined;
75
83
  switch (stagedStatusChar) {
76
- case 'M':
77
- statusDesc = 'Modified';
84
+ case "M":
85
+ statusDesc = "Modified";
78
86
  break;
79
- case 'A':
80
- statusDesc = 'Added';
87
+ case "A":
88
+ statusDesc = "Added";
81
89
  break;
82
- case 'D':
83
- statusDesc = 'Deleted';
90
+ case "D":
91
+ statusDesc = "Deleted";
84
92
  break;
85
- case 'R':
86
- statusDesc = 'Renamed';
93
+ case "R":
94
+ statusDesc = "Renamed";
87
95
  break;
88
- case 'C':
89
- statusDesc = 'Copied';
96
+ case "C":
97
+ statusDesc = "Copied";
90
98
  break;
91
- case 'T':
92
- statusDesc = 'TypeChanged';
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 !== ' ' && unstagedStatusChar !== '?') {
111
+ if (unstagedStatusChar !== " " && unstagedStatusChar !== "?") {
104
112
  let statusDesc = undefined;
105
113
  switch (unstagedStatusChar) {
106
- case 'M':
107
- statusDesc = 'Modified';
114
+ case "M":
115
+ statusDesc = "Modified";
108
116
  break;
109
- case 'D':
110
- statusDesc = 'Deleted';
117
+ case "D":
118
+ statusDesc = "Deleted";
111
119
  break;
112
- case 'T':
113
- statusDesc = 'TypeChanged';
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 = Object.keys(result.staged_changes).length === 0 &&
127
- Object.keys(result.unstaged_changes).length === 0 &&
128
- result.untracked_files.length === 0 &&
129
- result.conflicted_files.length === 0;
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 // Add getter to 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}`, { ...context, operation });
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}`, { ...context, operation, sessionId: context.sessionId });
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, { allowAbsolute: true });
163
- logger.debug('Sanitized path', { ...context, operation, sanitizedPathInfo });
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('Path resolution or sanitization failed', { ...context, operation, 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.info(`${operation} command executed, parsing output...`, { ...context, operation, path: targetPath });
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 !== 'HEAD') {
222
+ if (currentBranchName && currentBranchName !== "HEAD") {
194
223
  structuredResult.current_branch = currentBranchName;
195
224
  }
196
- else if (currentBranchName === 'HEAD' && !structuredResult.current_branch) {
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 = 'HEAD (detached)';
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('Could not determine branch via rev-parse, likely no commits yet.', { ...context, operation, branchError });
233
+ logger.debug("Could not determine branch via rev-parse, likely no commits yet.", { ...context, operation, branchError });
204
234
  }
205
235
  }
206
- logger.info(`${operation} parsed successfully`, { ...context, operation, path: targetPath });
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`, { ...context, operation, path: targetPath, error: error.message, stderr: error.stderr });
211
- const errorMessage = error.stderr || error.message || '';
212
- if (errorMessage.toLowerCase().includes('not a git repository')) {
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 '../../../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)
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 './logic.js';
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('State accessors initialized for git_status tool registration.');
16
+ logger.info("State accessors initialized for git_status tool registration.");
17
17
  }
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.';
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('State accessors for git_status must be initialized before registration.');
30
+ throw new Error("State accessors for git_status must be initialized before registration.");
31
31
  }
32
- const operation = 'registerGitStatusTool';
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 = 'tool:git_status';
37
+ const toolOperation = "tool:git_status";
38
38
  // Create context, potentially inheriting from callContext
39
- const requestContext = requestContextService.createRequestContext({ operation: toolOperation, parentContext: callContext });
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: 'text',
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: 'application/json', // Specify content type
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 './registration.js';
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 '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)
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.string().min(1).optional().default('.').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."),
11
- mode: z.enum(['list', 'create', 'delete']).describe("The tag operation to perform: 'list' (show all tags), 'create' (add a new tag), 'delete' (remove a local tag)."),
12
- tagName: z.string().min(1).optional().describe("The name for the tag. Required for 'create' and 'delete' modes. e.g., 'v2.3.0'."),
13
- message: z.string().optional().describe("The annotation message for the tag. Required and used only when 'mode' is 'create' and 'annotate' is true."),
14
- commitRef: z.string().optional().describe("The commit hash, branch name, or other reference to tag. Used only in 'create' mode. Defaults to the current HEAD if omitted."),
15
- annotate: z.boolean().default(false).describe("If true, creates an annotated tag (-a flag) instead of a lightweight tag. Requires 'message' to be provided. Used only in 'create' mode."),
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 === 'create' && data.annotate && !data.message), {
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
- }).refine(data => !(data.mode === 'create' && !data.tagName), {
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
- }).refine(data => !(data.mode === 'delete' && !data.tagName), {
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 = (input.path && input.path !== '.')
45
- ? input.path
46
- : workingDir ?? '.';
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 === '.' && workingDir) {
72
+ else if (targetPath === "." && workingDir) {
52
73
  targetPath = workingDir;
53
- logger.debug(`Using session working directory: ${targetPath}`, { ...context, operation, sessionId: context.sessionId });
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}`, { ...context, operation });
81
+ logger.debug(`Using provided path: ${targetPath}`, {
82
+ ...context,
83
+ operation,
84
+ });
57
85
  }
58
- targetPath = sanitization.sanitizePath(targetPath, { allowAbsolute: true }).sanitizedPath;
59
- logger.debug('Sanitized path', { ...context, operation, sanitizedPath: targetPath });
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('Path resolution or sanitization failed', { ...context, operation, 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 'list':
117
+ case "list":
80
118
  command = `git -C "${targetPath}" tag --list`;
81
- logger.debug(`Executing command: ${command}`, { ...context, operation });
119
+ logger.debug(`Executing command: ${command}`, {
120
+ ...context,
121
+ operation,
122
+ });
82
123
  const { stdout: listStdout } = await execAsync(command);
83
- const tags = listStdout.trim().split('\n').filter(tag => tag); // Filter out empty lines
84
- result = { success: true, mode: 'list', tags };
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 'create':
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}`, { ...context, operation });
142
+ logger.debug(`Executing command: ${command}`, {
143
+ ...context,
144
+ operation,
145
+ });
99
146
  await execAsync(command);
100
- result = { success: true, mode: 'create', message: `Tag '${tagNameCreate}' created successfully.`, tagName: tagNameCreate };
147
+ result = {
148
+ success: true,
149
+ mode: "create",
150
+ message: `Tag '${tagNameCreate}' created successfully.`,
151
+ tagName: tagNameCreate,
152
+ };
101
153
  break;
102
- case 'delete':
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}`, { ...context, operation });
158
+ logger.debug(`Executing command: ${command}`, {
159
+ ...context,
160
+ operation,
161
+ });
107
162
  await execAsync(command);
108
- result = { success: true, mode: 'delete', message: `Tag '${tagNameDelete}' deleted successfully.`, tagName: tagNameDelete };
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(`${operation} executed successfully`, { ...context, operation, path: targetPath });
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`, { ...context, operation, path: targetPath, error: errorMessage, stderr: error.stderr, stdout: error.stdout });
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('not a git repository')) {
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 === 'create' && errorMessage.toLowerCase().includes('already exists')) {
125
- return { success: false, mode: 'create', message: `Failed to create tag: Tag '${input.tagName}' already exists.`, error: errorMessage };
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 === 'delete' && errorMessage.toLowerCase().includes('not found')) {
128
- return { success: false, mode: 'delete', message: `Failed to delete tag: Tag '${input.tagName}' not found.`, error: errorMessage };
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 === 'create' && input.commitRef && errorMessage.toLowerCase().includes('unknown revision or path not in the working tree')) {
131
- return { success: false, mode: 'create', message: `Failed to create tag: Commit reference '${input.commitRef}' not found.`, error: errorMessage };
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
- // Return structured failure for other git errors
134
- return {
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
  }