@cyanheads/git-mcp-server 2.0.1 → 2.0.3

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 (99) hide show
  1. package/README.md +55 -89
  2. package/{build → dist}/config/index.js +16 -18
  3. package/{build → dist}/index.js +80 -30
  4. package/dist/mcp-server/server.js +296 -0
  5. package/{build → dist}/mcp-server/tools/gitAdd/logic.js +9 -6
  6. package/{build → dist}/mcp-server/tools/gitAdd/registration.js +7 -4
  7. package/{build → dist}/mcp-server/tools/gitBranch/logic.js +23 -12
  8. package/{build → dist}/mcp-server/tools/gitBranch/registration.js +8 -5
  9. package/{build → dist}/mcp-server/tools/gitCheckout/logic.js +92 -44
  10. package/{build → dist}/mcp-server/tools/gitCheckout/registration.js +8 -5
  11. package/{build → dist}/mcp-server/tools/gitCherryPick/logic.js +10 -7
  12. package/{build → dist}/mcp-server/tools/gitCherryPick/registration.js +8 -5
  13. package/{build → dist}/mcp-server/tools/gitClean/logic.js +9 -6
  14. package/{build → dist}/mcp-server/tools/gitClean/registration.js +8 -5
  15. package/{build → dist}/mcp-server/tools/gitClearWorkingDir/logic.js +3 -2
  16. package/{build → dist}/mcp-server/tools/gitClearWorkingDir/registration.js +7 -4
  17. package/{build → dist}/mcp-server/tools/gitClone/logic.js +8 -5
  18. package/{build → dist}/mcp-server/tools/gitClone/registration.js +7 -4
  19. package/dist/mcp-server/tools/gitCommit/logic.js +207 -0
  20. package/{build → dist}/mcp-server/tools/gitCommit/registration.js +22 -15
  21. package/{build → dist}/mcp-server/tools/gitDiff/logic.js +9 -6
  22. package/{build → dist}/mcp-server/tools/gitDiff/registration.js +8 -5
  23. package/{build → dist}/mcp-server/tools/gitFetch/logic.js +10 -7
  24. package/{build → dist}/mcp-server/tools/gitFetch/registration.js +8 -5
  25. package/{build → dist}/mcp-server/tools/gitInit/index.js +2 -2
  26. package/{build → dist}/mcp-server/tools/gitInit/logic.js +9 -6
  27. package/dist/mcp-server/tools/gitInit/registration.js +98 -0
  28. package/{build → dist}/mcp-server/tools/gitLog/logic.js +53 -16
  29. package/{build → dist}/mcp-server/tools/gitLog/registration.js +8 -5
  30. package/{build → dist}/mcp-server/tools/gitMerge/logic.js +9 -6
  31. package/{build → dist}/mcp-server/tools/gitMerge/registration.js +8 -5
  32. package/{build → dist}/mcp-server/tools/gitPull/logic.js +11 -8
  33. package/{build → dist}/mcp-server/tools/gitPull/registration.js +7 -4
  34. package/{build → dist}/mcp-server/tools/gitPush/logic.js +12 -9
  35. package/{build → dist}/mcp-server/tools/gitPush/registration.js +7 -4
  36. package/{build → dist}/mcp-server/tools/gitRebase/logic.js +9 -6
  37. package/{build → dist}/mcp-server/tools/gitRebase/registration.js +8 -5
  38. package/{build → dist}/mcp-server/tools/gitRemote/logic.js +4 -5
  39. package/{build → dist}/mcp-server/tools/gitRemote/registration.js +2 -4
  40. package/{build → dist}/mcp-server/tools/gitReset/logic.js +5 -6
  41. package/{build → dist}/mcp-server/tools/gitReset/registration.js +2 -4
  42. package/{build → dist}/mcp-server/tools/gitSetWorkingDir/logic.js +5 -6
  43. package/{build → dist}/mcp-server/tools/gitSetWorkingDir/registration.js +22 -13
  44. package/{build → dist}/mcp-server/tools/gitShow/logic.js +5 -6
  45. package/{build → dist}/mcp-server/tools/gitShow/registration.js +3 -5
  46. package/{build → dist}/mcp-server/tools/gitStash/logic.js +5 -6
  47. package/{build → dist}/mcp-server/tools/gitStash/registration.js +3 -5
  48. package/{build → dist}/mcp-server/tools/gitStatus/logic.js +5 -6
  49. package/{build → dist}/mcp-server/tools/gitStatus/registration.js +2 -4
  50. package/{build → dist}/mcp-server/tools/gitTag/logic.js +3 -4
  51. package/{build → dist}/mcp-server/tools/gitTag/registration.js +2 -4
  52. package/dist/mcp-server/transports/authentication/authMiddleware.js +145 -0
  53. package/dist/mcp-server/transports/httpTransport.js +432 -0
  54. package/dist/mcp-server/transports/stdioTransport.js +87 -0
  55. package/{build → dist}/types-global/errors.js +2 -2
  56. package/dist/utils/index.js +12 -0
  57. package/{build/utils → dist/utils/internal}/errorHandler.js +18 -8
  58. package/dist/utils/internal/index.js +3 -0
  59. package/dist/utils/internal/logger.js +254 -0
  60. package/{build/utils → dist/utils/internal}/requestContext.js +2 -3
  61. package/dist/utils/metrics/index.js +1 -0
  62. package/{build/utils → dist/utils/metrics}/tokenCounter.js +3 -3
  63. package/dist/utils/parsing/dateParser.js +62 -0
  64. package/dist/utils/parsing/index.js +2 -0
  65. package/{build/utils → dist/utils/parsing}/jsonParser.js +3 -2
  66. package/{build/utils → dist/utils/security}/idGenerator.js +4 -5
  67. package/dist/utils/security/index.js +3 -0
  68. package/{build/utils → dist/utils/security}/rateLimiter.js +7 -10
  69. package/{build/utils → dist/utils/security}/sanitization.js +4 -3
  70. package/package.json +20 -16
  71. package/build/mcp-server/server.js +0 -572
  72. package/build/mcp-server/tools/gitCommit/logic.js +0 -129
  73. package/build/mcp-server/tools/gitInit/registration.js +0 -44
  74. package/build/types-global/mcp.js +0 -59
  75. package/build/types-global/tool.js +0 -1
  76. package/build/utils/index.js +0 -11
  77. package/build/utils/logger.js +0 -266
  78. /package/{build → dist}/mcp-server/tools/gitAdd/index.js +0 -0
  79. /package/{build → dist}/mcp-server/tools/gitBranch/index.js +0 -0
  80. /package/{build → dist}/mcp-server/tools/gitCheckout/index.js +0 -0
  81. /package/{build → dist}/mcp-server/tools/gitCherryPick/index.js +0 -0
  82. /package/{build → dist}/mcp-server/tools/gitClean/index.js +0 -0
  83. /package/{build → dist}/mcp-server/tools/gitClearWorkingDir/index.js +0 -0
  84. /package/{build → dist}/mcp-server/tools/gitClone/index.js +0 -0
  85. /package/{build → dist}/mcp-server/tools/gitCommit/index.js +0 -0
  86. /package/{build → dist}/mcp-server/tools/gitDiff/index.js +0 -0
  87. /package/{build → dist}/mcp-server/tools/gitFetch/index.js +0 -0
  88. /package/{build → dist}/mcp-server/tools/gitLog/index.js +0 -0
  89. /package/{build → dist}/mcp-server/tools/gitMerge/index.js +0 -0
  90. /package/{build → dist}/mcp-server/tools/gitPull/index.js +0 -0
  91. /package/{build → dist}/mcp-server/tools/gitPush/index.js +0 -0
  92. /package/{build → dist}/mcp-server/tools/gitRebase/index.js +0 -0
  93. /package/{build → dist}/mcp-server/tools/gitRemote/index.js +0 -0
  94. /package/{build → dist}/mcp-server/tools/gitReset/index.js +0 -0
  95. /package/{build → dist}/mcp-server/tools/gitSetWorkingDir/index.js +0 -0
  96. /package/{build → dist}/mcp-server/tools/gitShow/index.js +0 -0
  97. /package/{build → dist}/mcp-server/tools/gitStash/index.js +0 -0
  98. /package/{build → dist}/mcp-server/tools/gitStatus/index.js +0 -0
  99. /package/{build → dist}/mcp-server/tools/gitTag/index.js +0 -0
@@ -1,16 +1,19 @@
1
- import { z } from 'zod';
2
- import { promisify } from 'util';
3
1
  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';
2
+ import { promisify } from 'util';
3
+ import { z } from 'zod';
4
+ // Import utils from barrel (logger from ../utils/internal/logger.js)
5
+ import { logger } from '../../../utils/index.js';
6
+ // Import utils from barrel (RequestContext from ../utils/internal/requestContext.js)
7
+ import { BaseErrorCode, McpError } from '../../../types-global/errors.js'; // Keep direct import for types-global
8
+ // Import utils from barrel (sanitization from ../utils/security/sanitization.js)
9
+ import { sanitization } from '../../../utils/index.js';
7
10
  const execAsync = promisify(exec);
8
11
  // Define the input schema for the git_cherry-pick tool using Zod
9
12
  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."),
13
+ path: z.string().min(1).optional().default('.').describe("Path to the local Git repository. Defaults to the directory set via `git_set_working_dir` for the session; set 'git_set_working_dir' if not set."),
11
14
  commitRef: z.string().min(1).describe("The commit reference(s) to cherry-pick (e.g., 'hash1', 'hash1..hash3', 'branchName~3..branchName')."),
12
15
  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."),
16
+ strategy: z.enum(['recursive', 'resolve', 'ours', 'theirs', 'octopus', 'subtree']).optional().describe("Specifies a merge strategy *option* (passed via -X)."),
14
17
  noCommit: z.boolean().default(false).describe("Apply the changes but do not create a commit."),
15
18
  signoff: z.boolean().default(false).describe("Add a Signed-off-by line to the commit message."),
16
19
  // Add options for conflict handling? (e.g., --continue, --abort, --skip) - Maybe separate tool or mode?
@@ -1,8 +1,11 @@
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';
1
+ // Import utils from barrel (logger from ../utils/internal/logger.js)
2
+ import { logger } from '../../../utils/index.js';
3
+ // Import utils from barrel (ErrorHandler from ../utils/internal/errorHandler.js)
4
+ import { ErrorHandler } from '../../../utils/index.js';
5
+ // Import utils from barrel (requestContextService from ../utils/internal/requestContext.js)
6
+ import { BaseErrorCode } from '../../../types-global/errors.js'; // Keep direct import for types-global
7
+ import { requestContextService } from '../../../utils/index.js';
8
+ import { GitCherryPickInputSchema, gitCherryPickLogic } from './logic.js';
6
9
  let _getWorkingDirectory;
7
10
  let _getSessionId;
8
11
  /**
@@ -1,14 +1,17 @@
1
- import { z } from 'zod';
2
- import { promisify } from 'util';
3
1
  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';
2
+ import { promisify } from 'util';
3
+ import { z } from 'zod';
4
+ // Import utils from barrel (logger from ../utils/internal/logger.js)
5
+ import { logger } from '../../../utils/index.js';
6
+ // Import utils from barrel (RequestContext from ../utils/internal/requestContext.js)
7
+ import { BaseErrorCode, McpError } from '../../../types-global/errors.js'; // Keep direct import for types-global
8
+ // Import utils from barrel (sanitization from ../utils/security/sanitization.js)
9
+ import { sanitization } from '../../../utils/index.js';
7
10
  const execAsync = promisify(exec);
8
11
  // Define the input schema for the git_clean tool using Zod
9
12
  // No refinements needed here, but the 'force' check is critical in the logic
10
13
  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."),
14
+ path: z.string().min(1).optional().default('.').describe("Path to the local Git repository. Defaults to the directory set via `git_set_working_dir` for the session; set 'git_set_working_dir' if not set."),
12
15
  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
16
  dryRun: z.boolean().default(false).describe("Show what would be deleted without actually deleting (-n flag)."),
14
17
  directories: z.boolean().default(false).describe("Remove untracked directories in addition to files (-d flag)."),
@@ -1,9 +1,12 @@
1
- import { logger } from '../../../utils/logger.js';
2
- import { ErrorHandler } from '../../../utils/errorHandler.js';
3
- import { requestContextService } from '../../../utils/requestContext.js';
1
+ // Import utils from barrel (logger from ../utils/internal/logger.js)
2
+ import { logger } from '../../../utils/index.js';
3
+ // Import utils from barrel (ErrorHandler from ../utils/internal/errorHandler.js)
4
+ import { ErrorHandler } from '../../../utils/index.js';
5
+ // Import utils from barrel (requestContextService from ../utils/internal/requestContext.js)
6
+ import { requestContextService } from '../../../utils/index.js';
4
7
  // Import the schema and types
5
- import { gitCleanLogic, GitCleanInputSchema } from './logic.js';
6
- import { BaseErrorCode } from '../../../types-global/errors.js';
8
+ import { BaseErrorCode } from '../../../types-global/errors.js'; // Keep direct import for types-global
9
+ import { GitCleanInputSchema, gitCleanLogic } from './logic.js';
7
10
  let _getWorkingDirectory;
8
11
  let _getSessionId;
9
12
  /**
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod';
2
- import { logger } from '../../../utils/logger.js';
3
- import { McpError, BaseErrorCode } from '../../../types-global/errors.js';
2
+ // Import utils from barrel (logger from ../utils/internal/logger.js)
3
+ import { BaseErrorCode, McpError } from '../../../types-global/errors.js'; // Keep direct import for types-global
4
+ import { logger } from '../../../utils/index.js';
4
5
  // Define the Zod schema for input validation (no arguments needed)
5
6
  export const GitClearWorkingDirInputSchema = z.object({});
6
7
  /**
@@ -1,8 +1,11 @@
1
- import { ErrorHandler } from '../../../utils/errorHandler.js';
2
- import { logger } from '../../../utils/logger.js';
3
- import { requestContextService } from '../../../utils/requestContext.js';
1
+ // Import utils from barrel (ErrorHandler from ../utils/internal/errorHandler.js)
2
+ import { ErrorHandler } from '../../../utils/index.js';
3
+ // Import utils from barrel (logger from ../utils/internal/logger.js)
4
+ import { logger } from '../../../utils/index.js';
5
+ // Import utils from barrel (requestContextService, RequestContext from ../utils/internal/requestContext.js)
6
+ import { BaseErrorCode } from '../../../types-global/errors.js'; // Keep direct import for types-global
7
+ import { requestContextService } from '../../../utils/index.js';
4
8
  import { GitClearWorkingDirInputSchema, gitClearWorkingDirLogic } from './logic.js';
5
- import { BaseErrorCode } from '../../../types-global/errors.js';
6
9
  let _clearWorkingDirectory;
7
10
  let _getSessionId;
8
11
  /**
@@ -1,10 +1,13 @@
1
- import { z } from 'zod';
2
- import { promisify } from 'util';
3
1
  import { exec } from 'child_process';
4
2
  import fs from 'fs/promises';
5
- import { logger } from '../../../utils/logger.js';
6
- import { McpError, BaseErrorCode } from '../../../types-global/errors.js';
7
- import { sanitization } from '../../../utils/sanitization.js';
3
+ import { promisify } from 'util';
4
+ import { z } from 'zod';
5
+ // Import utils from barrel (logger from ../utils/internal/logger.js)
6
+ import { logger } from '../../../utils/index.js';
7
+ // Import utils from barrel (RequestContext from ../utils/internal/requestContext.js)
8
+ import { BaseErrorCode, McpError } from '../../../types-global/errors.js'; // Keep direct import for types-global
9
+ // Import utils from barrel (sanitization from ../utils/security/sanitization.js)
10
+ import { sanitization } from '../../../utils/index.js';
8
11
  const execAsync = promisify(exec);
9
12
  // Define the input schema for the git_clone tool using Zod
10
13
  export const GitCloneInputSchema = z.object({
@@ -1,8 +1,11 @@
1
- import { ErrorHandler } from '../../../utils/errorHandler.js';
2
- import { logger } from '../../../utils/logger.js';
3
- import { requestContextService } from '../../../utils/requestContext.js';
1
+ // Import utils from barrel (ErrorHandler from ../utils/internal/errorHandler.js)
2
+ import { ErrorHandler } from '../../../utils/index.js';
3
+ // Import utils from barrel (logger from ../utils/internal/logger.js)
4
+ import { logger } from '../../../utils/index.js';
5
+ // Import utils from barrel (requestContextService from ../utils/internal/requestContext.js)
6
+ import { requestContextService } from '../../../utils/index.js';
4
7
  import { GitCloneInputSchema, gitCloneLogic } from './logic.js';
5
- import { BaseErrorCode } from '../../../types-global/errors.js';
8
+ import { BaseErrorCode } from '../../../types-global/errors.js'; // Keep direct import for types-global
6
9
  const TOOL_NAME = 'git_clone';
7
10
  const TOOL_DESCRIPTION = 'Clones a Git repository from a given URL into a specified absolute directory path. Supports cloning specific branches and setting clone depth.';
8
11
  /**
@@ -0,0 +1,207 @@
1
+ import { exec } from 'child_process';
2
+ import { promisify } from 'util';
3
+ import { z } from 'zod';
4
+ import { BaseErrorCode, McpError } from '../../../types-global/errors.js'; // Keep direct import for types-global
5
+ // Import utils from barrel (logger from ../utils/internal/logger.js)
6
+ import { logger } from '../../../utils/index.js';
7
+ // Import utils from barrel (sanitization from ../utils/security/sanitization.js)
8
+ import { sanitization } from '../../../utils/index.js';
9
+ // Import config to check signing flag
10
+ import { config } from '../../../config/index.js';
11
+ const execAsync = promisify(exec);
12
+ // Define the input schema for the git_commit tool using Zod
13
+ export const GitCommitInputSchema = z.object({
14
+ path: z.string().min(1).optional().default('.').describe("Path to the Git repository. Defaults to the directory set via `git_set_working_dir` for the session; set 'git_set_working_dir' if not set."),
15
+ message: z.string().min(1).describe('Commit message. Follow Conventional Commits format: `type(scope): subject`. Example: `feat(api): add user signup endpoint`'),
16
+ author: z.object({
17
+ name: z.string().describe('Author name for the commit'),
18
+ email: z.string().email().describe('Author email for the commit'),
19
+ }).optional().describe('Overrides the commit author information (name and email). Use only when necessary (e.g., applying external patches).'),
20
+ allowEmpty: z.boolean().default(false).describe('Allow creating empty commits'),
21
+ amend: z.boolean().default(false).describe('Amend the previous commit instead of creating a new one'),
22
+ forceUnsignedOnFailure: z.boolean().default(false).describe('If true and signing is enabled but fails, attempt the commit without signing instead of failing.'),
23
+ });
24
+ /**
25
+ * Executes the 'git commit' command and returns structured JSON output.
26
+ *
27
+ * @param {GitCommitInput} input - The validated input object.
28
+ * @param {RequestContext} context - The request context for logging and error handling.
29
+ * @returns {Promise<GitCommitResult>} A promise that resolves with the structured commit result.
30
+ * @throws {McpError} Throws an McpError if path resolution or validation fails, or if the git command fails unexpectedly.
31
+ */
32
+ export async function commitGitChanges(input, context // Add getter to context
33
+ ) {
34
+ const operation = 'commitGitChanges';
35
+ logger.debug(`Executing ${operation}`, { ...context, input });
36
+ let targetPath;
37
+ try {
38
+ // Resolve the target path
39
+ if (input.path && input.path !== '.') {
40
+ targetPath = input.path;
41
+ logger.debug(`Using provided path: ${targetPath}`, { ...context, operation });
42
+ }
43
+ else {
44
+ const workingDir = context.getWorkingDirectory();
45
+ if (!workingDir) {
46
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, "No path provided and no working directory set for the session.", { context, operation });
47
+ }
48
+ targetPath = workingDir;
49
+ logger.debug(`Using session working directory: ${targetPath}`, { ...context, operation, sessionId: context.sessionId });
50
+ }
51
+ // Sanitize the resolved path
52
+ const sanitizedPath = sanitization.sanitizePath(targetPath);
53
+ logger.debug('Sanitized path', { ...context, operation, sanitizedPath });
54
+ targetPath = sanitizedPath; // Use the sanitized path going forward
55
+ }
56
+ catch (error) {
57
+ logger.error('Path resolution or sanitization failed', { ...context, operation, error });
58
+ if (error instanceof McpError) {
59
+ throw error;
60
+ }
61
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
62
+ }
63
+ try {
64
+ // Escape message for shell safety
65
+ const escapeShellArg = (arg) => {
66
+ // Escape backslashes first, then other special chars
67
+ return arg.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/`/g, '\\`').replace(/\$/g, '\\$');
68
+ };
69
+ const escapedMessage = escapeShellArg(input.message);
70
+ // Construct the git commit command using the resolved targetPath
71
+ let command = `git -C "${targetPath}" commit -m "${escapedMessage}"`;
72
+ if (input.allowEmpty) {
73
+ command += ' --allow-empty';
74
+ }
75
+ if (input.amend) {
76
+ command += ' --amend --no-edit';
77
+ }
78
+ if (input.author) {
79
+ // Escape author details as well
80
+ const escapedAuthorName = escapeShellArg(input.author.name);
81
+ const escapedAuthorEmail = escapeShellArg(input.author.email); // Email typically safe, but escape anyway
82
+ // Use -c flags to override author for this commit, using the already escaped message
83
+ command = `git -C "${targetPath}" -c user.name="${escapedAuthorName}" -c user.email="${escapedAuthorEmail}" commit -m "${escapedMessage}"`;
84
+ }
85
+ // Append common flags (ensure they are appended to the potentially modified command from author block)
86
+ if (input.allowEmpty && !command.includes(' --allow-empty'))
87
+ command += ' --allow-empty';
88
+ if (input.amend && !command.includes(' --amend'))
89
+ command += ' --amend --no-edit'; // Avoid double adding if author block modified command
90
+ // Append signing flag if configured via GIT_SIGN_COMMITS env var
91
+ if (config.gitSignCommits) {
92
+ command += ' -S'; // Add signing flag (-S)
93
+ logger.info('Signing enabled via GIT_SIGN_COMMITS=true, adding -S flag.', { ...context, operation });
94
+ }
95
+ logger.debug(`Executing initial command attempt: ${command}`, { ...context, operation });
96
+ let stdout;
97
+ let stderr;
98
+ let commitResult;
99
+ try {
100
+ // Initial attempt (potentially with -S flag)
101
+ const execResult = await execAsync(command);
102
+ stdout = execResult.stdout;
103
+ stderr = execResult.stderr;
104
+ }
105
+ catch (error) {
106
+ const initialErrorMessage = error.stderr || error.message || '';
107
+ const isSigningError = initialErrorMessage.includes('gpg failed to sign') || initialErrorMessage.includes('signing failed');
108
+ if (isSigningError && input.forceUnsignedOnFailure) {
109
+ logger.warning('Initial commit attempt failed due to signing error. Retrying without signing as forceUnsignedOnFailure=true.', { ...context, operation, initialError: initialErrorMessage });
110
+ // Construct command *without* -S flag, using escaped message/author
111
+ const escapeShellArg = (arg) => {
112
+ return arg.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/`/g, '\\`').replace(/\$/g, '\\$');
113
+ };
114
+ const escapedMessage = escapeShellArg(input.message);
115
+ let unsignedCommand = `git -C "${targetPath}" commit -m "${escapedMessage}"`;
116
+ if (input.allowEmpty)
117
+ unsignedCommand += ' --allow-empty';
118
+ if (input.amend)
119
+ unsignedCommand += ' --amend --no-edit';
120
+ if (input.author) {
121
+ const escapedAuthorName = escapeShellArg(input.author.name);
122
+ const escapedAuthorEmail = escapeShellArg(input.author.email);
123
+ unsignedCommand = `git -C "${targetPath}" -c user.name="${escapedAuthorName}" -c user.email="${escapedAuthorEmail}" commit -m "${escapedMessage}"`;
124
+ // Re-append common flags if author block overwrote command
125
+ if (input.allowEmpty && !unsignedCommand.includes(' --allow-empty'))
126
+ unsignedCommand += ' --allow-empty';
127
+ if (input.amend && !unsignedCommand.includes(' --amend'))
128
+ unsignedCommand += ' --amend --no-edit';
129
+ }
130
+ logger.debug(`Executing unsigned fallback command: ${unsignedCommand}`, { ...context, operation });
131
+ try {
132
+ // Retry commit without signing
133
+ const fallbackResult = await execAsync(unsignedCommand);
134
+ stdout = fallbackResult.stdout;
135
+ stderr = fallbackResult.stderr;
136
+ // Add a note to the status message indicating signing was skipped
137
+ commitResult = {
138
+ success: true,
139
+ statusMessage: `Commit successful (unsigned, signing failed): ${stdout.trim()}`, // Default message, hash parsed below
140
+ commitHash: undefined // Will be parsed below
141
+ };
142
+ }
143
+ catch (fallbackError) {
144
+ // If the unsigned commit *also* fails, re-throw that error
145
+ logger.error('Unsigned fallback commit attempt also failed.', { ...context, operation, fallbackError: fallbackError.message, stderr: fallbackError.stderr });
146
+ throw fallbackError; // Re-throw the error from the unsigned attempt
147
+ }
148
+ }
149
+ else {
150
+ // If it wasn't a signing error, or forceUnsignedOnFailure is false, re-throw the original error
151
+ throw error;
152
+ }
153
+ }
154
+ // Process result (either from initial attempt or fallback)
155
+ // Check stderr first for common non-error messages
156
+ if (stderr && !commitResult) { // Don't overwrite fallback message if stderr also exists
157
+ if (stderr.includes('nothing to commit, working tree clean') || stderr.includes('no changes added to commit')) {
158
+ const msg = stderr.includes('nothing to commit') ? 'Nothing to commit, working tree clean.' : 'No changes added to commit.';
159
+ logger.info(msg, { ...context, operation, path: targetPath });
160
+ // Use statusMessage
161
+ return { success: true, statusMessage: msg, nothingToCommit: true };
162
+ }
163
+ // Log other stderr as warning but continue, as commit might still succeed
164
+ logger.warning(`Git commit command produced stderr`, { ...context, operation, stderr });
165
+ }
166
+ // Extract commit hash (more robustly)
167
+ let commitHash = undefined;
168
+ const hashMatch = stdout.match(/([a-f0-9]{7,40})/); // Look for typical short or long hash
169
+ if (hashMatch) {
170
+ commitHash = hashMatch[1];
171
+ }
172
+ else {
173
+ // Fallback parsing if needed, or rely on success message
174
+ logger.warning('Could not parse commit hash from stdout', { ...context, operation, stdout });
175
+ }
176
+ // Use statusMessage, potentially using the one set during fallback
177
+ const finalStatusMsg = commitResult?.statusMessage || (commitHash
178
+ ? `Commit successful: ${commitHash}`
179
+ : `Commit successful (stdout: ${stdout.trim()})`);
180
+ logger.info(`${operation} executed successfully`, { ...context, operation, path: targetPath, commitHash, signed: !commitResult }); // Log if it was signed (not fallback)
181
+ return {
182
+ success: true,
183
+ statusMessage: finalStatusMsg, // Use potentially modified message
184
+ commitHash: commitHash
185
+ };
186
+ }
187
+ catch (error) { // This catch block now primarily handles non-signing errors or errors from the fallback attempt
188
+ logger.error(`Failed to execute git commit command`, { ...context, operation, path: targetPath, error: error.message, stderr: error.stderr });
189
+ const errorMessage = error.stderr || error.message || '';
190
+ // Handle specific error cases first
191
+ if (errorMessage.toLowerCase().includes('not a git repository')) {
192
+ throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`, { context, operation, originalError: error });
193
+ }
194
+ if (errorMessage.includes('nothing to commit') || errorMessage.includes('no changes added to commit')) {
195
+ // This might happen if git exits with error despite these messages
196
+ const msg = errorMessage.includes('nothing to commit') ? 'Nothing to commit, working tree clean.' : 'No changes added to commit.';
197
+ logger.info(msg + ' (caught as error)', { ...context, operation, path: targetPath, errorMessage });
198
+ // Return success=false but indicate the reason using statusMessage
199
+ return { success: false, statusMessage: msg, nothingToCommit: true };
200
+ }
201
+ if (errorMessage.includes('conflicts')) {
202
+ throw new McpError(BaseErrorCode.CONFLICT, `Commit failed due to unresolved conflicts in ${targetPath}`, { context, operation, originalError: error });
203
+ }
204
+ // Generic internal error for other failures
205
+ throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to commit changes for path: ${targetPath}. Error: ${errorMessage}`, { context, operation, originalError: error });
206
+ }
207
+ }
@@ -1,8 +1,11 @@
1
- import { ErrorHandler } from '../../../utils/errorHandler.js';
2
- import { logger } from '../../../utils/logger.js';
3
- import { requestContextService } from '../../../utils/requestContext.js';
1
+ // Import utils from barrel (ErrorHandler from ../utils/internal/errorHandler.js)
2
+ import { ErrorHandler } from '../../../utils/index.js';
3
+ // Import utils from barrel (logger from ../utils/internal/logger.js)
4
+ import { logger } from '../../../utils/index.js';
5
+ // Import utils from barrel (requestContextService from ../utils/internal/requestContext.js)
6
+ import { requestContextService } from '../../../utils/index.js';
4
7
  // Import the result type along with the function and input schema
5
- import { BaseErrorCode } from '../../../types-global/errors.js'; // Import BaseErrorCode
8
+ import { BaseErrorCode } from '../../../types-global/errors.js'; // Keep direct import for types-global
6
9
  import { commitGitChanges, GitCommitInputSchema } from './logic.js';
7
10
  let _getWorkingDirectory;
8
11
  let _getSessionId;
@@ -26,28 +29,32 @@ Write clear, concise commit messages using the Conventional Commits format: \`ty
26
29
  - \`(scope)\`: Optional context (e.g., \`auth\`, \`ui\`, filename).
27
30
  - \`subject\`: Imperative, present tense description (e.g., "add login button", not "added login button").
28
31
 
32
+ I want to understand what you did and why. Use the body for detailed explanations, if necessary.
33
+
29
34
  **Example Commit Message:**
30
35
  \`\`\`
31
36
  feat(auth): implement password reset endpoint
32
37
 
33
- Adds the /api/auth/reset-password endpoint to allow users
34
- to reset their password via an email link. Includes input
35
- validation and rate limiting.
38
+ - Adds the /api/auth/reset-password endpoint to allow users to reset their password via an email link.
39
+ - Includes input validation and rate limiting.
36
40
 
37
41
  Closes #123 (if applicable).
38
42
  \`\`\`
39
43
 
40
44
  **Best Practice:** Commit related changes together in logical units. If you've modified multiple files for a single feature or fix, stage and commit them together with a message that describes the overall change.
41
45
 
42
- **Path Handling:** If the 'path' parameter is omitted or set to '.', the tool uses the working directory set by 'git_set_working_dir'. Providing a full, absolute path overrides this default and ensures explicitness.`;
46
+ **Path Handling:** If the 'path' parameter is omitted or set to '.', the tool uses the working directory set by 'git_set_working_dir'. Providing a full, absolute path overrides this default and ensures explicitness.
47
+
48
+ **Commit Signing:** If the server is configured with the \`GIT_SIGN_COMMITS=true\` environment variable, this tool adds the \`-S\` flag to the \`git commit\` command, requesting a signature. Signing requires proper GPG or SSH key setup and Git configuration on the server machine.
49
+ - **Fallback:** If signing is enabled but fails (e.g., key not found, agent issue), the commit will **fail by default**. However, if the optional \`forceUnsignedOnFailure: true\` parameter is provided in the tool call, the tool will attempt the commit again *without* the \`-S\` flag, resulting in an unsigned commit.`;
43
50
  /**
44
- * Registers the git_commit tool with the MCP server.
45
- * Uses the high-level server.tool() method for registration, schema validation, and routing.
46
- *
47
- * @param {McpServer} server - The McpServer instance to register the tool with.
48
- * @returns {Promise<void>}
49
- * @throws {Error} If registration fails or state accessors are not initialized.
50
- */
51
+ * Registers the git_commit tool with the MCP server.
52
+ * Uses the high-level server.tool() method for registration, schema validation, and routing.
53
+ *
54
+ * @param {McpServer} server - The McpServer instance to register the tool with.
55
+ * @returns {Promise<void>}
56
+ * @throws {Error} If registration fails or state accessors are not initialized.
57
+ */
51
58
  export const registerGitCommitTool = async (server) => {
52
59
  if (!_getWorkingDirectory || !_getSessionId) {
53
60
  throw new Error('State accessors for git_commit must be initialized before registration.');
@@ -1,13 +1,16 @@
1
- import { z } from 'zod';
2
- import { promisify } from 'util';
3
1
  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';
2
+ import { promisify } from 'util';
3
+ import { z } from 'zod';
4
+ // Import utils from barrel (logger from ../utils/internal/logger.js)
5
+ import { logger } from '../../../utils/index.js';
6
+ // Import utils from barrel (RequestContext from ../utils/internal/requestContext.js)
7
+ import { BaseErrorCode, McpError } from '../../../types-global/errors.js'; // Keep direct import for types-global
8
+ // Import utils from barrel (sanitization from ../utils/security/sanitization.js)
9
+ import { sanitization } from '../../../utils/index.js';
7
10
  const execAsync = promisify(exec);
8
11
  // Define the base input schema without refinement
9
12
  const GitDiffInputBaseSchema = 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."),
13
+ path: z.string().min(1).optional().default('.').describe("Path to the Git repository. Defaults to the directory set via `git_set_working_dir` for the session; set 'git_set_working_dir' if not set."),
11
14
  commit1: z.string().optional().describe("First commit, branch, or ref for comparison. If omitted, compares against the working tree or index (depending on 'staged')."),
12
15
  commit2: z.string().optional().describe("Second commit, branch, or ref for comparison. If omitted, compares commit1 against the working tree or index."),
13
16
  staged: z.boolean().optional().default(false).describe("Show diff of changes staged for the next commit (compares index against HEAD). Overrides commit1/commit2 if true."),
@@ -1,9 +1,12 @@
1
- import { ErrorHandler } from '../../../utils/errorHandler.js';
2
- import { logger } from '../../../utils/logger.js';
3
- import { requestContextService } from '../../../utils/requestContext.js';
1
+ // Import utils from barrel (ErrorHandler from ../utils/internal/errorHandler.js)
2
+ import { ErrorHandler } from '../../../utils/index.js';
3
+ // Import utils from barrel (logger from ../utils/internal/logger.js)
4
+ import { logger } from '../../../utils/index.js';
5
+ // Import utils from barrel (requestContextService, RequestContext from ../utils/internal/requestContext.js)
6
+ import { requestContextService } from '../../../utils/index.js';
4
7
  // Import the shape and the final schema/types
5
- import { GitDiffInputShape, diffGitChanges } from './logic.js';
6
- import { BaseErrorCode } from '../../../types-global/errors.js';
8
+ import { BaseErrorCode } from '../../../types-global/errors.js'; // Keep direct import for types-global
9
+ import { diffGitChanges, GitDiffInputShape } from './logic.js';
7
10
  let _getWorkingDirectory;
8
11
  let _getSessionId;
9
12
  /**
@@ -1,13 +1,16 @@
1
- import { z } from 'zod';
2
- import { promisify } from 'util';
3
1
  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';
2
+ import { promisify } from 'util';
3
+ import { z } from 'zod';
4
+ // Import utils from barrel (logger from ../utils/internal/logger.js)
5
+ import { logger } from '../../../utils/index.js';
6
+ // Import utils from barrel (RequestContext from ../utils/internal/requestContext.js)
7
+ import { BaseErrorCode, McpError } from '../../../types-global/errors.js'; // Keep direct import for types-global
8
+ // Import utils from barrel (sanitization from ../utils/security/sanitization.js)
9
+ import { sanitization } from '../../../utils/index.js';
7
10
  const execAsync = promisify(exec);
8
11
  // Define the input schema for the git_fetch tool using Zod
9
12
  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."),
13
+ path: z.string().min(1).optional().default('.').describe("Path to the Git repository. Defaults to the directory set via `git_set_working_dir` for the session; set 'git_set_working_dir' if not set."),
11
14
  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
15
  prune: z.boolean().optional().default(false).describe("Before fetching, remove any remote-tracking references that no longer exist on the remote."),
13
16
  tags: z.boolean().optional().default(false).describe("Fetch all tags from the remote (in addition to whatever else is fetched)."),
@@ -99,7 +102,7 @@ export async function fetchGitRemote(input, context) {
99
102
  throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`, { context, operation, originalError: error });
100
103
  }
101
104
  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 });
105
+ throw new McpError(BaseErrorCode.SERVICE_UNAVAILABLE, `Failed to connect to remote repository '${input.remote || 'default'}'. Error: ${errorMessage}`, { context, operation, originalError: error });
103
106
  }
104
107
  if (errorMessage.includes('fatal: ') && errorMessage.includes('couldn\'t find remote ref')) {
105
108
  throw new McpError(BaseErrorCode.NOT_FOUND, `Remote ref not found. Error: ${errorMessage}`, { context, operation, originalError: error });
@@ -1,8 +1,11 @@
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';
1
+ // Import utils from barrel (ErrorHandler from ../utils/internal/errorHandler.js)
2
+ import { ErrorHandler } from '../../../utils/index.js';
3
+ // Import utils from barrel (logger from ../utils/internal/logger.js)
4
+ import { logger } from '../../../utils/index.js';
5
+ // Import utils from barrel (requestContextService, RequestContext from ../utils/internal/requestContext.js)
6
+ import { BaseErrorCode } from '../../../types-global/errors.js'; // Keep direct import for types-global
7
+ import { requestContextService } from '../../../utils/index.js';
8
+ import { fetchGitRemote, GitFetchInputSchema } from './logic.js';
6
9
  let _getWorkingDirectory;
7
10
  let _getSessionId;
8
11
  /**
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @fileoverview Barrel file for the git_init tool.
3
- * Exports the registration function.
3
+ * Exports the registration function and the state accessor initializer.
4
4
  */
5
- export { registerGitInitTool } from './registration.js';
5
+ export { registerGitInitTool, initializeGitInitStateAccessors } from './registration.js';
6
6
  // Export types if needed elsewhere, e.g.:
7
7
  // export type { GitInitInput, GitInitResult } from './logic.js';
@@ -1,15 +1,18 @@
1
- import { z } from 'zod';
2
- import { promisify } from 'util';
3
1
  import { exec } from 'child_process';
4
2
  import fs from 'fs/promises';
5
3
  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';
4
+ import { promisify } from 'util';
5
+ import { z } from 'zod';
6
+ // Import utils from barrel (logger from ../utils/internal/logger.js)
7
+ import { logger } from '../../../utils/index.js';
8
+ // Import utils from barrel (RequestContext from ../utils/internal/requestContext.js)
9
+ import { BaseErrorCode, McpError } from '../../../types-global/errors.js'; // Keep direct import for types-global
10
+ // Import utils from barrel (sanitization from ../utils/security/sanitization.js)
11
+ import { sanitization } from '../../../utils/index.js';
9
12
  const execAsync = promisify(exec);
10
13
  // Define the input schema for the git_init tool using Zod
11
14
  export const GitInitInputSchema = z.object({
12
- path: z.string().min(1).describe("The absolute path where the new Git repository should be initialized."),
15
+ path: z.string().min(1).optional().default('.').describe("Path where the new Git repository should be initialized. Can be relative or absolute. If relative or '.', it resolves against the directory set via `git_set_working_dir` for the session. If absolute, it's used directly. If omitted, defaults to '.' (resolved against session git working directory)."),
13
16
  initialBranch: z.string().optional().describe("Optional name for the initial branch (e.g., 'main'). Uses Git's default if not specified."),
14
17
  bare: z.boolean().default(false).describe("Create a bare repository (no working directory)."),
15
18
  quiet: z.boolean().default(false).describe("Only print error and warning messages; all other output will be suppressed."),