@cyanheads/git-mcp-server 2.0.2 → 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 (73) hide show
  1. package/README.md +45 -85
  2. package/dist/config/index.js +16 -18
  3. package/dist/index.js +80 -30
  4. package/dist/mcp-server/server.js +247 -523
  5. package/dist/mcp-server/tools/gitAdd/logic.js +9 -6
  6. package/dist/mcp-server/tools/gitAdd/registration.js +7 -4
  7. package/dist/mcp-server/tools/gitBranch/logic.js +23 -12
  8. package/dist/mcp-server/tools/gitBranch/registration.js +8 -5
  9. package/dist/mcp-server/tools/gitCheckout/logic.js +92 -44
  10. package/dist/mcp-server/tools/gitCheckout/registration.js +8 -5
  11. package/dist/mcp-server/tools/gitCherryPick/logic.js +10 -7
  12. package/dist/mcp-server/tools/gitCherryPick/registration.js +8 -5
  13. package/dist/mcp-server/tools/gitClean/logic.js +9 -6
  14. package/dist/mcp-server/tools/gitClean/registration.js +8 -5
  15. package/dist/mcp-server/tools/gitClearWorkingDir/logic.js +3 -2
  16. package/dist/mcp-server/tools/gitClearWorkingDir/registration.js +7 -4
  17. package/dist/mcp-server/tools/gitClone/logic.js +8 -5
  18. package/dist/mcp-server/tools/gitClone/registration.js +7 -4
  19. package/dist/mcp-server/tools/gitCommit/logic.js +98 -20
  20. package/dist/mcp-server/tools/gitCommit/registration.js +22 -15
  21. package/dist/mcp-server/tools/gitDiff/logic.js +9 -6
  22. package/dist/mcp-server/tools/gitDiff/registration.js +8 -5
  23. package/dist/mcp-server/tools/gitFetch/logic.js +10 -7
  24. package/dist/mcp-server/tools/gitFetch/registration.js +8 -5
  25. package/dist/mcp-server/tools/gitInit/index.js +2 -2
  26. package/dist/mcp-server/tools/gitInit/logic.js +9 -6
  27. package/dist/mcp-server/tools/gitInit/registration.js +66 -12
  28. package/dist/mcp-server/tools/gitLog/logic.js +53 -16
  29. package/dist/mcp-server/tools/gitLog/registration.js +8 -5
  30. package/dist/mcp-server/tools/gitMerge/logic.js +9 -6
  31. package/dist/mcp-server/tools/gitMerge/registration.js +8 -5
  32. package/dist/mcp-server/tools/gitPull/logic.js +11 -8
  33. package/dist/mcp-server/tools/gitPull/registration.js +7 -4
  34. package/dist/mcp-server/tools/gitPush/logic.js +12 -9
  35. package/dist/mcp-server/tools/gitPush/registration.js +7 -4
  36. package/dist/mcp-server/tools/gitRebase/logic.js +9 -6
  37. package/dist/mcp-server/tools/gitRebase/registration.js +8 -5
  38. package/dist/mcp-server/tools/gitRemote/logic.js +4 -5
  39. package/dist/mcp-server/tools/gitRemote/registration.js +2 -4
  40. package/dist/mcp-server/tools/gitReset/logic.js +5 -6
  41. package/dist/mcp-server/tools/gitReset/registration.js +2 -4
  42. package/dist/mcp-server/tools/gitSetWorkingDir/logic.js +5 -6
  43. package/dist/mcp-server/tools/gitSetWorkingDir/registration.js +22 -13
  44. package/dist/mcp-server/tools/gitShow/logic.js +5 -6
  45. package/dist/mcp-server/tools/gitShow/registration.js +3 -5
  46. package/dist/mcp-server/tools/gitStash/logic.js +5 -6
  47. package/dist/mcp-server/tools/gitStash/registration.js +3 -5
  48. package/dist/mcp-server/tools/gitStatus/logic.js +5 -6
  49. package/dist/mcp-server/tools/gitStatus/registration.js +2 -4
  50. package/dist/mcp-server/tools/gitTag/logic.js +3 -4
  51. package/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/dist/types-global/errors.js +2 -2
  56. package/dist/utils/index.js +12 -11
  57. package/dist/utils/{errorHandler.js → 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/dist/utils/{requestContext.js → internal/requestContext.js} +2 -3
  61. package/dist/utils/metrics/index.js +1 -0
  62. package/dist/utils/{tokenCounter.js → 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/dist/utils/{jsonParser.js → parsing/jsonParser.js} +3 -2
  66. package/dist/utils/{idGenerator.js → security/idGenerator.js} +4 -5
  67. package/dist/utils/security/index.js +3 -0
  68. package/dist/utils/{rateLimiter.js → security/rateLimiter.js} +7 -10
  69. package/dist/utils/{sanitization.js → security/sanitization.js} +4 -3
  70. package/package.json +12 -9
  71. package/dist/types-global/mcp.js +0 -59
  72. package/dist/types-global/tool.js +0 -1
  73. package/dist/utils/logger.js +0 -266
@@ -1,13 +1,17 @@
1
1
  import { exec } from 'child_process';
2
2
  import { promisify } from 'util';
3
3
  import { z } from 'zod';
4
- import { BaseErrorCode, McpError } from '../../../types-global/errors.js';
5
- import { logger } from '../../../utils/logger.js';
6
- import { sanitization } from '../../../utils/sanitization.js';
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';
7
11
  const execAsync = promisify(exec);
8
12
  // Define the input schema for the git_commit tool using Zod
9
13
  export const GitCommitInputSchema = 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 via `git_set_working_dir`, otherwise defaults to the server's current working directory (`.`)."),
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."),
11
15
  message: z.string().min(1).describe('Commit message. Follow Conventional Commits format: `type(scope): subject`. Example: `feat(api): add user signup endpoint`'),
12
16
  author: z.object({
13
17
  name: z.string().describe('Author name for the commit'),
@@ -15,6 +19,7 @@ export const GitCommitInputSchema = z.object({
15
19
  }).optional().describe('Overrides the commit author information (name and email). Use only when necessary (e.g., applying external patches).'),
16
20
  allowEmpty: z.boolean().default(false).describe('Allow creating empty commits'),
17
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.'),
18
23
  });
19
24
  /**
20
25
  * Executes the 'git commit' command and returns structured JSON output.
@@ -56,8 +61,14 @@ export async function commitGitChanges(input, context // Add getter to context
56
61
  throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
57
62
  }
58
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);
59
70
  // Construct the git commit command using the resolved targetPath
60
- let command = `git -C "${targetPath}" commit -m "${input.message.replace(/"/g, '\\"')}"`; // Escape double quotes
71
+ let command = `git -C "${targetPath}" commit -m "${escapedMessage}"`;
61
72
  if (input.allowEmpty) {
62
73
  command += ' --allow-empty';
63
74
  }
@@ -65,17 +76,84 @@ export async function commitGitChanges(input, context // Add getter to context
65
76
  command += ' --amend --no-edit';
66
77
  }
67
78
  if (input.author) {
68
- // Ensure author details are properly escaped if needed, though exec usually handles this
69
- command = `git -C "${targetPath}" -c user.name="${input.author.name.replace(/"/g, '\\"')}" -c user.email="${input.author.email.replace(/"/g, '\\"')}" commit -m "${input.message.replace(/"/g, '\\"')}"`;
70
- if (input.allowEmpty)
71
- command += ' --allow-empty';
72
- if (input.amend)
73
- command += ' --amend --no-edit';
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;
74
104
  }
75
- logger.debug(`Executing command: ${command}`, { ...context, operation });
76
- const { stdout, stderr } = await execAsync(command);
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)
77
155
  // Check stderr first for common non-error messages
78
- if (stderr) {
156
+ if (stderr && !commitResult) { // Don't overwrite fallback message if stderr also exists
79
157
  if (stderr.includes('nothing to commit, working tree clean') || stderr.includes('no changes added to commit')) {
80
158
  const msg = stderr.includes('nothing to commit') ? 'Nothing to commit, working tree clean.' : 'No changes added to commit.';
81
159
  logger.info(msg, { ...context, operation, path: targetPath });
@@ -95,18 +173,18 @@ export async function commitGitChanges(input, context // Add getter to context
95
173
  // Fallback parsing if needed, or rely on success message
96
174
  logger.warning('Could not parse commit hash from stdout', { ...context, operation, stdout });
97
175
  }
98
- // Use statusMessage
99
- const statusMsg = commitHash
176
+ // Use statusMessage, potentially using the one set during fallback
177
+ const finalStatusMsg = commitResult?.statusMessage || (commitHash
100
178
  ? `Commit successful: ${commitHash}`
101
- : `Commit successful (stdout: ${stdout.trim()})`;
102
- logger.info(`${operation} executed successfully`, { ...context, operation, path: targetPath, 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)
103
181
  return {
104
182
  success: true,
105
- statusMessage: statusMsg,
183
+ statusMessage: finalStatusMsg, // Use potentially modified message
106
184
  commitHash: commitHash
107
185
  };
108
186
  }
109
- catch (error) {
187
+ catch (error) { // This catch block now primarily handles non-signing errors or errors from the fallback attempt
110
188
  logger.error(`Failed to execute git commit command`, { ...context, operation, path: targetPath, error: error.message, stderr: error.stderr });
111
189
  const errorMessage = error.stderr || error.message || '';
112
190
  // Handle specific error cases first
@@ -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."),
@@ -1,10 +1,31 @@
1
- import { ErrorHandler } from '../../../utils/errorHandler.js';
2
- import { logger } from '../../../utils/logger.js';
3
- import { requestContextService } from '../../../utils/requestContext.js';
1
+ import path from 'path';
2
+ import { z } from 'zod';
3
+ import { BaseErrorCode, McpError } from '../../../types-global/errors.js';
4
+ import { ErrorHandler, logger, requestContextService, sanitization } from '../../../utils/index.js';
4
5
  import { GitInitInputSchema, gitInitLogic } from './logic.js';
5
- import { BaseErrorCode } from '../../../types-global/errors.js';
6
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.';
7
+ const TOOL_DESCRIPTION = 'Initializes a new Git repository at the specified path. If path is relative or omitted, it resolves against the session working directory (if you have set the git_working_dir). Can optionally set the initial branch name and create a bare repository.';
8
+ const RegistrationSchema = GitInitInputSchema.extend({
9
+ path: z.string().min(1).optional().default('.'),
10
+ }).shape;
11
+ // --- Module-level State Accessors ---
12
+ // These will be populated by the initialize function called from server.ts
13
+ let _getWorkingDirectory = () => undefined;
14
+ let _getSessionIdFromContext = () => undefined;
15
+ /**
16
+ * Initializes state accessor functions for the git_init tool.
17
+ * This function is called by the main server setup to provide the tool
18
+ * with a way to access session-specific state (like the working directory)
19
+ * without needing direct access to the server or transport layer internals.
20
+ *
21
+ * @param getWorkingDirectory - Function to retrieve the working directory for a given session ID.
22
+ * @param getSessionIdFromContext - Function to extract the session ID from a tool's execution context.
23
+ */
24
+ export function initializeGitInitStateAccessors(getWorkingDirectory, getSessionIdFromContext) {
25
+ _getWorkingDirectory = getWorkingDirectory;
26
+ _getSessionIdFromContext = getSessionIdFromContext;
27
+ logger.debug(`State accessors initialized for ${TOOL_NAME}`);
28
+ }
8
29
  /**
9
30
  * Registers the git_init tool with the MCP server.
10
31
  *
@@ -16,15 +37,48 @@ export const registerGitInitTool = async (server) => {
16
37
  const operation = 'registerGitInitTool';
17
38
  const context = requestContextService.createRequestContext({ operation });
18
39
  await ErrorHandler.tryCatch(async () => {
19
- server.tool(TOOL_NAME, TOOL_DESCRIPTION, GitInitInputSchema.shape, // Provide the Zod schema shape
20
- async (validatedArgs, callContext) => {
40
+ server.tool(TOOL_NAME, TOOL_DESCRIPTION, RegistrationSchema, async (validatedArgs, callContext) => {
21
41
  const toolOperation = 'tool:git_init';
22
42
  const requestContext = requestContextService.createRequestContext({ operation: toolOperation, parentContext: callContext });
43
+ // Use the initialized accessor to get the session ID
44
+ const sessionId = _getSessionIdFromContext(requestContext); // Pass the created context
45
+ if (!sessionId && !path.isAbsolute(validatedArgs.path)) {
46
+ // If path is relative, we NEED a session ID to resolve against a potential working dir
47
+ logger.error('Session ID is missing in context, cannot resolve relative path', requestContext);
48
+ throw new McpError(BaseErrorCode.INTERNAL_ERROR, 'Session context is unavailable for relative path resolution.', { context: requestContext, operation: toolOperation });
49
+ }
23
50
  logger.info(`Executing tool: ${TOOL_NAME}`, requestContext);
24
51
  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
52
+ // Use the initialized accessor to get the working directory
53
+ const sessionWorkingDirectory = _getWorkingDirectory(sessionId);
54
+ const inputPath = validatedArgs.path;
55
+ let resolvedPath;
56
+ try {
57
+ if (path.isAbsolute(inputPath)) {
58
+ resolvedPath = sanitization.sanitizePath(inputPath);
59
+ logger.debug(`Using absolute path: ${resolvedPath}`, requestContext);
60
+ }
61
+ else if (sessionWorkingDirectory) {
62
+ resolvedPath = sanitization.sanitizePath(path.resolve(sessionWorkingDirectory, inputPath));
63
+ logger.debug(`Resolved relative path '${inputPath}' to absolute path: ${resolvedPath} using session CWD`, requestContext);
64
+ }
65
+ else {
66
+ // This case should now only be hit if the path is relative AND there's no session CWD set.
67
+ logger.error(`Relative path '${inputPath}' provided but no session working directory is set.`, requestContext);
68
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Relative path '${inputPath}' provided but no session working directory is set. Please provide an absolute path or set a working directory using git_set_working_dir.`, { context: requestContext, operation: toolOperation });
69
+ }
70
+ }
71
+ catch (error) {
72
+ logger.error('Path resolution or sanitization failed', { ...requestContext, operation: toolOperation, error });
73
+ if (error instanceof McpError)
74
+ throw error;
75
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path processing: ${error instanceof Error ? error.message : String(error)}`, { context: requestContext, operation: toolOperation, originalError: error });
76
+ }
77
+ const logicArgs = {
78
+ ...validatedArgs,
79
+ path: resolvedPath,
80
+ };
81
+ const initResult = await gitInitLogic(logicArgs, requestContext);
28
82
  const resultContent = {
29
83
  type: 'text',
30
84
  text: JSON.stringify(initResult, null, 2), // Pretty-print JSON
@@ -36,9 +90,9 @@ export const registerGitInitTool = async (server) => {
36
90
  operation: toolOperation,
37
91
  context: requestContext,
38
92
  input: validatedArgs,
39
- errorCode: BaseErrorCode.INTERNAL_ERROR, // Default if unexpected error occurs
93
+ errorCode: BaseErrorCode.INTERNAL_ERROR,
40
94
  });
41
95
  });
42
96
  logger.info(`Tool registered: ${TOOL_NAME}`, context);
43
- }, { operation, context, critical: true }); // Mark registration as critical
97
+ }, { operation, context, critical: true });
44
98
  };
@@ -1,9 +1,12 @@
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 structure for a single commit entry
9
12
  export const CommitEntrySchema = z.object({
@@ -16,13 +19,14 @@ export const CommitEntrySchema = z.object({
16
19
  });
17
20
  // Define the input schema for the git_log tool using Zod
18
21
  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."),
22
+ 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."),
20
23
  maxCount: z.number().int().positive().optional().describe("Limit the number of commits to output."),
21
24
  author: z.string().optional().describe("Limit commits to those matching the specified author pattern."),
22
25
  since: z.string().optional().describe("Show commits more recent than a specific date (e.g., '2 weeks ago', '2023-01-01')."),
23
26
  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.
27
+ branchOrFile: z.string().optional().describe("Show logs for a specific branch (e.g., 'main'), tag, or file path (e.g., 'src/utils/logger.ts')."),
28
+ showSignature: z.boolean().optional().default(false).describe("Show signature verification status for commits. Returns raw output instead of parsed JSON."),
29
+ // Note: We use a fixed pretty format for reliable parsing unless showSignature is true.
26
30
  });
27
31
  // Delimiters for parsing the custom format
28
32
  const FIELD_SEP = '\x1f'; // Unit Separator
@@ -62,11 +66,29 @@ export async function logGitHistory(input, context) {
62
66
  throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
63
67
  }
64
68
  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}`;
69
+ let command;
70
+ let isRawOutput = false; // Flag to indicate if we should parse or return raw
71
+ if (input.showSignature) {
72
+ isRawOutput = true;
73
+ command = `git -C "${targetPath}" log --show-signature`;
74
+ logger.info('Show signature requested, returning raw output.', { ...context, operation });
75
+ // Append other filters if provided
76
+ if (input.maxCount)
77
+ command += ` -n ${input.maxCount}`;
78
+ if (input.author)
79
+ command += ` --author="${input.author.replace(/[`"$&;*()|<>]/g, '')}"`;
80
+ if (input.since)
81
+ command += ` --since="${input.since.replace(/[`"$&;*()|<>]/g, '')}"`;
82
+ if (input.until)
83
+ command += ` --until="${input.until.replace(/[`"$&;*()|<>]/g, '')}"`;
84
+ if (input.branchOrFile)
85
+ command += ` ${input.branchOrFile.replace(/[`"$&;*()|<>]/g, '')}`;
86
+ }
87
+ else {
88
+ // Construct the git log command with the fixed format for parsing
89
+ command = `git -C "${targetPath}" log ${GIT_LOG_FORMAT}`;
90
+ if (input.maxCount)
91
+ command += ` -n ${input.maxCount}`;
70
92
  }
71
93
  if (input.author) {
72
94
  // Basic sanitization for author string
@@ -90,13 +112,28 @@ export async function logGitHistory(input, context) {
90
112
  const { stdout, stderr } = await execAsync(command, { maxBuffer: 1024 * 1024 * 10 }); // 10MB buffer
91
113
  if (stderr) {
92
114
  // Log stderr as warning, as git log might sometimes use it for non-fatal info
93
- logger.warning(`Git log stderr: ${stderr}`, { ...context, operation });
115
+ // Exception: If showing signature, stderr about allowedSignersFile is expected, treat as info
116
+ if (isRawOutput && stderr.includes('allowedSignersFile needs to be configured')) {
117
+ logger.info(`Git log stderr (signature verification note): ${stderr.trim()}`, { ...context, operation });
118
+ }
119
+ else {
120
+ logger.warning(`Git log stderr: ${stderr.trim()}`, { ...context, operation });
121
+ }
122
+ }
123
+ // If raw output was requested, return it directly
124
+ if (isRawOutput) {
125
+ const message = `Raw log output (showSignature=true):\n${stdout}`;
126
+ logger.info(`${operation} completed successfully (raw output).`, { ...context, operation, path: targetPath });
127
+ return { success: true, commits: [], message: message };
94
128
  }
95
- // Parse the output
129
+ // Otherwise, parse the structured output
96
130
  const commits = [];
97
131
  const commitRecords = stdout.split(RECORD_SEP).filter(record => record.trim() !== ''); // Split records and remove empty ones
98
132
  for (const record of commitRecords) {
99
- const fields = record.split(FIELD_SEP);
133
+ const trimmedRecord = record.trim(); // Trim leading/trailing whitespace (like newlines)
134
+ if (!trimmedRecord)
135
+ continue; // Skip empty records after trimming
136
+ const fields = trimmedRecord.split(FIELD_SEP); // Split the trimmed record
100
137
  if (fields.length >= 5) { // Need at least hash, name, email, timestamp, subject
101
138
  try {
102
139
  const commitEntry = {
@@ -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 { GitLogInputSchema, logGitHistory } from './logic.js';
5
- import { BaseErrorCode } from '../../../types-global/errors.js';
6
9
  let _getWorkingDirectory;
7
10
  let _getSessionId;
8
11
  /**
@@ -17,7 +20,7 @@ export function initializeGitLogStateAccessors(getWdFn, getSidFn) {
17
20
  logger.info('State accessors initialized for git_log tool registration.');
18
21
  }
19
22
  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.";
23
+ 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 by default. Can optionally show signature verification status (`showSignature: true`), which returns raw text output instead of JSON.";
21
24
  /**
22
25
  * Registers the git_log tool with the MCP server.
23
26
  *