@cyanheads/git-mcp-server 1.2.4 → 2.0.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 (105) hide show
  1. package/README.md +172 -285
  2. package/dist/config/index.js +69 -0
  3. package/dist/index.js +135 -0
  4. package/dist/mcp-server/server.js +572 -0
  5. package/dist/mcp-server/tools/gitAdd/index.js +7 -0
  6. package/dist/mcp-server/tools/gitAdd/logic.js +118 -0
  7. package/dist/mcp-server/tools/gitAdd/registration.js +73 -0
  8. package/dist/mcp-server/tools/gitBranch/index.js +7 -0
  9. package/dist/mcp-server/tools/gitBranch/logic.js +180 -0
  10. package/dist/mcp-server/tools/gitBranch/registration.js +72 -0
  11. package/dist/mcp-server/tools/gitCheckout/index.js +6 -0
  12. package/dist/mcp-server/tools/gitCheckout/logic.js +165 -0
  13. package/dist/mcp-server/tools/gitCheckout/registration.js +78 -0
  14. package/dist/mcp-server/tools/gitCherryPick/index.js +7 -0
  15. package/dist/mcp-server/tools/gitCherryPick/logic.js +115 -0
  16. package/dist/mcp-server/tools/gitCherryPick/registration.js +69 -0
  17. package/dist/mcp-server/tools/gitClean/index.js +7 -0
  18. package/dist/mcp-server/tools/gitClean/logic.js +110 -0
  19. package/dist/mcp-server/tools/gitClean/registration.js +98 -0
  20. package/dist/mcp-server/tools/gitClearWorkingDir/index.js +7 -0
  21. package/dist/mcp-server/tools/gitClearWorkingDir/logic.js +35 -0
  22. package/dist/mcp-server/tools/gitClearWorkingDir/registration.js +73 -0
  23. package/dist/mcp-server/tools/gitClone/index.js +7 -0
  24. package/dist/mcp-server/tools/gitClone/logic.js +136 -0
  25. package/dist/mcp-server/tools/gitClone/registration.js +44 -0
  26. package/dist/mcp-server/tools/gitCommit/index.js +7 -0
  27. package/dist/mcp-server/tools/gitCommit/logic.js +129 -0
  28. package/dist/mcp-server/tools/gitCommit/registration.js +100 -0
  29. package/dist/mcp-server/tools/gitDiff/index.js +6 -0
  30. package/dist/mcp-server/tools/gitDiff/logic.js +114 -0
  31. package/dist/mcp-server/tools/gitDiff/registration.js +74 -0
  32. package/dist/mcp-server/tools/gitFetch/index.js +6 -0
  33. package/dist/mcp-server/tools/gitFetch/logic.js +116 -0
  34. package/dist/mcp-server/tools/gitFetch/registration.js +71 -0
  35. package/dist/mcp-server/tools/gitInit/index.js +7 -0
  36. package/dist/mcp-server/tools/gitInit/logic.js +117 -0
  37. package/dist/mcp-server/tools/gitInit/registration.js +44 -0
  38. package/dist/mcp-server/tools/gitLog/index.js +6 -0
  39. package/dist/mcp-server/tools/gitLog/logic.js +148 -0
  40. package/dist/mcp-server/tools/gitLog/registration.js +71 -0
  41. package/dist/mcp-server/tools/gitMerge/index.js +7 -0
  42. package/dist/mcp-server/tools/gitMerge/logic.js +160 -0
  43. package/dist/mcp-server/tools/gitMerge/registration.js +77 -0
  44. package/dist/mcp-server/tools/gitPull/index.js +6 -0
  45. package/dist/mcp-server/tools/gitPull/logic.js +144 -0
  46. package/dist/mcp-server/tools/gitPull/registration.js +81 -0
  47. package/dist/mcp-server/tools/gitPush/index.js +6 -0
  48. package/dist/mcp-server/tools/gitPush/logic.js +188 -0
  49. package/dist/mcp-server/tools/gitPush/registration.js +81 -0
  50. package/dist/mcp-server/tools/gitRebase/index.js +7 -0
  51. package/dist/mcp-server/tools/gitRebase/logic.js +171 -0
  52. package/dist/mcp-server/tools/gitRebase/registration.js +72 -0
  53. package/dist/mcp-server/tools/gitRemote/index.js +7 -0
  54. package/dist/mcp-server/tools/gitRemote/logic.js +158 -0
  55. package/dist/mcp-server/tools/gitRemote/registration.js +76 -0
  56. package/dist/mcp-server/tools/gitReset/index.js +6 -0
  57. package/dist/mcp-server/tools/gitReset/logic.js +116 -0
  58. package/dist/mcp-server/tools/gitReset/registration.js +71 -0
  59. package/dist/mcp-server/tools/gitSetWorkingDir/index.js +7 -0
  60. package/dist/mcp-server/tools/gitSetWorkingDir/logic.js +91 -0
  61. package/dist/mcp-server/tools/gitSetWorkingDir/registration.js +78 -0
  62. package/dist/mcp-server/tools/gitShow/index.js +7 -0
  63. package/dist/mcp-server/tools/gitShow/logic.js +99 -0
  64. package/dist/mcp-server/tools/gitShow/registration.js +83 -0
  65. package/dist/mcp-server/tools/gitStash/index.js +7 -0
  66. package/dist/mcp-server/tools/gitStash/logic.js +161 -0
  67. package/dist/mcp-server/tools/gitStash/registration.js +84 -0
  68. package/dist/mcp-server/tools/gitStatus/index.js +7 -0
  69. package/dist/mcp-server/tools/gitStatus/logic.js +215 -0
  70. package/dist/mcp-server/tools/gitStatus/registration.js +77 -0
  71. package/dist/mcp-server/tools/gitTag/index.js +7 -0
  72. package/dist/mcp-server/tools/gitTag/logic.js +142 -0
  73. package/dist/mcp-server/tools/gitTag/registration.js +84 -0
  74. package/dist/types-global/errors.js +68 -0
  75. package/dist/types-global/mcp.js +59 -0
  76. package/dist/types-global/tool.js +1 -0
  77. package/dist/utils/errorHandler.js +237 -0
  78. package/dist/utils/idGenerator.js +148 -0
  79. package/dist/utils/index.js +11 -0
  80. package/dist/utils/jsonParser.js +78 -0
  81. package/dist/utils/logger.js +266 -0
  82. package/dist/utils/rateLimiter.js +177 -0
  83. package/dist/utils/requestContext.js +49 -0
  84. package/dist/utils/sanitization.js +371 -0
  85. package/dist/utils/tokenCounter.js +124 -0
  86. package/package.json +62 -17
  87. package/build/index.js +0 -54
  88. package/build/resources/descriptors.js +0 -77
  89. package/build/resources/diff.js +0 -241
  90. package/build/resources/file.js +0 -222
  91. package/build/resources/history.js +0 -242
  92. package/build/resources/index.js +0 -99
  93. package/build/resources/repository.js +0 -286
  94. package/build/server.js +0 -120
  95. package/build/services/error-service.js +0 -73
  96. package/build/services/git-service.js +0 -965
  97. package/build/tools/advanced.js +0 -526
  98. package/build/tools/branch.js +0 -296
  99. package/build/tools/index.js +0 -29
  100. package/build/tools/remote.js +0 -279
  101. package/build/tools/repository.js +0 -170
  102. package/build/tools/workdir.js +0 -445
  103. package/build/types/git.js +0 -7
  104. package/build/utils/global-settings.js +0 -64
  105. package/build/utils/validation.js +0 -108
@@ -0,0 +1,215 @@
1
+ import { z } from 'zod';
2
+ import { promisify } from 'util';
3
+ import { exec } from 'child_process';
4
+ import { logger } from '../../../utils/logger.js';
5
+ import { McpError, BaseErrorCode } from '../../../types-global/errors.js';
6
+ import { sanitization } from '../../../utils/sanitization.js';
7
+ const execAsync = promisify(exec);
8
+ // Define the input schema for the git_status tool using Zod
9
+ export const GitStatusInputSchema = z.object({
10
+ path: z.string().min(1).optional().default('.').describe("Path to the Git repository (defaults to '.' which uses the session's working directory if set)"),
11
+ });
12
+ /**
13
+ * Parses the output of 'git status --porcelain=v1 -b'.
14
+ * See: https://git-scm.com/docs/git-status#_porcelain_format_version_1
15
+ *
16
+ * @param {string} porcelainOutput - The raw output from the git command.
17
+ * @returns {GitStatusResult} - Structured status information.
18
+ */
19
+ function parseGitStatusPorcelainV1(porcelainOutput) {
20
+ const lines = porcelainOutput.trim().split('\n');
21
+ const result = {
22
+ currentBranch: null,
23
+ staged: [],
24
+ modified: [],
25
+ untracked: [],
26
+ conflicted: [],
27
+ isClean: true, // Assume clean initially
28
+ };
29
+ if (lines.length === 0 || (lines.length === 1 && lines[0] === '')) {
30
+ // If output is empty, it might mean no branch yet or truly clean
31
+ // We'll refine branch detection below if possible
32
+ return result;
33
+ }
34
+ // First line often contains branch info (e.g., ## master...origin/master)
35
+ if (lines[0].startsWith('## ')) {
36
+ const branchLine = lines.shift(); // Remove and process the branch line
37
+ // Try matching standard branch format first (e.g., ## master...origin/master [ahead 1])
38
+ // Make regex more specific: look for '...' or '[' after branch name, or end of line for simple branch name
39
+ const standardBranchMatch = branchLine.match(/^## ([^ ]+?)(?:\.\.\.| \[.*\]|$)/);
40
+ // Try matching the 'No commits yet' format (e.g., ## No commits yet on master)
41
+ const noCommitsMatch = branchLine.match(/^## No commits yet on (.+)/);
42
+ // Try matching detached HEAD format (e.g., ## HEAD (no branch))
43
+ const detachedMatch = branchLine.match(/^## HEAD \(no branch\)/);
44
+ if (standardBranchMatch) {
45
+ result.currentBranch = standardBranchMatch[1];
46
+ // TODO: Optionally parse ahead/behind counts if needed from the full match
47
+ }
48
+ else if (noCommitsMatch) {
49
+ // More descriptive state: Branch exists but has no commits
50
+ result.currentBranch = `${noCommitsMatch[1]} (no commits yet)`;
51
+ }
52
+ else if (detachedMatch) { // Handle detached HEAD
53
+ result.currentBranch = 'HEAD (detached)';
54
+ }
55
+ else {
56
+ // Fallback if branch line format is unexpected
57
+ logger.warning('Could not parse branch information from line:', { branchLine });
58
+ result.currentBranch = '(unknown)';
59
+ }
60
+ }
61
+ for (const line of lines) {
62
+ if (!line)
63
+ continue; // Skip empty lines if any
64
+ result.isClean = false; // Any line indicates non-clean state
65
+ const xy = line.substring(0, 2);
66
+ const file = line.substring(3); // Path starts after 'XY '
67
+ const stagedStatus = xy[0];
68
+ const unstagedStatus = xy[1];
69
+ // Handle untracked files
70
+ if (xy === '??') {
71
+ result.untracked.push(file);
72
+ continue;
73
+ }
74
+ // Handle conflicted files (complex statuses)
75
+ if (stagedStatus === 'U' || unstagedStatus === 'U' || (stagedStatus === 'A' && unstagedStatus === 'A') || (stagedStatus === 'D' && unstagedStatus === 'D')) {
76
+ result.conflicted.push(file);
77
+ // Decide how to represent conflicts (could be more granular)
78
+ if (!result.staged.some(f => f.file === file))
79
+ result.staged.push({ status: 'Conflicted', file });
80
+ if (!result.modified.some(f => f.file === file))
81
+ result.modified.push({ status: 'Conflicted', file });
82
+ continue;
83
+ }
84
+ // Handle staged changes (index status)
85
+ if (stagedStatus !== ' ' && stagedStatus !== '?') {
86
+ let statusDesc = 'Unknown Staged';
87
+ switch (stagedStatus) {
88
+ case 'M':
89
+ statusDesc = 'Modified';
90
+ break;
91
+ case 'A':
92
+ statusDesc = 'Added';
93
+ break;
94
+ case 'D':
95
+ statusDesc = 'Deleted';
96
+ break;
97
+ case 'R':
98
+ statusDesc = 'Renamed';
99
+ break; // Often includes ' -> new_name' in file path
100
+ case 'C':
101
+ statusDesc = 'Copied';
102
+ break; // Often includes ' -> new_name' in file path
103
+ case 'T':
104
+ statusDesc = 'Type Changed';
105
+ break;
106
+ }
107
+ result.staged.push({ status: statusDesc, file });
108
+ }
109
+ // Handle unstaged changes (worktree status)
110
+ if (unstagedStatus !== ' ' && unstagedStatus !== '?') {
111
+ let statusDesc = 'Unknown Unstaged';
112
+ switch (unstagedStatus) {
113
+ case 'M':
114
+ statusDesc = 'Modified';
115
+ break;
116
+ case 'D':
117
+ statusDesc = 'Deleted';
118
+ break;
119
+ case 'T':
120
+ statusDesc = 'Type Changed';
121
+ break;
122
+ // Note: 'A' (Added) in unstaged usually means untracked ('??') handled above
123
+ }
124
+ // Avoid duplicating if already marked as conflicted
125
+ if (!result.modified.some(f => f.file === file && f.status === 'Conflicted')) {
126
+ result.modified.push({ status: statusDesc, file });
127
+ }
128
+ }
129
+ }
130
+ // Final check for cleanliness
131
+ result.isClean = result.staged.length === 0 && result.modified.length === 0 && result.untracked.length === 0 && result.conflicted.length === 0;
132
+ return result;
133
+ }
134
+ /**
135
+ * Executes the 'git status --porcelain=v1 -b' command and returns structured JSON output.
136
+ *
137
+ * @param {GitStatusInput} input - The validated input object containing the repository path.
138
+ * @param {RequestContext} context - The request context for logging and error handling.
139
+ * @returns {Promise<GitStatusResult>} A promise that resolves with the structured git status.
140
+ * @throws {McpError} Throws an McpError if path resolution or validation fails, or if the git command fails.
141
+ */
142
+ export async function getGitStatus(input, context // Add getter to context
143
+ ) {
144
+ const operation = 'getGitStatus';
145
+ logger.debug(`Executing ${operation}`, { ...context, input });
146
+ let targetPath;
147
+ try {
148
+ // Resolve the target path
149
+ if (input.path && input.path !== '.') {
150
+ // Use the provided path directly
151
+ targetPath = input.path;
152
+ logger.debug(`Using provided path: ${targetPath}`, { ...context, operation });
153
+ }
154
+ else {
155
+ // Path is '.' or undefined, try to get the session's working directory
156
+ const workingDir = context.getWorkingDirectory();
157
+ if (!workingDir) {
158
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, "No path provided and no working directory set for the session.", { context, operation });
159
+ }
160
+ targetPath = workingDir;
161
+ logger.debug(`Using session working directory: ${targetPath}`, { ...context, operation, sessionId: context.sessionId });
162
+ }
163
+ // Sanitize the resolved path
164
+ const sanitizedPath = sanitization.sanitizePath(targetPath);
165
+ logger.debug('Sanitized path', { ...context, operation, sanitizedPath });
166
+ targetPath = sanitizedPath; // Use the sanitized path going forward
167
+ }
168
+ catch (error) {
169
+ logger.error('Path resolution or sanitization failed', { ...context, operation, error });
170
+ if (error instanceof McpError) {
171
+ throw error;
172
+ }
173
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
174
+ }
175
+ try {
176
+ // Using --porcelain=v1 for stable, scriptable output and -b for branch info
177
+ // Ensure the path passed to -C is correctly quoted for the shell
178
+ const command = `git -C "${targetPath}" status --porcelain=v1 -b`;
179
+ logger.debug(`Executing command: ${command}`, { ...context, operation });
180
+ const { stdout, stderr } = await execAsync(command);
181
+ if (stderr) {
182
+ // Log stderr as warning but proceed to parse stdout
183
+ logger.warning(`Git status command produced stderr (may be informational)`, { ...context, operation, stderr });
184
+ }
185
+ logger.info(`${operation} command executed, parsing output...`, { ...context, operation, path: targetPath });
186
+ // Parse the porcelain output
187
+ const structuredResult = parseGitStatusPorcelainV1(stdout);
188
+ // If parsing resulted in clean state but no branch, re-check branch explicitly
189
+ // This handles the case of an empty repo after init but before first commit
190
+ if (structuredResult.isClean && !structuredResult.currentBranch) {
191
+ try {
192
+ const branchCommand = `git -C "${targetPath}" rev-parse --abbrev-ref HEAD`;
193
+ const { stdout: branchStdout } = await execAsync(branchCommand);
194
+ const currentBranch = branchStdout.trim();
195
+ if (currentBranch && currentBranch !== 'HEAD') {
196
+ structuredResult.currentBranch = currentBranch;
197
+ }
198
+ }
199
+ catch (branchError) {
200
+ // Ignore error if rev-parse fails (e.g., still no commits)
201
+ logger.debug('Could not determine branch via rev-parse, likely no commits yet.', { ...context, operation, branchError });
202
+ }
203
+ }
204
+ logger.info(`${operation} parsed successfully`, { ...context, operation, path: targetPath });
205
+ return structuredResult; // Return the structured JSON object
206
+ }
207
+ catch (error) {
208
+ logger.error(`Failed to execute or parse git status command`, { ...context, operation, path: targetPath, error: error.message, stderr: error.stderr });
209
+ const errorMessage = error.stderr || error.message || '';
210
+ if (errorMessage.toLowerCase().includes('not a git repository')) {
211
+ throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`, { context, operation, originalError: error });
212
+ }
213
+ throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to get git status for path: ${targetPath}. Error: ${errorMessage}`, { context, operation, originalError: error });
214
+ }
215
+ }
@@ -0,0 +1,77 @@
1
+ import { logger } from '../../../utils/logger.js';
2
+ import { ErrorHandler } from '../../../utils/errorHandler.js';
3
+ import { requestContextService } from '../../../utils/requestContext.js';
4
+ // Import the result type along with the function and input schema
5
+ import { getGitStatus, GitStatusInputSchema } from './logic.js';
6
+ import { BaseErrorCode } from '../../../types-global/errors.js'; // Import BaseErrorCode
7
+ let _getWorkingDirectory;
8
+ let _getSessionId;
9
+ /**
10
+ * Initializes the state accessors needed by the tool registration.
11
+ * This should be called once during server setup.
12
+ * @param getWdFn - Function to get the working directory for a session.
13
+ * @param getSidFn - Function to get the session ID from context.
14
+ */
15
+ export function initializeGitStatusStateAccessors(getWdFn, getSidFn) {
16
+ _getWorkingDirectory = getWdFn;
17
+ _getSessionId = getSidFn;
18
+ logger.info('State accessors initialized for git_status tool registration.');
19
+ }
20
+ const TOOL_NAME = 'git_status';
21
+ const TOOL_DESCRIPTION = 'Retrieves the status of a Git repository. Shows the working tree status including tracked/untracked files, modifications, staged changes, and current branch information. Returns the status as a JSON object.';
22
+ /**
23
+ * Registers the git_status tool with the MCP server.
24
+ * Uses the high-level server.tool() method for registration, schema validation, and routing.
25
+ *
26
+ * @param {McpServer} server - The McpServer instance to register the tool with.
27
+ * @returns {Promise<void>}
28
+ * @throws {Error} If registration fails or state accessors are not initialized.
29
+ */
30
+ export const registerGitStatusTool = async (server) => {
31
+ if (!_getWorkingDirectory || !_getSessionId) {
32
+ throw new Error('State accessors for git_status must be initialized before registration.');
33
+ }
34
+ const operation = 'registerGitStatusTool';
35
+ const context = requestContextService.createRequestContext({ operation });
36
+ await ErrorHandler.tryCatch(async () => {
37
+ server.tool(TOOL_NAME, TOOL_DESCRIPTION, GitStatusInputSchema.shape, // Provide the Zod schema shape
38
+ async (validatedArgs, callContext) => {
39
+ const toolOperation = 'tool:git_status';
40
+ // Create context, potentially inheriting from callContext
41
+ const requestContext = requestContextService.createRequestContext({ operation: toolOperation, parentContext: callContext });
42
+ // Get session ID
43
+ const sessionId = _getSessionId(requestContext);
44
+ // Define the session-specific getter function
45
+ const getWorkingDirectoryForSession = () => {
46
+ return _getWorkingDirectory(sessionId);
47
+ };
48
+ // Enhance context for the logic function
49
+ const logicContext = {
50
+ ...requestContext,
51
+ sessionId: sessionId,
52
+ getWorkingDirectory: getWorkingDirectoryForSession,
53
+ };
54
+ logger.info(`Executing tool: ${TOOL_NAME}`, logicContext);
55
+ // Use ErrorHandler.tryCatch to wrap the logic execution
56
+ return await ErrorHandler.tryCatch(async () => {
57
+ // Call the core logic function with validated args and enhanced context
58
+ const statusResult = await getGitStatus(validatedArgs, logicContext);
59
+ // Format the successful result as a JSON string within TextContent
60
+ const resultContent = {
61
+ type: 'text',
62
+ // Stringify the JSON object for the response content
63
+ text: JSON.stringify(statusResult, null, 2), // Pretty-print JSON
64
+ contentType: 'application/json', // Specify content type
65
+ };
66
+ logger.info(`Tool ${TOOL_NAME} executed successfully, returning JSON`, logicContext);
67
+ return { content: [resultContent] }; // isError defaults to false
68
+ }, {
69
+ operation: toolOperation,
70
+ context: logicContext,
71
+ input: validatedArgs, // Log sanitized input
72
+ errorCode: BaseErrorCode.INTERNAL_ERROR, // Default error code if logic fails unexpectedly
73
+ });
74
+ });
75
+ logger.info(`Tool registered: ${TOOL_NAME}`, context);
76
+ }, { operation, context, critical: true }); // Treat registration failure as critical
77
+ };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @fileoverview Barrel file for the git_tag tool.
3
+ * Exports the registration function and state accessor initialization function.
4
+ */
5
+ export { registerGitTagTool, initializeGitTagStateAccessors } from './registration.js';
6
+ // Export types if needed elsewhere, e.g.:
7
+ // export type { GitTagInput, GitTagResult } from './logic.js';
@@ -0,0 +1,142 @@
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';
5
+ import { logger } from '../../../utils/logger.js';
6
+ import { sanitization } from '../../../utils/sanitization.js';
7
+ const execAsync = promisify(exec);
8
+ // Define the base input schema for the git_tag tool using Zod
9
+ // We export this separately to access its .shape for registration
10
+ export const GitTagBaseSchema = z.object({
11
+ path: z.string().min(1).optional().default('.').describe("Path to the local Git repository. If omitted, defaults to the path set by `git_set_working_dir` for the current session, or the server's CWD if no session path is set."),
12
+ 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)."),
13
+ tagName: z.string().min(1).optional().describe("The name for the tag. Required for 'create' and 'delete' modes. e.g., 'v2.3.0'."),
14
+ message: z.string().optional().describe("The annotation message for the tag. Required and used only when 'mode' is 'create' and 'annotate' is true."),
15
+ 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."),
16
+ 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."),
17
+ // force: z.boolean().default(false).describe("Force tag creation/update (-f flag). Use with caution as it can overwrite existing tags."), // Consider adding later
18
+ });
19
+ // Apply refinements for conditional validation and export the final schema
20
+ export const GitTagInputSchema = GitTagBaseSchema.refine(data => !(data.mode === 'create' && data.annotate && !data.message), {
21
+ message: "An annotation 'message' is required when creating an annotated tag (annotate=true).",
22
+ path: ["message"], // Point Zod error to the message field
23
+ }).refine(data => !(data.mode === 'create' && !data.tagName), {
24
+ message: "A 'tagName' is required for 'create' mode.",
25
+ path: ["tagName"], // Point Zod error to the tagName field
26
+ }).refine(data => !(data.mode === 'delete' && !data.tagName), {
27
+ message: "A 'tagName' is required for 'delete' mode.",
28
+ path: ["tagName"], // Point Zod error to the tagName field
29
+ });
30
+ /**
31
+ * Executes git tag commands based on the specified mode.
32
+ *
33
+ * @param {GitTagInput} input - The validated input object.
34
+ * @param {RequestContext} context - The request context for logging and error handling.
35
+ * @returns {Promise<GitTagResult>} A promise that resolves with the structured result.
36
+ * @throws {McpError} Throws an McpError for path resolution/validation failures or unexpected errors.
37
+ */
38
+ export async function gitTagLogic(input, context) {
39
+ const operation = `gitTagLogic:${input.mode}`;
40
+ logger.debug(`Executing ${operation}`, { ...context, input });
41
+ let targetPath;
42
+ try {
43
+ // Resolve and sanitize the target path
44
+ const workingDir = context.getWorkingDirectory();
45
+ targetPath = (input.path && input.path !== '.')
46
+ ? input.path
47
+ : workingDir ?? '.';
48
+ if (targetPath === '.' && !workingDir) {
49
+ logger.warning("Executing git tag in server's CWD as no path provided and no session WD set.", { ...context, operation });
50
+ targetPath = process.cwd();
51
+ }
52
+ else if (targetPath === '.' && workingDir) {
53
+ targetPath = workingDir;
54
+ logger.debug(`Using session working directory: ${targetPath}`, { ...context, operation, sessionId: context.sessionId });
55
+ }
56
+ else {
57
+ logger.debug(`Using provided path: ${targetPath}`, { ...context, operation });
58
+ }
59
+ targetPath = sanitization.sanitizePath(targetPath);
60
+ logger.debug('Sanitized path', { ...context, operation, sanitizedPath: targetPath });
61
+ }
62
+ catch (error) {
63
+ logger.error('Path resolution or sanitization failed', { ...context, operation, error });
64
+ if (error instanceof McpError)
65
+ throw error;
66
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
67
+ }
68
+ // Validate tag name format (simple validation)
69
+ if (input.tagName && !/^[a-zA-Z0-9_./-]+$/.test(input.tagName)) {
70
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid tag name format: ${input.tagName}`, { context, operation });
71
+ }
72
+ // Validate commit ref format (simple validation - allows hashes, HEAD, branches, etc.)
73
+ if (input.commitRef && !/^[a-zA-Z0-9_./~^-]+$/.test(input.commitRef)) {
74
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid commit reference format: ${input.commitRef}`, { context, operation });
75
+ }
76
+ try {
77
+ let command;
78
+ let result;
79
+ switch (input.mode) {
80
+ case 'list':
81
+ command = `git -C "${targetPath}" tag --list`;
82
+ logger.debug(`Executing command: ${command}`, { ...context, operation });
83
+ const { stdout: listStdout } = await execAsync(command);
84
+ const tags = listStdout.trim().split('\n').filter(tag => tag); // Filter out empty lines
85
+ result = { success: true, mode: 'list', tags };
86
+ break;
87
+ case 'create':
88
+ // TagName is validated by Zod refine
89
+ const tagNameCreate = input.tagName;
90
+ command = `git -C "${targetPath}" tag`;
91
+ if (input.annotate) {
92
+ // Message is validated by Zod refine
93
+ command += ` -a -m "${input.message.replace(/"/g, '\\"')}"`;
94
+ }
95
+ command += ` "${tagNameCreate}"`;
96
+ if (input.commitRef) {
97
+ command += ` "${input.commitRef}"`;
98
+ }
99
+ logger.debug(`Executing command: ${command}`, { ...context, operation });
100
+ await execAsync(command);
101
+ result = { success: true, mode: 'create', message: `Tag '${tagNameCreate}' created successfully.`, tagName: tagNameCreate };
102
+ break;
103
+ case 'delete':
104
+ // TagName is validated by Zod refine
105
+ const tagNameDelete = input.tagName;
106
+ command = `git -C "${targetPath}" tag -d "${tagNameDelete}"`;
107
+ logger.debug(`Executing command: ${command}`, { ...context, operation });
108
+ await execAsync(command);
109
+ result = { success: true, mode: 'delete', message: `Tag '${tagNameDelete}' deleted successfully.`, tagName: tagNameDelete };
110
+ break;
111
+ default:
112
+ // Should not happen due to Zod validation
113
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid mode: ${input.mode}`, { context, operation });
114
+ }
115
+ logger.info(`${operation} executed successfully`, { ...context, operation, path: targetPath });
116
+ return result;
117
+ }
118
+ catch (error) {
119
+ const errorMessage = error.stderr || error.message || '';
120
+ logger.error(`Failed to execute git tag command`, { ...context, operation, path: targetPath, error: errorMessage, stderr: error.stderr, stdout: error.stdout });
121
+ // Specific error handling
122
+ if (errorMessage.toLowerCase().includes('not a git repository')) {
123
+ throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`, { context, operation, originalError: error });
124
+ }
125
+ if (input.mode === 'create' && errorMessage.toLowerCase().includes('already exists')) {
126
+ return { success: false, mode: 'create', message: `Failed to create tag: Tag '${input.tagName}' already exists.`, error: errorMessage };
127
+ }
128
+ if (input.mode === 'delete' && errorMessage.toLowerCase().includes('not found')) {
129
+ return { success: false, mode: 'delete', message: `Failed to delete tag: Tag '${input.tagName}' not found.`, error: errorMessage };
130
+ }
131
+ if (input.mode === 'create' && input.commitRef && errorMessage.toLowerCase().includes('unknown revision or path not in the working tree')) {
132
+ return { success: false, mode: 'create', message: `Failed to create tag: Commit reference '${input.commitRef}' not found.`, error: errorMessage };
133
+ }
134
+ // Return structured failure for other git errors
135
+ return {
136
+ success: false,
137
+ mode: input.mode,
138
+ message: `Git tag ${input.mode} failed for path: ${targetPath}.`,
139
+ error: errorMessage
140
+ };
141
+ }
142
+ }
@@ -0,0 +1,84 @@
1
+ import { ErrorHandler } from '../../../utils/errorHandler.js';
2
+ import { logger } from '../../../utils/logger.js';
3
+ import { requestContextService } from '../../../utils/requestContext.js';
4
+ // Import the final schema and types for handler logic
5
+ // Import the BASE schema separately for registration shape
6
+ import { BaseErrorCode } from '../../../types-global/errors.js';
7
+ import { GitTagBaseSchema, gitTagLogic } from './logic.js';
8
+ let _getWorkingDirectory;
9
+ let _getSessionId;
10
+ /**
11
+ * Initializes the state accessors needed by the git_tag tool registration.
12
+ * @param getWdFn - Function to get the working directory for a session.
13
+ * @param getSidFn - Function to get the session ID from context.
14
+ */
15
+ export function initializeGitTagStateAccessors(getWdFn, getSidFn) {
16
+ _getWorkingDirectory = getWdFn;
17
+ _getSessionId = getSidFn;
18
+ logger.info('State accessors initialized for git_tag tool registration.');
19
+ }
20
+ const TOOL_NAME = 'git_tag';
21
+ const TOOL_DESCRIPTION = 'Manages Git tags. Supports listing existing tags, creating new lightweight or annotated tags against specific commits, and deleting local tags. Returns results as a JSON object.';
22
+ /**
23
+ * Registers the git_tag tool with the MCP server.
24
+ *
25
+ * @param {McpServer} server - The McpServer instance to register the tool with.
26
+ * @returns {Promise<void>}
27
+ * @throws {Error} If registration fails or state accessors are not initialized.
28
+ */
29
+ export const registerGitTagTool = async (server) => {
30
+ if (!_getWorkingDirectory || !_getSessionId) {
31
+ throw new Error('State accessors for git_tag must be initialized before registration.');
32
+ }
33
+ const operation = 'registerGitTagTool';
34
+ const context = requestContextService.createRequestContext({ operation });
35
+ await ErrorHandler.tryCatch(async () => {
36
+ // Register the tool using the *base* schema's shape for definition
37
+ server.tool(// Use BASE schema shape here
38
+ TOOL_NAME, TOOL_DESCRIPTION, GitTagBaseSchema.shape, // Use the shape from the BASE schema
39
+ // Let TypeScript infer handler argument types.
40
+ // The SDK validates against the full GitTagInputSchema before calling this.
41
+ async (validatedArgs, callContext) => {
42
+ // Cast validatedArgs to the specific input type for use within the handler
43
+ const toolInput = validatedArgs;
44
+ const toolOperation = `tool:${TOOL_NAME}:${toolInput.mode}`; // Include mode in operation
45
+ const requestContext = requestContextService.createRequestContext({ operation: toolOperation, parentContext: callContext });
46
+ const sessionId = _getSessionId(requestContext);
47
+ const getWorkingDirectoryForSession = () => {
48
+ return _getWorkingDirectory(sessionId);
49
+ };
50
+ const logicContext = {
51
+ ...requestContext,
52
+ sessionId: sessionId,
53
+ getWorkingDirectory: getWorkingDirectoryForSession,
54
+ };
55
+ logger.info(`Executing tool: ${TOOL_NAME} (mode: ${toolInput.mode})`, logicContext);
56
+ return await ErrorHandler.tryCatch(async () => {
57
+ // Call the core logic function which returns a GitTagResult object
58
+ const tagResult = await gitTagLogic(toolInput, logicContext);
59
+ // Format the result as a JSON string within TextContent
60
+ const resultContent = {
61
+ type: 'text',
62
+ text: JSON.stringify(tagResult, null, 2), // Pretty-print JSON
63
+ contentType: 'application/json',
64
+ };
65
+ // Log based on the success flag in the result
66
+ if (tagResult.success) {
67
+ logger.info(`Tool ${TOOL_NAME} (mode: ${toolInput.mode}) executed successfully, returning JSON`, logicContext);
68
+ }
69
+ else {
70
+ // Log specific failure message from the result
71
+ logger.warning(`Tool ${TOOL_NAME} (mode: ${toolInput.mode}) failed: ${tagResult.message}`, { ...logicContext, errorDetails: tagResult.error });
72
+ }
73
+ // Return the result, whether success or structured failure
74
+ return { content: [resultContent] };
75
+ }, {
76
+ operation: toolOperation,
77
+ context: logicContext,
78
+ input: validatedArgs, // Log the raw validated args
79
+ errorCode: BaseErrorCode.INTERNAL_ERROR, // Default if unexpected error occurs in logic/wrapper
80
+ });
81
+ });
82
+ logger.info(`Tool registered: ${TOOL_NAME}`, context);
83
+ }, { operation, context, critical: true }); // Mark registration as critical
84
+ };
@@ -0,0 +1,68 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Defines a set of standardized error codes for common issues within MCP servers or tools.
4
+ * These codes help clients understand the nature of an error programmatically.
5
+ */
6
+ export var BaseErrorCode;
7
+ (function (BaseErrorCode) {
8
+ /** Access denied due to invalid credentials or lack of authentication. */
9
+ BaseErrorCode["UNAUTHORIZED"] = "UNAUTHORIZED";
10
+ /** Access denied despite valid authentication, due to insufficient permissions. */
11
+ BaseErrorCode["FORBIDDEN"] = "FORBIDDEN";
12
+ /** The requested resource or entity could not be found. */
13
+ BaseErrorCode["NOT_FOUND"] = "NOT_FOUND";
14
+ /** The request could not be completed due to a conflict with the current state of the resource. */
15
+ BaseErrorCode["CONFLICT"] = "CONFLICT";
16
+ /** The request failed due to invalid input parameters or data. */
17
+ BaseErrorCode["VALIDATION_ERROR"] = "VALIDATION_ERROR";
18
+ /** The request was rejected because the client has exceeded rate limits. */
19
+ BaseErrorCode["RATE_LIMITED"] = "RATE_LIMITED";
20
+ /** The request timed out before a response could be generated. */
21
+ BaseErrorCode["TIMEOUT"] = "TIMEOUT";
22
+ /** The service is temporarily unavailable, possibly due to maintenance or overload. */
23
+ BaseErrorCode["SERVICE_UNAVAILABLE"] = "SERVICE_UNAVAILABLE";
24
+ /** An unexpected error occurred on the server side. */
25
+ BaseErrorCode["INTERNAL_ERROR"] = "INTERNAL_ERROR";
26
+ /** An error occurred, but the specific cause is unknown or cannot be categorized. */
27
+ BaseErrorCode["UNKNOWN_ERROR"] = "UNKNOWN_ERROR";
28
+ /** An error occurred during the loading or validation of configuration data. */
29
+ BaseErrorCode["CONFIGURATION_ERROR"] = "CONFIGURATION_ERROR";
30
+ /** An error occurred related to network connectivity (e.g., DNS resolution, connection refused). */
31
+ BaseErrorCode["NETWORK_ERROR"] = "NETWORK_ERROR";
32
+ })(BaseErrorCode || (BaseErrorCode = {}));
33
+ /**
34
+ * Custom error class for MCP-specific errors.
35
+ * Encapsulates a `BaseErrorCode`, a descriptive message, and optional details.
36
+ * Provides a method to format the error into a standard MCP tool response.
37
+ */
38
+ export class McpError extends Error {
39
+ code;
40
+ details;
41
+ /**
42
+ * Creates an instance of McpError.
43
+ * @param {BaseErrorCode} code - The standardized error code.
44
+ * @param {string} message - A human-readable description of the error.
45
+ * @param {Record<string, unknown>} [details] - Optional additional details about the error.
46
+ */
47
+ constructor(code, message, details) {
48
+ super(message);
49
+ this.code = code;
50
+ this.details = details;
51
+ // Set the error name for identification
52
+ this.name = 'McpError';
53
+ // Ensure the prototype chain is correct
54
+ Object.setPrototypeOf(this, McpError.prototype);
55
+ }
56
+ }
57
+ /**
58
+ * Zod schema for validating error objects, potentially used for parsing
59
+ * error responses or validating error structures internally.
60
+ */
61
+ export const ErrorSchema = z.object({
62
+ /** The error code, corresponding to BaseErrorCode enum values. */
63
+ code: z.nativeEnum(BaseErrorCode).describe("Standardized error code"),
64
+ /** A human-readable description of the error. */
65
+ message: z.string().describe("Detailed error message"),
66
+ /** Optional additional details or context about the error. */
67
+ details: z.record(z.unknown()).optional().describe("Optional structured error details")
68
+ }).describe("Schema for validating structured error objects.");
@@ -0,0 +1,59 @@
1
+ // Type definitions for the MCP (Message Control Protocol) protocol
2
+ // and standard JSON-RPC 2.0 structures
3
+ // Standard JSON-RPC 2.0 Error Codes
4
+ export var JsonRpcErrorCode;
5
+ (function (JsonRpcErrorCode) {
6
+ JsonRpcErrorCode[JsonRpcErrorCode["PARSE_ERROR"] = -32700] = "PARSE_ERROR";
7
+ JsonRpcErrorCode[JsonRpcErrorCode["INVALID_REQUEST"] = -32600] = "INVALID_REQUEST";
8
+ JsonRpcErrorCode[JsonRpcErrorCode["METHOD_NOT_FOUND"] = -32601] = "METHOD_NOT_FOUND";
9
+ JsonRpcErrorCode[JsonRpcErrorCode["INVALID_PARAMS"] = -32602] = "INVALID_PARAMS";
10
+ JsonRpcErrorCode[JsonRpcErrorCode["INTERNAL_ERROR"] = -32603] = "INTERNAL_ERROR";
11
+ // -32000 to -32099 are reserved for implementation-defined server-errors.
12
+ JsonRpcErrorCode[JsonRpcErrorCode["SERVER_ERROR_START"] = -32000] = "SERVER_ERROR_START";
13
+ JsonRpcErrorCode[JsonRpcErrorCode["SERVER_ERROR_END"] = -32099] = "SERVER_ERROR_END";
14
+ })(JsonRpcErrorCode || (JsonRpcErrorCode = {}));
15
+ // ==================================
16
+ // Helper Functions (Updated for JSON-RPC context)
17
+ // ==================================
18
+ /**
19
+ * Creates a JSON-RPC 2.0 Success Response containing an McpToolResult.
20
+ */
21
+ export const createJsonRpcToolSuccessResponse = (id, text) => ({
22
+ jsonrpc: "2.0",
23
+ result: {
24
+ content: [{ type: "text", text }]
25
+ },
26
+ id
27
+ });
28
+ /**
29
+ * Creates a JSON-RPC 2.0 Error Response.
30
+ */
31
+ export const createJsonRpcErrorResponse = (id, code, message, data) => ({
32
+ jsonrpc: "2.0",
33
+ error: {
34
+ code,
35
+ message,
36
+ ...(data !== undefined && { data }) // Include data only if provided
37
+ },
38
+ id
39
+ });
40
+ /**
41
+ * Creates a JSON-RPC 2.0 Success Response containing an McpResourceResult.
42
+ */
43
+ export const createJsonRpcResourceSuccessResponse = (id, uri, text, mimeType) => ({
44
+ jsonrpc: "2.0",
45
+ result: {
46
+ contents: [{ uri, text, mimeType }]
47
+ },
48
+ id
49
+ });
50
+ // Note: PromptResponse helper might need adjustment depending on how prompts fit into JSON-RPC
51
+ export const createPromptResponse = (text, role = "assistant") => ({
52
+ messages: [{
53
+ role,
54
+ content: {
55
+ type: "text",
56
+ text
57
+ }
58
+ }]
59
+ });
@@ -0,0 +1 @@
1
+ export {};