@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,78 @@
1
+ import { ErrorHandler } from '../../../utils/errorHandler.js';
2
+ import { logger } from '../../../utils/logger.js';
3
+ import { requestContextService } from '../../../utils/requestContext.js';
4
+ import { GitCheckoutInputSchema, checkoutGit } from './logic.js';
5
+ import { BaseErrorCode } from '../../../types-global/errors.js';
6
+ let _getWorkingDirectory;
7
+ let _getSessionId;
8
+ /**
9
+ * Initializes the state accessors needed by the tool registration.
10
+ * This should be called once during server setup.
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 initializeGitCheckoutStateAccessors(getWdFn, getSidFn) {
15
+ _getWorkingDirectory = getWdFn;
16
+ _getSessionId = getSidFn;
17
+ logger.info('State accessors initialized for git_checkout tool registration.');
18
+ }
19
+ const TOOL_NAME = 'git_checkout';
20
+ const TOOL_DESCRIPTION = "Switches branches or restores working tree files. Can checkout branches, commits, tags, or specific file paths. Supports creating new branches and forcing checkout.";
21
+ /**
22
+ * Registers the git_checkout tool with the MCP server.
23
+ *
24
+ * @param {McpServer} server - The MCP server instance.
25
+ * @throws {Error} If state accessors are not initialized.
26
+ */
27
+ export async function registerGitCheckoutTool(server) {
28
+ if (!_getWorkingDirectory || !_getSessionId) {
29
+ throw new Error('State accessors for git_checkout must be initialized before registration.');
30
+ }
31
+ const operation = 'registerGitCheckoutTool';
32
+ const context = requestContextService.createRequestContext({ operation });
33
+ await ErrorHandler.tryCatch(async () => {
34
+ server.tool(TOOL_NAME, TOOL_DESCRIPTION, GitCheckoutInputSchema.shape, // Provide the Zod schema shape
35
+ async (validatedArgs, callContext) => {
36
+ const toolOperation = 'tool:git_checkout';
37
+ const requestContext = requestContextService.createRequestContext({ operation: toolOperation, parentContext: callContext });
38
+ const sessionId = _getSessionId(requestContext);
39
+ const getWorkingDirectoryForSession = () => {
40
+ return _getWorkingDirectory(sessionId);
41
+ };
42
+ const logicContext = {
43
+ ...requestContext,
44
+ sessionId: sessionId,
45
+ getWorkingDirectory: getWorkingDirectoryForSession,
46
+ };
47
+ logger.info(`Executing tool: ${TOOL_NAME}`, logicContext);
48
+ return await ErrorHandler.tryCatch(async () => {
49
+ // Call the core logic function
50
+ const checkoutResult = await checkoutGit(validatedArgs, logicContext);
51
+ // Format the result as a JSON string within TextContent
52
+ const resultContent = {
53
+ type: 'text',
54
+ text: JSON.stringify(checkoutResult, null, 2), // Pretty-print JSON
55
+ contentType: 'application/json',
56
+ };
57
+ // Log based on the success flag in the result
58
+ if (checkoutResult.success) {
59
+ logger.info(`Tool ${TOOL_NAME} executed successfully: ${checkoutResult.message}`, logicContext);
60
+ }
61
+ else {
62
+ // Log non-fatal conditions like conflicts differently if needed
63
+ logger.info(`Tool ${TOOL_NAME} completed with status: ${checkoutResult.message}`, logicContext);
64
+ }
65
+ // Even if success is false (e.g., due to conflicts reported by logic),
66
+ // it's not a tool execution error unless the logic threw an McpError.
67
+ return { content: [resultContent] };
68
+ }, {
69
+ operation: toolOperation,
70
+ context: logicContext,
71
+ input: validatedArgs,
72
+ errorCode: BaseErrorCode.INTERNAL_ERROR, // Default if unexpected error in logic
73
+ });
74
+ });
75
+ logger.info(`Tool registered: ${TOOL_NAME}`, context);
76
+ }, { operation, context, critical: true });
77
+ }
78
+ ;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @fileoverview Barrel file for the git_cherry_pick tool.
3
+ * Exports the registration function and state accessor initialization function.
4
+ */
5
+ export { registerGitCherryPickTool, initializeGitCherryPickStateAccessors } from './registration.js';
6
+ // Export types if needed elsewhere, e.g.:
7
+ // export type { GitCherryPickInput, GitCherryPickResult } from './logic.js';
@@ -0,0 +1,115 @@
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_cherry-pick tool using Zod
9
+ export const GitCherryPickInputSchema = 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
+ commitRef: z.string().min(1).describe("The commit reference(s) to cherry-pick (e.g., 'hash1', 'hash1..hash3', 'branchName~3..branchName')."),
12
+ mainline: z.number().int().min(1).optional().describe("Specify the parent number (starting from 1) when cherry-picking a merge commit."),
13
+ strategy: z.enum(['recursive', 'resolve', 'ours', 'theirs', 'octopus', 'subtree']).optional().describe("Specifies the merge strategy to use."),
14
+ noCommit: z.boolean().default(false).describe("Apply the changes but do not create a commit."),
15
+ signoff: z.boolean().default(false).describe("Add a Signed-off-by line to the commit message."),
16
+ // Add options for conflict handling? (e.g., --continue, --abort, --skip) - Maybe separate tool or mode?
17
+ });
18
+ /**
19
+ * Executes the 'git cherry-pick' command.
20
+ *
21
+ * @param {GitCherryPickInput} input - The validated input object.
22
+ * @param {RequestContext} context - The request context for logging and error handling.
23
+ * @returns {Promise<GitCherryPickResult>} A promise that resolves with the structured result.
24
+ * @throws {McpError} Throws an McpError for path resolution/validation failures or unexpected errors.
25
+ */
26
+ export async function gitCherryPickLogic(input, context) {
27
+ const operation = 'gitCherryPickLogic';
28
+ logger.debug(`Executing ${operation}`, { ...context, input });
29
+ let targetPath;
30
+ try {
31
+ // Resolve and sanitize the target path
32
+ const workingDir = context.getWorkingDirectory();
33
+ targetPath = (input.path && input.path !== '.')
34
+ ? input.path
35
+ : workingDir ?? '.';
36
+ if (targetPath === '.' && !workingDir) {
37
+ logger.warning("Executing git cherry-pick in server's CWD as no path provided and no session WD set.", { ...context, operation });
38
+ targetPath = process.cwd();
39
+ }
40
+ else if (targetPath === '.' && workingDir) {
41
+ targetPath = workingDir;
42
+ logger.debug(`Using session working directory: ${targetPath}`, { ...context, operation, sessionId: context.sessionId });
43
+ }
44
+ else {
45
+ logger.debug(`Using provided path: ${targetPath}`, { ...context, operation });
46
+ }
47
+ targetPath = sanitization.sanitizePath(targetPath);
48
+ logger.debug('Sanitized path', { ...context, operation, sanitizedPath: targetPath });
49
+ }
50
+ catch (error) {
51
+ logger.error('Path resolution or sanitization failed', { ...context, operation, error });
52
+ if (error instanceof McpError)
53
+ throw error;
54
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
55
+ }
56
+ try {
57
+ let command = `git -C "${targetPath}" cherry-pick`;
58
+ if (input.mainline)
59
+ command += ` -m ${input.mainline}`;
60
+ if (input.strategy)
61
+ command += ` -X${input.strategy}`; // Note: -X for strategy options
62
+ if (input.noCommit)
63
+ command += ' --no-commit';
64
+ if (input.signoff)
65
+ command += ' --signoff';
66
+ // Add the commit reference(s) - ensure it's treated as a single argument potentially containing special chars like '..'
67
+ command += ` "${input.commitRef.replace(/"/g, '\\"')}"`;
68
+ logger.debug(`Executing command: ${command}`, { ...context, operation });
69
+ try {
70
+ const { stdout, stderr } = await execAsync(command);
71
+ // Check stdout/stderr for conflict messages, although exit code 0 usually means success
72
+ const output = stdout + stderr;
73
+ const conflicts = /conflict/i.test(output);
74
+ const commitCreated = !input.noCommit && !conflicts;
75
+ const message = conflicts
76
+ ? `Cherry-pick resulted in conflicts for commit(s) '${input.commitRef}'. Manual resolution required.`
77
+ : `Successfully cherry-picked commit(s) '${input.commitRef}'.` + (commitCreated ? ' New commit created.' : (input.noCommit ? ' Changes staged.' : ''));
78
+ logger.info(message, { ...context, operation, path: targetPath, conflicts, commitCreated });
79
+ return { success: true, message, commitCreated, conflicts };
80
+ }
81
+ catch (cherryPickError) {
82
+ const errorMessage = cherryPickError.stderr || cherryPickError.stdout || cherryPickError.message || '';
83
+ if (/conflict/i.test(errorMessage)) {
84
+ logger.warning(`Cherry-pick failed due to conflicts for commit(s) '${input.commitRef}'.`, { ...context, operation, path: targetPath, error: errorMessage });
85
+ return { success: false, message: `Failed to cherry-pick commit(s) '${input.commitRef}' due to conflicts. Resolve conflicts manually and potentially use 'git cherry-pick --continue' or '--abort'.`, error: errorMessage, conflicts: true };
86
+ }
87
+ // Rethrow other errors to be caught by the outer try-catch
88
+ throw cherryPickError;
89
+ }
90
+ }
91
+ catch (error) {
92
+ const errorMessage = error.stderr || error.stdout || error.message || '';
93
+ logger.error(`Failed to execute git cherry-pick command`, { ...context, operation, path: targetPath, error: errorMessage, stderr: error.stderr, stdout: error.stdout });
94
+ // Specific error handling
95
+ if (errorMessage.toLowerCase().includes('not a git repository')) {
96
+ throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`, { context, operation, originalError: error });
97
+ }
98
+ if (/bad revision/i.test(errorMessage)) {
99
+ return { success: false, message: `Failed to cherry-pick: Invalid commit reference '${input.commitRef}'.`, error: errorMessage };
100
+ }
101
+ if (/after resolving the conflicts/i.test(errorMessage)) {
102
+ // This might indicate a previous conflict state
103
+ return { success: false, message: `Failed to cherry-pick: Unresolved conflicts from a previous operation exist. Resolve conflicts and use 'git cherry-pick --continue' or '--abort'.`, error: errorMessage, conflicts: true };
104
+ }
105
+ if (/your local changes would be overwritten/i.test(errorMessage)) {
106
+ return { success: false, message: `Failed to cherry-pick: Your local changes to tracked files would be overwritten. Please commit or stash them.`, error: errorMessage };
107
+ }
108
+ // Return structured failure for other git errors
109
+ return {
110
+ success: false,
111
+ message: `Git cherry-pick failed for path: ${targetPath}.`,
112
+ error: errorMessage
113
+ };
114
+ }
115
+ }
@@ -0,0 +1,69 @@
1
+ import { logger } from '../../../utils/logger.js';
2
+ import { ErrorHandler } from '../../../utils/errorHandler.js';
3
+ import { requestContextService } from '../../../utils/requestContext.js';
4
+ import { gitCherryPickLogic, GitCherryPickInputSchema } from './logic.js';
5
+ import { BaseErrorCode } from '../../../types-global/errors.js';
6
+ let _getWorkingDirectory;
7
+ let _getSessionId;
8
+ /**
9
+ * Initializes the state accessors needed by the git_cherry_pick tool registration.
10
+ * @param getWdFn - Function to get the working directory for a session.
11
+ * @param getSidFn - Function to get the session ID from context.
12
+ */
13
+ export function initializeGitCherryPickStateAccessors(getWdFn, getSidFn) {
14
+ _getWorkingDirectory = getWdFn;
15
+ _getSessionId = getSidFn;
16
+ logger.info('State accessors initialized for git_cherry_pick tool registration.');
17
+ }
18
+ const TOOL_NAME = 'git_cherry_pick';
19
+ const TOOL_DESCRIPTION = 'Applies the changes introduced by existing commits. Supports picking single commits or ranges, handling merge commits, and options like --no-commit and --signoff. Returns results as a JSON object, indicating success, failure, or conflicts.';
20
+ /**
21
+ * Registers the git_cherry_pick tool with the MCP server.
22
+ *
23
+ * @param {McpServer} server - The McpServer instance to register the tool with.
24
+ * @returns {Promise<void>}
25
+ * @throws {Error} If registration fails or state accessors are not initialized.
26
+ */
27
+ export const registerGitCherryPickTool = async (server) => {
28
+ if (!_getWorkingDirectory || !_getSessionId) {
29
+ throw new Error('State accessors for git_cherry_pick must be initialized before registration.');
30
+ }
31
+ const operation = 'registerGitCherryPickTool';
32
+ const context = requestContextService.createRequestContext({ operation });
33
+ await ErrorHandler.tryCatch(async () => {
34
+ server.tool(TOOL_NAME, TOOL_DESCRIPTION, GitCherryPickInputSchema.shape, async (validatedArgs, callContext) => {
35
+ const toolInput = validatedArgs;
36
+ const toolOperation = `tool:${TOOL_NAME}`;
37
+ const requestContext = requestContextService.createRequestContext({ operation: toolOperation, parentContext: callContext });
38
+ const sessionId = _getSessionId(requestContext);
39
+ const getWorkingDirectoryForSession = () => _getWorkingDirectory(sessionId);
40
+ const logicContext = {
41
+ ...requestContext,
42
+ sessionId: sessionId,
43
+ getWorkingDirectory: getWorkingDirectoryForSession,
44
+ };
45
+ logger.info(`Executing tool: ${TOOL_NAME}`, logicContext);
46
+ return await ErrorHandler.tryCatch(async () => {
47
+ const cherryPickResult = await gitCherryPickLogic(toolInput, logicContext);
48
+ const resultContent = {
49
+ type: 'text',
50
+ text: JSON.stringify(cherryPickResult, null, 2), // Pretty-print JSON
51
+ contentType: 'application/json',
52
+ };
53
+ if (cherryPickResult.success) {
54
+ logger.info(`Tool ${TOOL_NAME} executed successfully (Conflicts: ${!!cherryPickResult.conflicts}), returning JSON`, logicContext);
55
+ }
56
+ else {
57
+ logger.warning(`Tool ${TOOL_NAME} failed: ${cherryPickResult.message}`, { ...logicContext, errorDetails: cherryPickResult.error, conflicts: cherryPickResult.conflicts });
58
+ }
59
+ return { content: [resultContent] };
60
+ }, {
61
+ operation: toolOperation,
62
+ context: logicContext,
63
+ input: validatedArgs,
64
+ errorCode: BaseErrorCode.INTERNAL_ERROR,
65
+ });
66
+ });
67
+ logger.info(`Tool registered: ${TOOL_NAME}`, context);
68
+ }, { operation, context, critical: true });
69
+ };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @fileoverview Barrel file for the git_clean tool.
3
+ * Exports the registration function and state accessor initialization function.
4
+ */
5
+ export { registerGitCleanTool, initializeGitCleanStateAccessors } from './registration.js';
6
+ // Export types if needed elsewhere, e.g.:
7
+ // export type { GitCleanInput, GitCleanResult } from './logic.js';
@@ -0,0 +1,110 @@
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_clean tool using Zod
9
+ // No refinements needed here, but the 'force' check is critical in the logic
10
+ export const GitCleanInputSchema = 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
+ force: z.boolean().describe("REQUIRED confirmation to run the command. Must be explicitly set to true to perform the clean operation. If false or omitted, the command will not run."),
13
+ dryRun: z.boolean().default(false).describe("Show what would be deleted without actually deleting (-n flag)."),
14
+ directories: z.boolean().default(false).describe("Remove untracked directories in addition to files (-d flag)."),
15
+ ignored: z.boolean().default(false).describe("Remove ignored files as well (-x flag). Use with extreme caution."),
16
+ // exclude: z.string().optional().describe("Exclude files matching pattern (-e <pattern>)"), // Consider adding later
17
+ });
18
+ /**
19
+ * Executes the 'git clean' command to remove untracked files.
20
+ * CRITICAL: Requires the 'force' parameter to be explicitly true.
21
+ *
22
+ * @param {GitCleanInput} input - The validated input object.
23
+ * @param {RequestContext} context - The request context for logging and error handling.
24
+ * @returns {Promise<GitCleanResult>} A promise that resolves with the structured result.
25
+ * @throws {McpError} Throws an McpError for path/validation failures, if force=false, or unexpected errors.
26
+ */
27
+ export async function gitCleanLogic(input, context) {
28
+ const operation = 'gitCleanLogic';
29
+ logger.debug(`Executing ${operation}`, { ...context, input });
30
+ // --- CRITICAL SAFETY CHECK ---
31
+ if (!input.force) {
32
+ logger.error("Attempted to run git clean without force=true.", { ...context, operation });
33
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, "Operation aborted: 'force' parameter must be explicitly set to true to execute 'git clean'. This is a destructive command.", { context, operation });
34
+ }
35
+ // Log that the force check passed
36
+ logger.warning("Executing 'git clean' with force=true. This is a destructive operation.", { ...context, operation });
37
+ let targetPath;
38
+ try {
39
+ // Resolve and sanitize the target path
40
+ const workingDir = context.getWorkingDirectory();
41
+ targetPath = (input.path && input.path !== '.')
42
+ ? input.path
43
+ : workingDir ?? '.';
44
+ if (targetPath === '.' && !workingDir) {
45
+ logger.warning("Executing git clean in server's CWD as no path provided and no session WD set.", { ...context, operation });
46
+ targetPath = process.cwd();
47
+ }
48
+ else if (targetPath === '.' && workingDir) {
49
+ targetPath = workingDir;
50
+ logger.debug(`Using session working directory: ${targetPath}`, { ...context, operation, sessionId: context.sessionId });
51
+ }
52
+ else {
53
+ logger.debug(`Using provided path: ${targetPath}`, { ...context, operation });
54
+ }
55
+ targetPath = sanitization.sanitizePath(targetPath);
56
+ logger.debug('Sanitized path', { ...context, operation, sanitizedPath: targetPath });
57
+ }
58
+ catch (error) {
59
+ logger.error('Path resolution or sanitization failed', { ...context, operation, error });
60
+ if (error instanceof McpError)
61
+ throw error;
62
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
63
+ }
64
+ try {
65
+ // Construct the command
66
+ // Force (-f) is always added because the logic checks input.force
67
+ let command = `git -C "${targetPath}" clean -f`;
68
+ if (input.dryRun) {
69
+ command += ' -n';
70
+ }
71
+ if (input.directories) {
72
+ command += ' -d';
73
+ }
74
+ if (input.ignored) {
75
+ command += ' -x';
76
+ }
77
+ logger.debug(`Executing command: ${command}`, { ...context, operation });
78
+ const { stdout, stderr } = await execAsync(command);
79
+ if (stderr) {
80
+ // Log stderr as warning, as git clean might report non-fatal issues here
81
+ logger.warning(`Git clean command produced stderr`, { ...context, operation, stderr });
82
+ }
83
+ // Parse stdout to list affected files
84
+ const filesAffected = stdout.trim().split('\n')
85
+ .map(line => line.replace(/^Would remove /i, '').replace(/^Removing /i, '').trim()) // Clean up prefixes
86
+ .filter(file => file); // Remove empty lines
87
+ const message = input.dryRun
88
+ ? `Dry run complete. Files that would be removed: ${filesAffected.length}`
89
+ : `Clean operation complete. Files removed: ${filesAffected.length}`;
90
+ logger.info(`${operation} executed successfully`, { ...context, operation, path: targetPath, dryRun: input.dryRun, filesAffectedCount: filesAffected.length });
91
+ return { success: true, message, filesAffected, dryRun: input.dryRun };
92
+ }
93
+ catch (error) {
94
+ const errorMessage = error.stderr || error.message || '';
95
+ logger.error(`Failed to execute git clean command`, { ...context, operation, path: targetPath, error: errorMessage, stderr: error.stderr, stdout: error.stdout });
96
+ // Specific error handling
97
+ if (errorMessage.toLowerCase().includes('not a git repository')) {
98
+ throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`, { context, operation, originalError: error });
99
+ }
100
+ // Git clean usually doesn't fail with specific messages like others,
101
+ // but returns non-zero exit code on general failure.
102
+ // Return structured failure for other git errors
103
+ return {
104
+ success: false,
105
+ message: `Git clean failed for path: ${targetPath}.`,
106
+ error: errorMessage,
107
+ dryRun: input.dryRun // Still report dryRun status
108
+ };
109
+ }
110
+ }
@@ -0,0 +1,98 @@
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 { gitCleanLogic, GitCleanInputSchema } 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_clean 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 initializeGitCleanStateAccessors(getWdFn, getSidFn) {
15
+ _getWorkingDirectory = getWdFn;
16
+ _getSessionId = getSidFn;
17
+ logger.info('State accessors initialized for git_clean tool registration.');
18
+ }
19
+ const TOOL_NAME = 'git_clean';
20
+ const TOOL_DESCRIPTION = 'Removes untracked files from the working directory. Supports dry runs, removing directories, and removing ignored files. CRITICAL: Requires explicit `force: true` parameter for safety as this is a destructive operation. Returns results as a JSON object.';
21
+ /**
22
+ * Registers the git_clean 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 registerGitCleanTool = async (server) => {
29
+ if (!_getWorkingDirectory || !_getSessionId) {
30
+ throw new Error('State accessors for git_clean must be initialized before registration.');
31
+ }
32
+ const operation = 'registerGitCleanTool';
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, GitCleanInputSchema.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
+ // --- SAFETY CHECK (Redundant but good practice) ---
44
+ // The core logic already checks this, but adding a check here ensures
45
+ // the intent is clear even before calling the logic.
46
+ if (!toolInput.force) {
47
+ logger.error(`Tool ${TOOL_NAME} called without force=true. Aborting.`, requestContext);
48
+ // Return a structured error via CallToolResult
49
+ const errorContent = {
50
+ type: 'text',
51
+ text: JSON.stringify({
52
+ success: false,
53
+ message: "Operation aborted: 'force' parameter must be explicitly set to true to execute 'git clean'.",
54
+ dryRun: toolInput.dryRun // Include dryRun status
55
+ }, null, 2),
56
+ contentType: 'application/json',
57
+ };
58
+ return { content: [errorContent], isError: true }; // Indicate it's an error result
59
+ }
60
+ const sessionId = _getSessionId(requestContext);
61
+ const getWorkingDirectoryForSession = () => {
62
+ return _getWorkingDirectory(sessionId);
63
+ };
64
+ const logicContext = {
65
+ ...requestContext,
66
+ sessionId: sessionId,
67
+ getWorkingDirectory: getWorkingDirectoryForSession,
68
+ };
69
+ logger.info(`Executing tool: ${TOOL_NAME}`, logicContext);
70
+ return await ErrorHandler.tryCatch(async () => {
71
+ // Call the core logic function which returns a GitCleanResult object
72
+ const cleanResult = await gitCleanLogic(toolInput, logicContext);
73
+ // Format the result as JSON within TextContent
74
+ const resultContent = {
75
+ type: 'text',
76
+ text: JSON.stringify(cleanResult, null, 2),
77
+ contentType: 'application/json',
78
+ };
79
+ // Log based on the success flag in the result
80
+ if (cleanResult.success) {
81
+ logger.info(`Tool ${TOOL_NAME} executed successfully (DryRun: ${cleanResult.dryRun})`, logicContext);
82
+ }
83
+ else {
84
+ // Log specific failure message from the result
85
+ logger.warning(`Tool ${TOOL_NAME} failed: ${cleanResult.message}`, { ...logicContext, errorDetails: cleanResult.error });
86
+ }
87
+ // Return the result, whether success or structured failure
88
+ return { content: [resultContent] };
89
+ }, {
90
+ operation: toolOperation,
91
+ context: logicContext,
92
+ input: validatedArgs, // Log the raw validated args
93
+ errorCode: BaseErrorCode.INTERNAL_ERROR, // Default if unexpected error occurs in logic/wrapper
94
+ });
95
+ });
96
+ logger.info(`Tool registered: ${TOOL_NAME}`, context);
97
+ }, { operation, context, critical: true }); // Mark registration as critical
98
+ };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @fileoverview Barrel file for the git_clear_working_dir tool.
3
+ * Exports the registration function and related components.
4
+ */
5
+ export { registerGitClearWorkingDirTool, initializeGitClearWorkingDirStateAccessors } from './registration.js';
6
+ // Export types if needed elsewhere, e.g.:
7
+ // export type { GitClearWorkingDirInput, GitClearWorkingDirResult } from './logic.js';
@@ -0,0 +1,35 @@
1
+ import { z } from 'zod';
2
+ import { logger } from '../../../utils/logger.js';
3
+ import { McpError, BaseErrorCode } from '../../../types-global/errors.js';
4
+ // Define the Zod schema for input validation (no arguments needed)
5
+ export const GitClearWorkingDirInputSchema = z.object({});
6
+ /**
7
+ * Logic for the git_clear_working_dir tool.
8
+ * Clears the global working directory setting for the current session.
9
+ *
10
+ * @param {GitClearWorkingDirInput} input - The validated input arguments (empty object).
11
+ * @param {RequestContext} context - The request context, containing session ID and the clear function.
12
+ * @returns {Promise<GitClearWorkingDirResult>} The result of the operation.
13
+ * @throws {McpError} Throws McpError for operational errors.
14
+ */
15
+ export async function gitClearWorkingDirLogic(input, context // Assuming context provides session info and clearer
16
+ ) {
17
+ const operation = 'gitClearWorkingDirLogic';
18
+ logger.info('Executing git_clear_working_dir logic', { ...context, operation });
19
+ // --- Update Session State ---
20
+ // This part needs access to the session state mechanism defined in server.ts
21
+ // We assume the context provides a way to clear the working directory for the current session.
22
+ try {
23
+ context.clearWorkingDirectory();
24
+ logger.info(`Working directory cleared for session ${context.sessionId || 'stdio'}`, { ...context, operation });
25
+ }
26
+ catch (error) {
27
+ logger.error('Failed to clear working directory in session state', error, { ...context, operation });
28
+ // This indicates an internal logic error in how state is passed/managed.
29
+ throw new McpError(BaseErrorCode.INTERNAL_ERROR, 'Failed to update session state.', { context, operation });
30
+ }
31
+ return {
32
+ success: true,
33
+ message: 'Global working directory setting cleared.',
34
+ };
35
+ }
@@ -0,0 +1,73 @@
1
+ import { ErrorHandler } from '../../../utils/errorHandler.js';
2
+ import { logger } from '../../../utils/logger.js';
3
+ import { requestContextService } from '../../../utils/requestContext.js';
4
+ import { GitClearWorkingDirInputSchema, gitClearWorkingDirLogic } from './logic.js';
5
+ import { BaseErrorCode } from '../../../types-global/errors.js';
6
+ let _clearWorkingDirectory;
7
+ let _getSessionId;
8
+ /**
9
+ * Initializes the state accessors needed by the tool registration.
10
+ * This should be called once during server setup.
11
+ * @param clearFn - Function to clear the working directory for a session.
12
+ * @param getFn - Function to get the session ID from context.
13
+ */
14
+ export function initializeGitClearWorkingDirStateAccessors(clearFn, getFn) {
15
+ _clearWorkingDirectory = clearFn;
16
+ _getSessionId = getFn; // Can reuse the getter from the set tool
17
+ logger.info('State accessors initialized for git_clear_working_dir tool registration.');
18
+ }
19
+ const TOOL_NAME = 'git_clear_working_dir';
20
+ const TOOL_DESCRIPTION = "Clears the session-specific working directory previously set by `git_set_working_dir`. Subsequent Git tool calls in this session will require an explicit `path` parameter or will default to the server's current working directory. Returns the result as a JSON object.";
21
+ /**
22
+ * Registers the git_clear_working_dir tool with the MCP server.
23
+ *
24
+ * @param {McpServer} server - The MCP server instance.
25
+ * @throws {Error} If state accessors are not initialized.
26
+ */
27
+ export async function registerGitClearWorkingDirTool(server) {
28
+ if (!_clearWorkingDirectory || !_getSessionId) {
29
+ throw new Error('State accessors for git_clear_working_dir must be initialized before registration.');
30
+ }
31
+ try {
32
+ server.tool(TOOL_NAME, TOOL_DESCRIPTION, GitClearWorkingDirInputSchema.shape, // Empty shape
33
+ async (validatedArgs, callContext) => {
34
+ const operation = 'tool:git_clear_working_dir';
35
+ const requestContext = requestContextService.createRequestContext({ operation, parentContext: callContext });
36
+ const sessionId = _getSessionId(requestContext);
37
+ // Define the session-specific clear function
38
+ const clearWorkingDirectoryForSession = () => {
39
+ _clearWorkingDirectory(sessionId);
40
+ };
41
+ const logicContext = {
42
+ ...requestContext,
43
+ sessionId: sessionId,
44
+ clearWorkingDirectory: clearWorkingDirectoryForSession,
45
+ };
46
+ return await ErrorHandler.tryCatch(async () => {
47
+ // Call the core logic function
48
+ const result = await gitClearWorkingDirLogic(validatedArgs, logicContext);
49
+ // Format the successful result
50
+ const responseContent = {
51
+ type: 'text',
52
+ text: JSON.stringify(result, null, 2),
53
+ contentType: 'application/json',
54
+ };
55
+ logger.info(`Tool ${TOOL_NAME} executed successfully`, { ...logicContext, result });
56
+ return { content: [responseContent] };
57
+ }, {
58
+ operation,
59
+ context: logicContext,
60
+ input: validatedArgs,
61
+ errorCode: BaseErrorCode.INTERNAL_ERROR,
62
+ });
63
+ });
64
+ logger.info(`Tool registered: ${TOOL_NAME}`);
65
+ }
66
+ catch (error) {
67
+ logger.error(`Failed to register tool: ${TOOL_NAME}`, {
68
+ error: error instanceof Error ? error.message : String(error),
69
+ stack: error instanceof Error ? error.stack : undefined,
70
+ });
71
+ throw error;
72
+ }
73
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @fileoverview Barrel file for the git_clone tool.
3
+ * Exports the registration function.
4
+ */
5
+ export { registerGitCloneTool } from './registration.js';
6
+ // Export types if needed elsewhere, e.g.:
7
+ // export type { GitCloneInput, GitCloneResult } from './logic.js';