@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,99 @@
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_show tool using Zod
9
+ // No refinements needed here, so we don't need a separate BaseSchema
10
+ export const GitShowInputSchema = 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
+ ref: z.string().min(1).describe("The object reference (commit hash, tag name, branch name, HEAD, etc.) to show."),
13
+ filePath: z.string().optional().describe("Optional specific file path within the ref to show (e.g., show a file's content at a specific commit). If provided, use the format '<ref>:<filePath>'."),
14
+ // format: z.string().optional().describe("Optional format string for the output"), // Consider adding later
15
+ });
16
+ /**
17
+ * Executes the 'git show' command for a given reference and optional file path.
18
+ *
19
+ * @param {GitShowInput} input - The validated input object.
20
+ * @param {RequestContext} context - The request context for logging and error handling.
21
+ * @returns {Promise<GitShowResult>} A promise that resolves with the structured result.
22
+ * @throws {McpError} Throws an McpError for path resolution/validation failures or unexpected errors.
23
+ */
24
+ export async function gitShowLogic(input, context) {
25
+ const operation = 'gitShowLogic';
26
+ logger.debug(`Executing ${operation}`, { ...context, input });
27
+ let targetPath;
28
+ try {
29
+ // Resolve and sanitize the target path
30
+ const workingDir = context.getWorkingDirectory();
31
+ targetPath = (input.path && input.path !== '.')
32
+ ? input.path
33
+ : workingDir ?? '.';
34
+ if (targetPath === '.' && !workingDir) {
35
+ logger.warning("Executing git show in server's CWD as no path provided and no session WD set.", { ...context, operation });
36
+ targetPath = process.cwd();
37
+ }
38
+ else if (targetPath === '.' && workingDir) {
39
+ targetPath = workingDir;
40
+ logger.debug(`Using session working directory: ${targetPath}`, { ...context, operation, sessionId: context.sessionId });
41
+ }
42
+ else {
43
+ logger.debug(`Using provided path: ${targetPath}`, { ...context, operation });
44
+ }
45
+ targetPath = sanitization.sanitizePath(targetPath);
46
+ logger.debug('Sanitized path', { ...context, operation, sanitizedPath: targetPath });
47
+ }
48
+ catch (error) {
49
+ logger.error('Path resolution or sanitization failed', { ...context, operation, error });
50
+ if (error instanceof McpError)
51
+ throw error;
52
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
53
+ }
54
+ // Validate ref format (simple validation)
55
+ if (!/^[a-zA-Z0-9_./~^:-]+$/.test(input.ref)) { // Allow ':' for filePath combination
56
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid reference format: ${input.ref}`, { context, operation });
57
+ }
58
+ // Validate filePath format if provided (basic path chars)
59
+ if (input.filePath && !/^[a-zA-Z0-9_./-]+$/.test(input.filePath)) {
60
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid file path format: ${input.filePath}`, { context, operation });
61
+ }
62
+ try {
63
+ // Construct the refspec, combining ref and filePath if needed
64
+ const refSpec = input.filePath ? `${input.ref}:"${input.filePath}"` : `"${input.ref}"`;
65
+ // Construct the command
66
+ const command = `git -C "${targetPath}" show ${refSpec}`;
67
+ logger.debug(`Executing command: ${command}`, { ...context, operation });
68
+ // Execute command. Note: git show might write to stderr for non-error info (like commit details before diff)
69
+ // We primarily care about stdout for the content. Errors usually have non-zero exit code.
70
+ const { stdout, stderr } = await execAsync(command);
71
+ if (stderr) {
72
+ // Log stderr as debug info, as it might contain commit details etc.
73
+ logger.debug(`Git show command produced stderr (may be informational)`, { ...context, operation, stderr });
74
+ }
75
+ logger.info(`${operation} executed successfully`, { ...context, operation, path: targetPath, refSpec });
76
+ return { success: true, content: stdout }; // Return raw stdout content
77
+ }
78
+ catch (error) {
79
+ const errorMessage = error.stderr || error.message || '';
80
+ logger.error(`Failed to execute git show command`, { ...context, operation, path: targetPath, error: errorMessage, stderr: error.stderr, stdout: error.stdout });
81
+ // Specific error handling
82
+ if (errorMessage.toLowerCase().includes('not a git repository')) {
83
+ throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`, { context, operation, originalError: error });
84
+ }
85
+ if (/unknown revision or path not in the working tree/i.test(errorMessage)) {
86
+ const target = input.filePath ? `${input.ref}:${input.filePath}` : input.ref;
87
+ return { success: false, message: `Failed to show: Reference or pathspec '${target}' not found.`, error: errorMessage };
88
+ }
89
+ if (/ambiguous argument/i.test(errorMessage)) {
90
+ return { success: false, message: `Failed to show: Reference '${input.ref}' is ambiguous. Provide a more specific reference.`, error: errorMessage };
91
+ }
92
+ // Return structured failure for other git errors
93
+ return {
94
+ success: false,
95
+ message: `Git show failed for path: ${targetPath}, ref: ${input.ref}.`,
96
+ error: errorMessage
97
+ };
98
+ }
99
+ }
@@ -0,0 +1,83 @@
1
+ import { logger } from '../../../utils/logger.js';
2
+ import { ErrorHandler } from '../../../utils/errorHandler.js';
3
+ import { requestContextService } from '../../../utils/requestContext.js';
4
+ // Import the schema and types
5
+ import { gitShowLogic, GitShowInputSchema } from './logic.js';
6
+ import { BaseErrorCode } from '../../../types-global/errors.js';
7
+ let _getWorkingDirectory;
8
+ let _getSessionId;
9
+ /**
10
+ * Initializes the state accessors needed by the git_show tool registration.
11
+ * @param getWdFn - Function to get the working directory for a session.
12
+ * @param getSidFn - Function to get the session ID from context.
13
+ */
14
+ export function initializeGitShowStateAccessors(getWdFn, getSidFn) {
15
+ _getWorkingDirectory = getWdFn;
16
+ _getSessionId = getSidFn;
17
+ logger.info('State accessors initialized for git_show tool registration.');
18
+ }
19
+ const TOOL_NAME = 'git_show';
20
+ const TOOL_DESCRIPTION = 'Shows information about Git objects (commits, tags, blobs, trees) based on a reference. Can optionally show the content of a specific file at that reference. Returns the raw output.';
21
+ /**
22
+ * Registers the git_show tool with the MCP server.
23
+ *
24
+ * @param {McpServer} server - The McpServer instance to register the tool with.
25
+ * @returns {Promise<void>}
26
+ * @throws {Error} If registration fails or state accessors are not initialized.
27
+ */
28
+ export const registerGitShowTool = async (server) => {
29
+ if (!_getWorkingDirectory || !_getSessionId) {
30
+ throw new Error('State accessors for git_show must be initialized before registration.');
31
+ }
32
+ const operation = 'registerGitShowTool';
33
+ const context = requestContextService.createRequestContext({ operation });
34
+ await ErrorHandler.tryCatch(async () => {
35
+ // Register the tool using the schema's shape (no refinements here)
36
+ server.tool(TOOL_NAME, TOOL_DESCRIPTION, GitShowInputSchema.shape, // Use the shape directly
37
+ // Let TypeScript infer handler argument types.
38
+ async (validatedArgs, callContext) => {
39
+ // Cast validatedArgs to the specific input type for use within the handler
40
+ const toolInput = validatedArgs;
41
+ const toolOperation = `tool:${TOOL_NAME}`;
42
+ const requestContext = requestContextService.createRequestContext({ operation: toolOperation, parentContext: callContext });
43
+ const sessionId = _getSessionId(requestContext);
44
+ const getWorkingDirectoryForSession = () => {
45
+ return _getWorkingDirectory(sessionId);
46
+ };
47
+ const logicContext = {
48
+ ...requestContext,
49
+ sessionId: sessionId,
50
+ getWorkingDirectory: getWorkingDirectoryForSession,
51
+ };
52
+ logger.info(`Executing tool: ${TOOL_NAME}`, logicContext);
53
+ return await ErrorHandler.tryCatch(async () => {
54
+ // Call the core logic function which returns a GitShowResult object
55
+ const showResult = await gitShowLogic(toolInput, logicContext);
56
+ // Format the result within TextContent
57
+ const resultContent = {
58
+ type: 'text',
59
+ // Return raw content on success, or error message on failure
60
+ text: showResult.success ? showResult.content : `Error: ${showResult.message}${showResult.error ? `\nDetails: ${showResult.error}` : ''}`,
61
+ // Use plain text content type, unless we decide to return JSON later
62
+ contentType: 'text/plain',
63
+ };
64
+ // Log based on the success flag in the result
65
+ if (showResult.success) {
66
+ logger.info(`Tool ${TOOL_NAME} executed successfully`, logicContext);
67
+ }
68
+ else {
69
+ // Log specific failure message from the result
70
+ logger.warning(`Tool ${TOOL_NAME} failed: ${showResult.message}`, { ...logicContext, errorDetails: showResult.error });
71
+ }
72
+ // Return the result, whether success or structured failure
73
+ return { content: [resultContent] };
74
+ }, {
75
+ operation: toolOperation,
76
+ context: logicContext,
77
+ input: validatedArgs, // Log the raw validated args
78
+ errorCode: BaseErrorCode.INTERNAL_ERROR, // Default if unexpected error occurs in logic/wrapper
79
+ });
80
+ });
81
+ logger.info(`Tool registered: ${TOOL_NAME}`, context);
82
+ }, { operation, context, critical: true }); // Mark registration as critical
83
+ };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @fileoverview Barrel file for the git_stash tool.
3
+ * Exports the registration function and state accessor initialization function.
4
+ */
5
+ export { registerGitStashTool, initializeGitStashStateAccessors } from './registration.js';
6
+ // Export types if needed elsewhere, e.g.:
7
+ // export type { GitStashInput, GitStashResult } from './logic.js';
@@ -0,0 +1,161 @@
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 BASE input schema for the git_stash tool using Zod
9
+ export const GitStashBaseSchema = z.object({
10
+ 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."),
11
+ mode: z.enum(['list', 'apply', 'pop', 'drop', 'save']).describe("The stash operation to perform: 'list', 'apply', 'pop', 'drop', 'save'."),
12
+ stashRef: z.string().optional().describe("Stash reference (e.g., 'stash@{1}'). Required for 'apply', 'pop', 'drop' modes."),
13
+ message: z.string().optional().describe("Optional descriptive message used only for 'save' mode."),
14
+ // includeUntracked: z.boolean().default(false).describe("Include untracked files in 'save' mode (-u)"), // Consider adding later
15
+ // keepIndex: z.boolean().default(false).describe("Keep staged changes in 'save' mode (--keep-index)"), // Consider adding later
16
+ });
17
+ // Apply refinements and export the FINAL schema for validation within the handler
18
+ export const GitStashInputSchema = GitStashBaseSchema.refine(data => !(['apply', 'pop', 'drop'].includes(data.mode) && !data.stashRef), {
19
+ message: "A 'stashRef' (e.g., 'stash@{0}') is required for 'apply', 'pop', and 'drop' modes.",
20
+ path: ["stashRef"], // Point error to the stashRef field
21
+ });
22
+ /**
23
+ * Executes git stash commands based on the specified mode.
24
+ *
25
+ * @param {GitStashInput} input - The validated input object (validated against GitStashInputSchema).
26
+ * @param {RequestContext} context - The request context for logging and error handling.
27
+ * @returns {Promise<GitStashResult>} A promise that resolves with the structured result.
28
+ * @throws {McpError} Throws an McpError for path resolution/validation failures or unexpected errors.
29
+ */
30
+ export async function gitStashLogic(input, context) {
31
+ const operation = `gitStashLogic:${input.mode}`;
32
+ logger.debug(`Executing ${operation}`, { ...context, input });
33
+ let targetPath;
34
+ try {
35
+ // Resolve and sanitize the target path
36
+ const workingDir = context.getWorkingDirectory();
37
+ targetPath = (input.path && input.path !== '.')
38
+ ? input.path
39
+ : workingDir ?? '.';
40
+ if (targetPath === '.' && !workingDir) {
41
+ logger.warning("Executing git stash in server's CWD as no path provided and no session WD set.", { ...context, operation });
42
+ targetPath = process.cwd();
43
+ }
44
+ else if (targetPath === '.' && workingDir) {
45
+ targetPath = workingDir;
46
+ logger.debug(`Using session working directory: ${targetPath}`, { ...context, operation, sessionId: context.sessionId });
47
+ }
48
+ else {
49
+ logger.debug(`Using provided path: ${targetPath}`, { ...context, operation });
50
+ }
51
+ targetPath = sanitization.sanitizePath(targetPath);
52
+ logger.debug('Sanitized path', { ...context, operation, sanitizedPath: targetPath });
53
+ }
54
+ catch (error) {
55
+ logger.error('Path resolution or sanitization failed', { ...context, operation, error });
56
+ if (error instanceof McpError)
57
+ throw error;
58
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
59
+ }
60
+ // Validate stashRef format if provided (simple validation)
61
+ if (input.stashRef && !/^stash@\{[0-9]+\}$/.test(input.stashRef)) {
62
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid stash reference format: ${input.stashRef}. Expected format: stash@{n}`, { context, operation });
63
+ }
64
+ try {
65
+ let command;
66
+ let result;
67
+ switch (input.mode) {
68
+ case 'list':
69
+ command = `git -C "${targetPath}" stash list`;
70
+ logger.debug(`Executing command: ${command}`, { ...context, operation });
71
+ const { stdout: listStdout } = await execAsync(command);
72
+ const stashes = listStdout.trim().split('\n')
73
+ .filter(line => line)
74
+ .map(line => {
75
+ // Improved regex to handle different stash list formats
76
+ const match = line.match(/^(stash@\{(\d+)\}):\s*(?:(?:WIP on|On)\s*([^:]+):\s*)?(.*)$/);
77
+ return match
78
+ ? { ref: match[1], branch: match[3] || 'unknown', description: match[4] }
79
+ : { ref: 'unknown', branch: 'unknown', description: line }; // Fallback parsing
80
+ });
81
+ result = { success: true, mode: 'list', stashes };
82
+ break;
83
+ case 'apply':
84
+ case 'pop':
85
+ // stashRef is validated by Zod refine
86
+ const stashRefApplyPop = input.stashRef;
87
+ command = `git -C "${targetPath}" stash ${input.mode} ${stashRefApplyPop}`;
88
+ logger.debug(`Executing command: ${command}`, { ...context, operation });
89
+ try {
90
+ const { stdout, stderr } = await execAsync(command);
91
+ // Check stdout/stderr for conflict messages, although exit code 0 usually means success
92
+ const conflicts = /conflict/i.test(stdout) || /conflict/i.test(stderr);
93
+ const message = conflicts
94
+ ? `Stash ${input.mode} resulted in conflicts that need manual resolution.`
95
+ : `Stash ${stashRefApplyPop} ${input.mode === 'apply' ? 'applied' : 'popped'} successfully.`;
96
+ logger.info(message, { ...context, operation, path: targetPath, conflicts });
97
+ result = { success: true, mode: input.mode, message, conflicts };
98
+ }
99
+ catch (applyError) {
100
+ const applyErrorMessage = applyError.stderr || applyError.message || '';
101
+ if (/conflict/i.test(applyErrorMessage)) {
102
+ logger.warning(`Stash ${input.mode} failed due to conflicts.`, { ...context, operation, path: targetPath, error: applyErrorMessage });
103
+ // Return failure but indicate conflicts
104
+ return { success: false, mode: input.mode, message: `Failed to ${input.mode} stash ${stashRefApplyPop} due to conflicts. Resolve conflicts manually.`, error: applyErrorMessage, conflicts: true };
105
+ }
106
+ // Rethrow other errors
107
+ throw applyError;
108
+ }
109
+ break;
110
+ case 'drop':
111
+ // stashRef is validated by Zod refine
112
+ const stashRefDrop = input.stashRef;
113
+ command = `git -C "${targetPath}" stash drop ${stashRefDrop}`;
114
+ logger.debug(`Executing command: ${command}`, { ...context, operation });
115
+ await execAsync(command);
116
+ result = { success: true, mode: 'drop', message: `Dropped ${stashRefDrop} successfully.`, stashRef: stashRefDrop };
117
+ break;
118
+ case 'save':
119
+ command = `git -C "${targetPath}" stash save`;
120
+ if (input.message) {
121
+ // Ensure message is properly quoted for the shell
122
+ command += ` "${input.message.replace(/"/g, '\\"')}"`;
123
+ }
124
+ logger.debug(`Executing command: ${command}`, { ...context, operation });
125
+ const { stdout: saveStdout } = await execAsync(command);
126
+ const stashCreated = !/no local changes to save/i.test(saveStdout);
127
+ const saveMessage = stashCreated
128
+ ? `Changes stashed successfully.` + (input.message ? ` Message: "${input.message}"` : '')
129
+ : "No local changes to save.";
130
+ result = { success: true, mode: 'save', message: saveMessage, stashCreated };
131
+ break;
132
+ default:
133
+ // Should not happen due to Zod validation
134
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid mode: ${input.mode}`, { context, operation });
135
+ }
136
+ logger.info(`${operation} executed successfully`, { ...context, operation, path: targetPath });
137
+ return result;
138
+ }
139
+ catch (error) {
140
+ const errorMessage = error.stderr || error.message || '';
141
+ logger.error(`Failed to execute git stash command`, { ...context, operation, path: targetPath, error: errorMessage, stderr: error.stderr, stdout: error.stdout });
142
+ // Specific error handling
143
+ if (errorMessage.toLowerCase().includes('not a git repository')) {
144
+ throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`, { context, operation, originalError: error });
145
+ }
146
+ if ((input.mode === 'apply' || input.mode === 'pop' || input.mode === 'drop') && /no such stash/i.test(errorMessage)) {
147
+ return { success: false, mode: input.mode, message: `Failed to ${input.mode} stash: Stash '${input.stashRef}' not found.`, error: errorMessage };
148
+ }
149
+ if ((input.mode === 'apply' || input.mode === 'pop') && /conflict/i.test(errorMessage)) {
150
+ // This case might be caught above, but double-check here
151
+ return { success: false, mode: input.mode, message: `Failed to ${input.mode} stash '${input.stashRef}' due to conflicts. Resolve conflicts manually.`, error: errorMessage, conflicts: true };
152
+ }
153
+ // Return structured failure for other git errors
154
+ return {
155
+ success: false,
156
+ mode: input.mode,
157
+ message: `Git stash ${input.mode} failed for path: ${targetPath}.`,
158
+ error: errorMessage
159
+ };
160
+ }
161
+ }
@@ -0,0 +1,84 @@
1
+ import { logger } from '../../../utils/logger.js';
2
+ import { ErrorHandler } from '../../../utils/errorHandler.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 { gitStashLogic, GitStashBaseSchema } from './logic.js';
7
+ import { BaseErrorCode } from '../../../types-global/errors.js';
8
+ let _getWorkingDirectory;
9
+ let _getSessionId;
10
+ /**
11
+ * Initializes the state accessors needed by the git_stash 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 initializeGitStashStateAccessors(getWdFn, getSidFn) {
16
+ _getWorkingDirectory = getWdFn;
17
+ _getSessionId = getSidFn;
18
+ logger.info('State accessors initialized for git_stash tool registration.');
19
+ }
20
+ const TOOL_NAME = 'git_stash';
21
+ const TOOL_DESCRIPTION = 'Manages stashed changes in the working directory. Supports listing stashes, applying/popping specific stashes (with conflict detection), dropping stashes, and saving current changes to a new stash with an optional message. Returns results as a JSON object.';
22
+ /**
23
+ * Registers the git_stash 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 registerGitStashTool = async (server) => {
30
+ if (!_getWorkingDirectory || !_getSessionId) {
31
+ throw new Error('State accessors for git_stash must be initialized before registration.');
32
+ }
33
+ const operation = 'registerGitStashTool';
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, GitStashBaseSchema.shape, // Use the shape from the BASE schema
39
+ // Let TypeScript infer handler argument types.
40
+ // The SDK validates against the full GitStashInputSchema 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 GitStashResult object
58
+ const stashResult = await gitStashLogic(toolInput, logicContext);
59
+ // Format the result as a JSON string within TextContent
60
+ const resultContent = {
61
+ type: 'text',
62
+ text: JSON.stringify(stashResult, null, 2), // Pretty-print JSON
63
+ contentType: 'application/json',
64
+ };
65
+ // Log based on the success flag in the result
66
+ if (stashResult.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: ${stashResult.message}`, { ...logicContext, errorDetails: stashResult.error, conflicts: stashResult.conflicts });
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,7 @@
1
+ /**
2
+ * @fileoverview Barrel file for the gitStatus tool.
3
+ * Exports the registration function and state accessor initialization function.
4
+ */
5
+ export { registerGitStatusTool, initializeGitStatusStateAccessors } from './registration.js';
6
+ // Export types if needed elsewhere, e.g.:
7
+ // export type { GitStatusInput, GitStatusResult } from './logic.js';