@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,116 @@
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_fetch tool using Zod
9
+ export const GitFetchInputSchema = z.object({
10
+ path: z.string().min(1).optional().default('.').describe("Path to the Git repository. Defaults to the session's working directory if set."),
11
+ remote: z.string().optional().describe("The remote repository to fetch from (e.g., 'origin'). If omitted, fetches from 'origin' or the default configured remote."),
12
+ prune: z.boolean().optional().default(false).describe("Before fetching, remove any remote-tracking references that no longer exist on the remote."),
13
+ tags: z.boolean().optional().default(false).describe("Fetch all tags from the remote (in addition to whatever else is fetched)."),
14
+ all: z.boolean().optional().default(false).describe("Fetch all remotes."),
15
+ // Add options like --depth, specific refspecs if needed
16
+ });
17
+ /**
18
+ * Executes the 'git fetch' command and returns structured JSON output.
19
+ *
20
+ * @param {GitFetchInput} input - The validated input object.
21
+ * @param {RequestContext} context - The request context for logging and error handling.
22
+ * @returns {Promise<GitFetchResult>} A promise that resolves with the structured fetch result.
23
+ * @throws {McpError} Throws an McpError if path resolution, validation, or the git command fails unexpectedly.
24
+ */
25
+ export async function fetchGitRemote(input, context) {
26
+ const operation = 'fetchGitRemote';
27
+ logger.debug(`Executing ${operation}`, { ...context, input });
28
+ let targetPath;
29
+ try {
30
+ // Resolve and sanitize the target path
31
+ if (input.path && input.path !== '.') {
32
+ targetPath = input.path;
33
+ }
34
+ else {
35
+ const workingDir = context.getWorkingDirectory();
36
+ if (!workingDir) {
37
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, "No path provided and no working directory set for the session.", { context, operation });
38
+ }
39
+ targetPath = workingDir;
40
+ }
41
+ targetPath = sanitization.sanitizePath(targetPath);
42
+ logger.debug('Sanitized path', { ...context, operation, sanitizedPath: targetPath });
43
+ }
44
+ catch (error) {
45
+ logger.error('Path resolution or sanitization failed', { ...context, operation, error });
46
+ if (error instanceof McpError)
47
+ throw error;
48
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
49
+ }
50
+ // Basic sanitization for remote name
51
+ const safeRemote = input.remote?.replace(/[^a-zA-Z0-9_.\-/]/g, '');
52
+ try {
53
+ // Construct the git fetch command
54
+ let command = `git -C "${targetPath}" fetch`;
55
+ if (input.prune) {
56
+ command += ' --prune';
57
+ }
58
+ if (input.tags) {
59
+ command += ' --tags';
60
+ }
61
+ if (input.all) {
62
+ command += ' --all';
63
+ }
64
+ else if (safeRemote) {
65
+ command += ` ${safeRemote}`; // Fetch specific remote if 'all' is not used
66
+ }
67
+ // If neither 'all' nor 'remote' is specified, git fetch defaults to 'origin' or configured upstream.
68
+ logger.debug(`Executing command: ${command}`, { ...context, operation });
69
+ // Execute command. Fetch output is primarily on stderr.
70
+ const { stdout, stderr } = await execAsync(command);
71
+ logger.info(`Git fetch stdout: ${stdout}`, { ...context, operation }); // stdout is usually empty
72
+ logger.info(`Git fetch stderr: ${stderr}`, { ...context, operation }); // stderr contains fetch details
73
+ // Analyze stderr for success/summary
74
+ let message = stderr.trim() || 'Fetch command executed.'; // Use stderr as the primary message
75
+ let summary = undefined;
76
+ // Check for common patterns in stderr
77
+ if (stderr.includes('Updating') || stderr.includes('->') || stderr.includes('new tag') || stderr.includes('new branch')) {
78
+ message = 'Fetch successful.';
79
+ summary = stderr.trim(); // Use the full stderr as summary
80
+ }
81
+ else if (stderr.trim() === '') {
82
+ // Sometimes fetch completes successfully with no output if nothing changed
83
+ message = 'Fetch successful (no changes detected).';
84
+ }
85
+ else if (message.includes('fatal:')) {
86
+ // Should be caught by catch block, but double-check
87
+ logger.error(`Git fetch command indicated failure: ${message}`, { ...context, operation, stdout, stderr });
88
+ // Re-throw as an internal error if not caught below
89
+ throw new Error(`Fetch failed: ${message}`);
90
+ }
91
+ logger.info(`${operation} completed successfully. ${message}`, { ...context, operation, path: targetPath });
92
+ return { success: true, message, summary };
93
+ }
94
+ catch (error) {
95
+ logger.error(`Failed to execute git fetch command`, { ...context, operation, path: targetPath, error: error.message, stderr: error.stderr, stdout: error.stdout });
96
+ const errorMessage = error.stderr || error.stdout || error.message || '';
97
+ // Handle specific error cases
98
+ if (errorMessage.toLowerCase().includes('not a git repository')) {
99
+ throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`, { context, operation, originalError: error });
100
+ }
101
+ if (errorMessage.includes('resolve host') || errorMessage.includes('Could not read from remote repository') || errorMessage.includes('Connection timed out')) {
102
+ throw new McpError(BaseErrorCode.NETWORK_ERROR, `Failed to connect to remote repository '${input.remote || 'default'}'. Error: ${errorMessage}`, { context, operation, originalError: error });
103
+ }
104
+ if (errorMessage.includes('fatal: ') && errorMessage.includes('couldn\'t find remote ref')) {
105
+ throw new McpError(BaseErrorCode.NOT_FOUND, `Remote ref not found. Error: ${errorMessage}`, { context, operation, originalError: error });
106
+ }
107
+ if (errorMessage.includes('Authentication failed') || errorMessage.includes('Permission denied')) {
108
+ throw new McpError(BaseErrorCode.UNAUTHORIZED, `Authentication failed for remote repository '${input.remote || 'default'}'. Error: ${errorMessage}`, { context, operation, originalError: error });
109
+ }
110
+ if (errorMessage.includes('does not appear to be a git repository')) {
111
+ throw new McpError(BaseErrorCode.NOT_FOUND, `Remote '${input.remote || 'default'}' does not appear to be a git repository. Error: ${errorMessage}`, { context, operation, originalError: error });
112
+ }
113
+ // Generic internal error for other failures
114
+ throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to git fetch for path: ${targetPath}. Error: ${errorMessage}`, { context, operation, originalError: error });
115
+ }
116
+ }
@@ -0,0 +1,71 @@
1
+ import { ErrorHandler } from '../../../utils/errorHandler.js';
2
+ import { logger } from '../../../utils/logger.js';
3
+ import { requestContextService } from '../../../utils/requestContext.js';
4
+ import { GitFetchInputSchema, fetchGitRemote } 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 initializeGitFetchStateAccessors(getWdFn, getSidFn) {
15
+ _getWorkingDirectory = getWdFn;
16
+ _getSessionId = getSidFn;
17
+ logger.info('State accessors initialized for git_fetch tool registration.');
18
+ }
19
+ const TOOL_NAME = 'git_fetch';
20
+ const TOOL_DESCRIPTION = "Downloads objects and refs from one or more other repositories. Can fetch specific remotes or all, prune stale branches, and fetch tags.";
21
+ /**
22
+ * Registers the git_fetch 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 registerGitFetchTool(server) {
28
+ if (!_getWorkingDirectory || !_getSessionId) {
29
+ throw new Error('State accessors for git_fetch must be initialized before registration.');
30
+ }
31
+ const operation = 'registerGitFetchTool';
32
+ const context = requestContextService.createRequestContext({ operation });
33
+ await ErrorHandler.tryCatch(async () => {
34
+ server.tool(TOOL_NAME, TOOL_DESCRIPTION, GitFetchInputSchema.shape, // Provide the Zod schema shape
35
+ async (validatedArgs, callContext) => {
36
+ const toolOperation = 'tool:git_fetch';
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 fetchResult = await fetchGitRemote(validatedArgs, logicContext);
51
+ // Format the result as a JSON string within TextContent
52
+ const resultContent = {
53
+ type: 'text',
54
+ // Stringify the entire GitFetchResult object
55
+ text: JSON.stringify(fetchResult, null, 2), // Pretty-print JSON
56
+ contentType: 'application/json',
57
+ };
58
+ logger.info(`Tool ${TOOL_NAME} executed successfully: ${fetchResult.message}`, logicContext);
59
+ // Success is determined by the logic function and included in the result object
60
+ return { content: [resultContent] };
61
+ }, {
62
+ operation: toolOperation,
63
+ context: logicContext,
64
+ input: validatedArgs,
65
+ errorCode: BaseErrorCode.INTERNAL_ERROR, // Default if unexpected error in logic
66
+ });
67
+ });
68
+ logger.info(`Tool registered: ${TOOL_NAME}`, context);
69
+ }, { operation, context, critical: true });
70
+ }
71
+ ;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @fileoverview Barrel file for the git_init tool.
3
+ * Exports the registration function.
4
+ */
5
+ export { registerGitInitTool } from './registration.js';
6
+ // Export types if needed elsewhere, e.g.:
7
+ // export type { GitInitInput, GitInitResult } from './logic.js';
@@ -0,0 +1,117 @@
1
+ import { z } from 'zod';
2
+ import { promisify } from 'util';
3
+ import { exec } from 'child_process';
4
+ import fs from 'fs/promises';
5
+ import path from 'path';
6
+ import { logger } from '../../../utils/logger.js';
7
+ import { McpError, BaseErrorCode } from '../../../types-global/errors.js';
8
+ import { sanitization } from '../../../utils/sanitization.js';
9
+ const execAsync = promisify(exec);
10
+ // Define the input schema for the git_init tool using Zod
11
+ export const GitInitInputSchema = z.object({
12
+ path: z.string().min(1).describe("The absolute path where the new Git repository should be initialized."),
13
+ initialBranch: z.string().optional().describe("Optional name for the initial branch (e.g., 'main'). Uses Git's default if not specified."),
14
+ bare: z.boolean().default(false).describe("Create a bare repository (no working directory)."),
15
+ quiet: z.boolean().default(false).describe("Only print error and warning messages; all other output will be suppressed."),
16
+ });
17
+ /**
18
+ * Executes the 'git init' command to initialize a new Git repository.
19
+ *
20
+ * @param {GitInitInput} input - The validated input object.
21
+ * @param {RequestContext} context - The request context for logging and error handling.
22
+ * @returns {Promise<GitInitResult>} A promise that resolves with the structured init result.
23
+ * @throws {McpError} Throws an McpError if path validation fails or the git command fails unexpectedly.
24
+ */
25
+ export async function gitInitLogic(input, context) {
26
+ const operation = 'gitInitLogic';
27
+ logger.debug(`Executing ${operation}`, { ...context, input });
28
+ let targetPath;
29
+ try {
30
+ // Sanitize the provided absolute path
31
+ targetPath = sanitization.sanitizePath(input.path);
32
+ logger.debug('Sanitized path', { ...context, operation, sanitizedPath: targetPath });
33
+ // Ensure the target directory exists before trying to init inside it
34
+ // git init creates the directory if it doesn't exist, but we might want to ensure the parent exists
35
+ const parentDir = path.dirname(targetPath);
36
+ try {
37
+ await fs.access(parentDir, fs.constants.W_OK); // Check write access in parent
38
+ }
39
+ catch (accessError) {
40
+ logger.error(`Parent directory check failed for ${targetPath}`, { ...context, operation, error: accessError.message });
41
+ if (accessError.code === 'ENOENT') {
42
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Parent directory does not exist: ${parentDir}`, { context, operation });
43
+ }
44
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Cannot access parent directory: ${parentDir}. Error: ${accessError.message}`, { context, operation });
45
+ }
46
+ }
47
+ catch (error) {
48
+ logger.error('Path validation or sanitization failed', { ...context, operation, error });
49
+ if (error instanceof McpError)
50
+ throw error;
51
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
52
+ }
53
+ try {
54
+ // Construct the git init command
55
+ let command = `git init`;
56
+ if (input.quiet) {
57
+ command += ' --quiet';
58
+ }
59
+ if (input.bare) {
60
+ command += ' --bare';
61
+ }
62
+ if (input.initialBranch) {
63
+ // Use -b for modern Git versions, older might need --initial-branch=
64
+ // Sticking with -b as it's common now. Add quotes around branch name.
65
+ command += ` -b "${input.initialBranch.replace(/"/g, '\\"')}"`;
66
+ }
67
+ // Add the target directory path at the end
68
+ command += ` "${targetPath}"`;
69
+ logger.debug(`Executing command: ${command}`, { ...context, operation });
70
+ const { stdout, stderr } = await execAsync(command);
71
+ if (stderr && !input.quiet) {
72
+ // Log stderr as warning but proceed, as init might still succeed (e.g., reinitializing)
73
+ logger.warning(`Git init command produced stderr`, { ...context, operation, stderr });
74
+ }
75
+ if (stdout && !input.quiet) {
76
+ logger.info(`Git init command produced stdout`, { ...context, operation, stdout });
77
+ }
78
+ // Verify .git directory exists (or equivalent for bare repo)
79
+ const gitDirPath = input.bare ? targetPath : path.join(targetPath, '.git');
80
+ let gitDirExists = false;
81
+ try {
82
+ await fs.access(gitDirPath);
83
+ gitDirExists = true;
84
+ }
85
+ catch (e) {
86
+ logger.warning(`Could not verify existence of ${gitDirPath} after git init`, { ...context, operation });
87
+ }
88
+ const successMessage = stdout.trim() || `Initialized empty Git repository in ${targetPath}`;
89
+ logger.info(`${operation} executed successfully`, { ...context, operation, path: targetPath });
90
+ return {
91
+ success: true,
92
+ message: successMessage,
93
+ path: targetPath,
94
+ gitDirExists: gitDirExists
95
+ };
96
+ }
97
+ catch (error) {
98
+ const errorMessage = error.stderr || error.message || '';
99
+ logger.error(`Failed to execute git init command`, { ...context, operation, path: targetPath, error: errorMessage, stderr: error.stderr, stdout: error.stdout });
100
+ // Handle specific error cases
101
+ if (errorMessage.toLowerCase().includes('already exists') && errorMessage.toLowerCase().includes('git repository')) {
102
+ // Reinitializing is often okay, treat as success but mention it.
103
+ logger.info(`Repository already exists, reinitialized: ${targetPath}`, { ...context, operation });
104
+ return {
105
+ success: true, // Treat reinitialization as success
106
+ message: `Reinitialized existing Git repository in ${targetPath}`,
107
+ path: targetPath,
108
+ gitDirExists: true // Assume it exists if reinit message appears
109
+ };
110
+ }
111
+ if (errorMessage.toLowerCase().includes('permission denied')) {
112
+ throw new McpError(BaseErrorCode.FORBIDDEN, `Permission denied to initialize repository at: ${targetPath}. Error: ${errorMessage}`, { context, operation, originalError: error });
113
+ }
114
+ // Generic internal error for other failures
115
+ throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to initialize repository at: ${targetPath}. Error: ${errorMessage}`, { context, operation, originalError: error });
116
+ }
117
+ }
@@ -0,0 +1,44 @@
1
+ import { ErrorHandler } from '../../../utils/errorHandler.js';
2
+ import { logger } from '../../../utils/logger.js';
3
+ import { requestContextService } from '../../../utils/requestContext.js';
4
+ import { GitInitInputSchema, gitInitLogic } from './logic.js';
5
+ import { BaseErrorCode } from '../../../types-global/errors.js';
6
+ const TOOL_NAME = 'git_init';
7
+ const TOOL_DESCRIPTION = 'Initializes a new Git repository at the specified absolute path. Can optionally set the initial branch name and create a bare repository.';
8
+ /**
9
+ * Registers the git_init tool with the MCP server.
10
+ *
11
+ * @param {McpServer} server - The McpServer instance to register the tool with.
12
+ * @returns {Promise<void>}
13
+ * @throws {Error} If registration fails.
14
+ */
15
+ export const registerGitInitTool = async (server) => {
16
+ const operation = 'registerGitInitTool';
17
+ const context = requestContextService.createRequestContext({ operation });
18
+ await ErrorHandler.tryCatch(async () => {
19
+ server.tool(TOOL_NAME, TOOL_DESCRIPTION, GitInitInputSchema.shape, // Provide the Zod schema shape
20
+ async (validatedArgs, callContext) => {
21
+ const toolOperation = 'tool:git_init';
22
+ const requestContext = requestContextService.createRequestContext({ operation: toolOperation, parentContext: callContext });
23
+ logger.info(`Executing tool: ${TOOL_NAME}`, requestContext);
24
+ return await ErrorHandler.tryCatch(async () => {
25
+ // Call the core logic function
26
+ const initResult = await gitInitLogic(validatedArgs, requestContext);
27
+ // Format the result as a JSON string within TextContent
28
+ const resultContent = {
29
+ type: 'text',
30
+ text: JSON.stringify(initResult, null, 2), // Pretty-print JSON
31
+ contentType: 'application/json',
32
+ };
33
+ logger.info(`Tool ${TOOL_NAME} executed successfully, returning JSON`, requestContext);
34
+ return { content: [resultContent] };
35
+ }, {
36
+ operation: toolOperation,
37
+ context: requestContext,
38
+ input: validatedArgs,
39
+ errorCode: BaseErrorCode.INTERNAL_ERROR, // Default if unexpected error occurs
40
+ });
41
+ });
42
+ logger.info(`Tool registered: ${TOOL_NAME}`, context);
43
+ }, { operation, context, critical: true }); // Mark registration as critical
44
+ };
@@ -0,0 +1,6 @@
1
+ /**
2
+ * @fileoverview Barrel file for the gitLog tool.
3
+ */
4
+ export { registerGitLogTool, initializeGitLogStateAccessors } from './registration.js';
5
+ // Export types if needed elsewhere, e.g.:
6
+ // export type { GitLogInput, GitLogResult } from './logic.js';
@@ -0,0 +1,148 @@
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 structure for a single commit entry
9
+ export const CommitEntrySchema = z.object({
10
+ hash: z.string().describe("Full commit hash"),
11
+ authorName: z.string().describe("Author's name"),
12
+ authorEmail: z.string().email().describe("Author's email"),
13
+ timestamp: z.number().int().positive().describe("Commit timestamp (Unix epoch seconds)"),
14
+ subject: z.string().describe("Commit subject line"),
15
+ body: z.string().optional().describe("Commit body (optional)"),
16
+ });
17
+ // Define the input schema for the git_log tool using Zod
18
+ export const GitLogInputSchema = z.object({
19
+ path: z.string().min(1).optional().default('.').describe("Path to the Git repository. Defaults to the session's working directory if set."),
20
+ maxCount: z.number().int().positive().optional().describe("Limit the number of commits to output."),
21
+ author: z.string().optional().describe("Limit commits to those matching the specified author pattern."),
22
+ since: z.string().optional().describe("Show commits more recent than a specific date (e.g., '2 weeks ago', '2023-01-01')."),
23
+ until: z.string().optional().describe("Show commits older than a specific date."),
24
+ branchOrFile: z.string().optional().describe("Show logs for a specific branch, tag, or file path."),
25
+ // Note: We use a fixed pretty format for reliable parsing. Custom formats are not directly supported via input.
26
+ });
27
+ // Delimiters for parsing the custom format
28
+ const FIELD_SEP = '\x1f'; // Unit Separator
29
+ const RECORD_SEP = '\x1e'; // Record Separator
30
+ const GIT_LOG_FORMAT = `--pretty=format:%H${FIELD_SEP}%an${FIELD_SEP}%ae${FIELD_SEP}%at${FIELD_SEP}%s${FIELD_SEP}%b${RECORD_SEP}`; // %H=hash, %an=author name, %ae=author email, %at=timestamp, %s=subject, %b=body
31
+ /**
32
+ * Executes the 'git log' command with a specific format and returns structured JSON output.
33
+ *
34
+ * @param {GitLogInput} input - The validated input object.
35
+ * @param {RequestContext} context - The request context for logging and error handling.
36
+ * @returns {Promise<GitLogResult>} A promise that resolves with the structured log result.
37
+ * @throws {McpError} Throws an McpError if path resolution, validation, or the git command fails unexpectedly.
38
+ */
39
+ export async function logGitHistory(input, context) {
40
+ const operation = 'logGitHistory';
41
+ logger.debug(`Executing ${operation}`, { ...context, input });
42
+ let targetPath;
43
+ try {
44
+ // Resolve and sanitize the target path
45
+ if (input.path && input.path !== '.') {
46
+ targetPath = input.path;
47
+ }
48
+ else {
49
+ const workingDir = context.getWorkingDirectory();
50
+ if (!workingDir) {
51
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, "No path provided and no working directory set for the session.", { context, operation });
52
+ }
53
+ targetPath = workingDir;
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 git log command
66
+ // Use a specific format for reliable parsing
67
+ let command = `git -C "${targetPath}" log ${GIT_LOG_FORMAT}`;
68
+ if (input.maxCount) {
69
+ command += ` -n ${input.maxCount}`;
70
+ }
71
+ if (input.author) {
72
+ // Basic sanitization for author string
73
+ const safeAuthor = input.author.replace(/[`"$&;*()|<>]/g, '');
74
+ command += ` --author="${safeAuthor}"`;
75
+ }
76
+ if (input.since) {
77
+ const safeSince = input.since.replace(/[`"$&;*()|<>]/g, '');
78
+ command += ` --since="${safeSince}"`;
79
+ }
80
+ if (input.until) {
81
+ const safeUntil = input.until.replace(/[`"$&;*()|<>]/g, '');
82
+ command += ` --until="${safeUntil}"`;
83
+ }
84
+ if (input.branchOrFile) {
85
+ const safeBranchOrFile = input.branchOrFile.replace(/[`"$&;*()|<>]/g, '');
86
+ command += ` ${safeBranchOrFile}`; // Add branch or file path at the end
87
+ }
88
+ logger.debug(`Executing command: ${command}`, { ...context, operation });
89
+ // Increase maxBuffer if logs can be large
90
+ const { stdout, stderr } = await execAsync(command, { maxBuffer: 1024 * 1024 * 10 }); // 10MB buffer
91
+ if (stderr) {
92
+ // Log stderr as warning, as git log might sometimes use it for non-fatal info
93
+ logger.warning(`Git log stderr: ${stderr}`, { ...context, operation });
94
+ }
95
+ // Parse the output
96
+ const commits = [];
97
+ const commitRecords = stdout.split(RECORD_SEP).filter(record => record.trim() !== ''); // Split records and remove empty ones
98
+ for (const record of commitRecords) {
99
+ const fields = record.split(FIELD_SEP);
100
+ if (fields.length >= 5) { // Need at least hash, name, email, timestamp, subject
101
+ try {
102
+ const commitEntry = {
103
+ hash: fields[0],
104
+ authorName: fields[1],
105
+ authorEmail: fields[2],
106
+ timestamp: parseInt(fields[3], 10), // Unix timestamp
107
+ subject: fields[4],
108
+ body: fields[5] || undefined, // Body might be empty
109
+ };
110
+ // Validate parsed entry (optional but recommended)
111
+ CommitEntrySchema.parse(commitEntry);
112
+ commits.push(commitEntry);
113
+ }
114
+ catch (parseError) {
115
+ logger.warning(`Failed to parse commit record field`, { ...context, operation, fieldIndex: fields.findIndex((_, i) => i > 5), recordFragment: record.substring(0, 100), parseError });
116
+ // Decide whether to skip the commit or throw an error
117
+ }
118
+ }
119
+ else {
120
+ logger.warning(`Skipping commit record due to unexpected number of fields (${fields.length})`, { ...context, operation, recordFragment: record.substring(0, 100) });
121
+ }
122
+ }
123
+ const message = commits.length > 0 ? `${commits.length} commit(s) found.` : 'No commits found matching criteria.';
124
+ logger.info(`${operation} completed successfully. ${message}`, { ...context, operation, path: targetPath, commitCount: commits.length });
125
+ return { success: true, commits, message };
126
+ }
127
+ catch (error) {
128
+ logger.error(`Failed to execute git log command`, { ...context, operation, path: targetPath, error: error.message, stderr: error.stderr, stdout: error.stdout });
129
+ const errorMessage = error.stderr || error.stdout || error.message || '';
130
+ // Handle specific error cases
131
+ if (errorMessage.toLowerCase().includes('not a git repository')) {
132
+ throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`, { context, operation, originalError: error });
133
+ }
134
+ if (errorMessage.includes('fatal: bad revision')) {
135
+ throw new McpError(BaseErrorCode.NOT_FOUND, `Invalid branch, tag, or revision specified: '${input.branchOrFile}'. Error: ${errorMessage}`, { context, operation, originalError: error });
136
+ }
137
+ if (errorMessage.includes('fatal: ambiguous argument')) {
138
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Ambiguous argument provided (e.g., branch/tag/file conflict): '${input.branchOrFile}'. Error: ${errorMessage}`, { context, operation, originalError: error });
139
+ }
140
+ // Check if it's just that no commits were found (which git might treat as an error in some cases, though usually not for log)
141
+ if (errorMessage.includes('does not have any commits yet')) {
142
+ logger.info('Repository has no commits yet.', { ...context, operation, path: targetPath });
143
+ return { success: true, commits: [], message: 'Repository has no commits yet.' };
144
+ }
145
+ // Generic internal error for other failures
146
+ throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to get git log for path: ${targetPath}. Error: ${errorMessage}`, { context, operation, originalError: error });
147
+ }
148
+ }
@@ -0,0 +1,71 @@
1
+ import { ErrorHandler } from '../../../utils/errorHandler.js';
2
+ import { logger } from '../../../utils/logger.js';
3
+ import { requestContextService } from '../../../utils/requestContext.js';
4
+ import { GitLogInputSchema, logGitHistory } 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 initializeGitLogStateAccessors(getWdFn, getSidFn) {
15
+ _getWorkingDirectory = getWdFn;
16
+ _getSessionId = getSidFn;
17
+ logger.info('State accessors initialized for git_log tool registration.');
18
+ }
19
+ const TOOL_NAME = 'git_log';
20
+ const TOOL_DESCRIPTION = "Shows commit logs for the repository. Supports limiting count, filtering by author, date range, and specific branch/file. Returns a list of commit objects.";
21
+ /**
22
+ * Registers the git_log 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 registerGitLogTool(server) {
28
+ if (!_getWorkingDirectory || !_getSessionId) {
29
+ throw new Error('State accessors for git_log must be initialized before registration.');
30
+ }
31
+ const operation = 'registerGitLogTool';
32
+ const context = requestContextService.createRequestContext({ operation });
33
+ await ErrorHandler.tryCatch(async () => {
34
+ server.tool(TOOL_NAME, TOOL_DESCRIPTION, GitLogInputSchema.shape, // Provide the Zod schema shape
35
+ async (validatedArgs, callContext) => {
36
+ const toolOperation = 'tool:git_log';
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 logResult = await logGitHistory(validatedArgs, logicContext);
51
+ // Format the result (array of commits) as a JSON string within TextContent
52
+ const resultContent = {
53
+ type: 'text',
54
+ // Stringify the entire GitLogResult object which includes the commits array and success flag
55
+ text: JSON.stringify(logResult, null, 2), // Pretty-print JSON
56
+ contentType: 'application/json',
57
+ };
58
+ logger.info(`Tool ${TOOL_NAME} executed successfully, returning JSON`, logicContext);
59
+ // Success is determined by the logic function and included in the result object
60
+ return { content: [resultContent] };
61
+ }, {
62
+ operation: toolOperation,
63
+ context: logicContext,
64
+ input: validatedArgs,
65
+ errorCode: BaseErrorCode.INTERNAL_ERROR, // Default if unexpected error in logic
66
+ });
67
+ });
68
+ logger.info(`Tool registered: ${TOOL_NAME}`, context);
69
+ }, { operation, context, critical: true });
70
+ }
71
+ ;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @fileoverview Barrel file for the git_merge tool.
3
+ * Exports the registration function and state accessor initialization function.
4
+ */
5
+ export { registerGitMergeTool, initializeGitMergeStateAccessors } from './registration.js';
6
+ // Export types if needed elsewhere, e.g.:
7
+ // export type { GitMergeInput, GitMergeResult } from './logic.js';