@cyanheads/git-mcp-server 2.0.3 → 2.0.6

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.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![TypeScript](https://img.shields.io/badge/TypeScript-^5.8.3-blue.svg)](https://www.typescriptlang.org/)
4
4
  [![Model Context Protocol](https://img.shields.io/badge/MCP%20SDK-^1.11.0-green.svg)](https://modelcontextprotocol.io/)
5
- [![Version](https://img.shields.io/badge/Version-2.0.3-blue.svg)](./CHANGELOG.md)
5
+ [![Version](https://img.shields.io/badge/Version-2.0.6-blue.svg)](./CHANGELOG.md)
6
6
  [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
7
7
  [![Status](https://img.shields.io/badge/Status-Stable-green.svg)](https://github.com/cyanheads/git-mcp-server/issues)
8
8
  [![GitHub](https://img.shields.io/github/stars/cyanheads/git-mcp-server?style=social)](https://github.com/cyanheads/git-mcp-server)
@@ -17,30 +17,30 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
17
17
  import { config, environment } from '../config/index.js';
18
18
  // Import core utilities: ErrorHandler, logger, requestContextService.
19
19
  import { ErrorHandler, logger, requestContextService } from '../utils/index.js'; // Added RequestContext
20
- // Import registration AND state initialization functions for ALL Git tools (assuming pattern)
20
+ // Import registration AND state initialization functions for ALL Git tools (alphabetized)
21
21
  import { registerGitAddTool, initializeGitAddStateAccessors } from './tools/gitAdd/index.js';
22
22
  import { registerGitBranchTool, initializeGitBranchStateAccessors } from './tools/gitBranch/index.js';
23
- import { registerGitCheckoutTool, initializeGitCheckoutStateAccessors } from './tools/gitCheckout/index.js'; // Assumed initializer
24
- import { registerGitCherryPickTool, initializeGitCherryPickStateAccessors } from './tools/gitCherryPick/index.js'; // Assumed initializer
25
- import { registerGitCleanTool, initializeGitCleanStateAccessors } from './tools/gitClean/index.js'; // Assumed initializer
26
- import { registerGitClearWorkingDirTool, initializeGitClearWorkingDirStateAccessors } from './tools/gitClearWorkingDir/index.js'; // Assumed initializer
27
- import { registerGitCloneTool } from './tools/gitClone/index.js'; // Removed initializer import
28
- import { registerGitCommitTool, initializeGitCommitStateAccessors } from './tools/gitCommit/index.js'; // Assumed initializer
29
- import { registerGitDiffTool, initializeGitDiffStateAccessors } from './tools/gitDiff/index.js'; // Assumed initializer
23
+ import { registerGitCheckoutTool, initializeGitCheckoutStateAccessors } from './tools/gitCheckout/index.js';
24
+ import { registerGitCherryPickTool, initializeGitCherryPickStateAccessors } from './tools/gitCherryPick/index.js';
25
+ import { registerGitCleanTool, initializeGitCleanStateAccessors } from './tools/gitClean/index.js';
26
+ import { registerGitClearWorkingDirTool, initializeGitClearWorkingDirStateAccessors } from './tools/gitClearWorkingDir/index.js';
27
+ import { registerGitCloneTool } from './tools/gitClone/index.js'; // No initializer needed/available
28
+ import { registerGitCommitTool, initializeGitCommitStateAccessors } from './tools/gitCommit/index.js';
29
+ import { registerGitDiffTool, initializeGitDiffStateAccessors } from './tools/gitDiff/index.js';
30
30
  import { registerGitFetchTool, initializeGitFetchStateAccessors } from './tools/gitFetch/index.js';
31
- import { registerGitInitTool, initializeGitInitStateAccessors } from './tools/gitInit/index.js'; // Added initializer import
32
- import { registerGitLogTool, initializeGitLogStateAccessors } from './tools/gitLog/index.js'; // Assumed initializer
33
- import { registerGitMergeTool, initializeGitMergeStateAccessors } from './tools/gitMerge/index.js'; // Assumed initializer
31
+ import { registerGitInitTool, initializeGitInitStateAccessors } from './tools/gitInit/index.js';
32
+ import { registerGitLogTool, initializeGitLogStateAccessors } from './tools/gitLog/index.js';
33
+ import { registerGitMergeTool, initializeGitMergeStateAccessors } from './tools/gitMerge/index.js';
34
34
  import { registerGitPullTool, initializeGitPullStateAccessors } from './tools/gitPull/index.js';
35
35
  import { registerGitPushTool, initializeGitPushStateAccessors } from './tools/gitPush/index.js';
36
- import { registerGitRebaseTool, initializeGitRebaseStateAccessors } from './tools/gitRebase/index.js'; // Assumed initializer
37
- import { registerGitRemoteTool, initializeGitRemoteStateAccessors } from './tools/gitRemote/index.js'; // Assumed initializer
38
- import { registerGitResetTool, initializeGitResetStateAccessors } from './tools/gitReset/index.js'; // Assumed initializer
36
+ import { registerGitRebaseTool, initializeGitRebaseStateAccessors } from './tools/gitRebase/index.js';
37
+ import { registerGitRemoteTool, initializeGitRemoteStateAccessors } from './tools/gitRemote/index.js';
38
+ import { registerGitResetTool, initializeGitResetStateAccessors } from './tools/gitReset/index.js';
39
39
  import { registerGitSetWorkingDirTool, initializeGitSetWorkingDirStateAccessors } from './tools/gitSetWorkingDir/index.js';
40
- import { registerGitShowTool, initializeGitShowStateAccessors } from './tools/gitShow/index.js'; // Assumed initializer
41
- import { registerGitStashTool, initializeGitStashStateAccessors } from './tools/gitStash/index.js'; // Assumed initializer
42
- import { registerGitStatusTool, initializeGitStatusStateAccessors } from './tools/gitStatus/index.js'; // Assumed initializer
43
- import { registerGitTagTool, initializeGitTagStateAccessors } from './tools/gitTag/index.js'; // Assumed initializer
40
+ import { registerGitShowTool, initializeGitShowStateAccessors } from './tools/gitShow/index.js';
41
+ import { registerGitStashTool, initializeGitStashStateAccessors } from './tools/gitStash/index.js';
42
+ import { registerGitStatusTool, initializeGitStatusStateAccessors } from './tools/gitStatus/index.js';
43
+ import { registerGitTagTool, initializeGitTagStateAccessors } from './tools/gitTag/index.js';
44
44
  // Import transport setup functions AND state accessors
45
45
  import { startHttpTransport, getHttpSessionWorkingDirectory, setHttpSessionWorkingDirectory } from './transports/httpTransport.js';
46
46
  import { connectStdioTransport, getStdioWorkingDirectory, setStdioWorkingDirectory } from './transports/stdioTransport.js';
@@ -132,7 +132,7 @@ async function createMcpServerInstance() {
132
132
  // Pass the defined unified accessor functions to the initializers.
133
133
  logger.debug('Initializing state accessors for tools...', context);
134
134
  try {
135
- // Call initializers for all tools that likely need state access.
135
+ // Call initializers for all tools that likely need state access (alphabetized).
136
136
  // If an initializer doesn't exist, the import would have failed earlier (or build will fail).
137
137
  initializeGitAddStateAccessors(getWorkingDirectory, getSessionIdFromContext);
138
138
  initializeGitBranchStateAccessors(getWorkingDirectory, getSessionIdFromContext);
@@ -140,11 +140,11 @@ async function createMcpServerInstance() {
140
140
  initializeGitCherryPickStateAccessors(getWorkingDirectory, getSessionIdFromContext);
141
141
  initializeGitCleanStateAccessors(getWorkingDirectory, getSessionIdFromContext);
142
142
  initializeGitClearWorkingDirStateAccessors(getWorkingDirectory, getSessionIdFromContext);
143
- // initializeGitCloneStateAccessors(getWorkingDirectory, getSessionIdFromContext); // Removed call
143
+ // initializeGitCloneStateAccessors - No initializer needed/available
144
144
  initializeGitCommitStateAccessors(getWorkingDirectory, getSessionIdFromContext);
145
145
  initializeGitDiffStateAccessors(getWorkingDirectory, getSessionIdFromContext);
146
146
  initializeGitFetchStateAccessors(getWorkingDirectory, getSessionIdFromContext);
147
- initializeGitInitStateAccessors(getWorkingDirectory, getSessionIdFromContext); // Added call
147
+ initializeGitInitStateAccessors(getWorkingDirectory, getSessionIdFromContext);
148
148
  initializeGitLogStateAccessors(getWorkingDirectory, getSessionIdFromContext);
149
149
  initializeGitMergeStateAccessors(getWorkingDirectory, getSessionIdFromContext);
150
150
  initializeGitPullStateAccessors(getWorkingDirectory, getSessionIdFromContext);
@@ -169,7 +169,7 @@ async function createMcpServerInstance() {
169
169
  throw initError; // Re-throw to prevent server starting incorrectly
170
170
  }
171
171
  try {
172
- // Register all defined Git tools. These calls populate the server's
172
+ // Register all defined Git tools (alphabetized). These calls populate the server's
173
173
  // internal registry, making them available via MCP methods like 'tools/list'.
174
174
  logger.debug('Registering Git tools...', context);
175
175
  await registerGitAddTool(server);
@@ -20,6 +20,7 @@ export const GitCommitInputSchema = z.object({
20
20
  allowEmpty: z.boolean().default(false).describe('Allow creating empty commits'),
21
21
  amend: z.boolean().default(false).describe('Amend the previous commit instead of creating a new one'),
22
22
  forceUnsignedOnFailure: z.boolean().default(false).describe('If true and signing is enabled but fails, attempt the commit without signing instead of failing.'),
23
+ filesToStage: z.array(z.string().min(1)).optional().describe('Optional array of specific file paths (relative to the repository root) to stage automatically before committing. If provided, only these files will be staged.'),
23
24
  });
24
25
  /**
25
26
  * Executes the 'git commit' command and returns structured JSON output.
@@ -61,6 +62,24 @@ export async function commitGitChanges(input, context // Add getter to context
61
62
  throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
62
63
  }
63
64
  try {
65
+ // --- Stage specific files if requested ---
66
+ if (input.filesToStage && input.filesToStage.length > 0) {
67
+ logger.debug(`Attempting to stage specific files: ${input.filesToStage.join(', ')}`, { ...context, operation });
68
+ try {
69
+ // Correctly pass targetPath as rootDir in options object
70
+ const sanitizedFiles = input.filesToStage.map(file => sanitization.sanitizePath(file, { rootDir: targetPath })); // Sanitize relative to repo root
71
+ const filesToAddString = sanitizedFiles.map(file => `"${file}"`).join(' '); // Quote paths for safety
72
+ const addCommand = `git -C "${targetPath}" add -- ${filesToAddString}`;
73
+ logger.debug(`Executing git add command: ${addCommand}`, { ...context, operation });
74
+ await execAsync(addCommand);
75
+ logger.info(`Successfully staged specified files: ${sanitizedFiles.join(', ')}`, { ...context, operation });
76
+ }
77
+ catch (addError) {
78
+ logger.error('Failed to stage specified files', { ...context, operation, files: input.filesToStage, error: addError.message, stderr: addError.stderr });
79
+ throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to stage files before commit: ${addError.stderr || addError.message}`, { context, operation, originalError: addError });
80
+ }
81
+ }
82
+ // --- End staging files ---
64
83
  // Escape message for shell safety
65
84
  const escapeShellArg = (arg) => {
66
85
  // Escape backslashes first, then other special chars
@@ -177,11 +196,28 @@ export async function commitGitChanges(input, context // Add getter to context
177
196
  const finalStatusMsg = commitResult?.statusMessage || (commitHash
178
197
  ? `Commit successful: ${commitHash}`
179
198
  : `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)
199
+ let committedFiles = [];
200
+ if (commitHash) {
201
+ try {
202
+ // Get the list of files included in this specific commit
203
+ const showCommand = `git -C "${targetPath}" show --pretty="" --name-only ${commitHash}`;
204
+ logger.debug(`Executing git show command: ${showCommand}`, { ...context, operation });
205
+ const { stdout: showStdout } = await execAsync(showCommand);
206
+ committedFiles = showStdout.trim().split('\n').filter(Boolean); // Split by newline, remove empty lines
207
+ logger.debug(`Retrieved committed files list for ${commitHash}`, { ...context, operation, count: committedFiles.length });
208
+ }
209
+ catch (showError) {
210
+ // Log a warning but don't fail the overall operation if we can't get the file list
211
+ logger.warning('Failed to retrieve committed files list', { ...context, operation, commitHash, error: showError.message, stderr: showError.stderr });
212
+ }
213
+ }
214
+ logger.info(`${operation} executed successfully`, { ...context, operation, path: targetPath, commitHash, signed: !commitResult, committedFilesCount: committedFiles.length }); // Log if it was signed (not fallback)
181
215
  return {
182
216
  success: true,
183
217
  statusMessage: finalStatusMsg, // Use potentially modified message
184
- commitHash: commitHash
218
+ commitHash: commitHash,
219
+ commitMessage: input.message, // Include the original commit message
220
+ committedFiles: committedFiles // Include the list of files
185
221
  };
186
222
  }
187
223
  catch (error) { // This catch block now primarily handles non-signing errors or errors from the fallback attempt
@@ -41,12 +41,9 @@ feat(auth): implement password reset endpoint
41
41
  Closes #123 (if applicable).
42
42
  \`\`\`
43
43
 
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.
45
-
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.`;
44
+ **Tool Options & Behavior:**
45
+ - Commit related changes logically. Use the optional \`filesToStage\` parameter to auto-stage specific files before committing.
46
+ - The \`path\` defaults to the session's working directory unless overridden. If \`GIT_SIGN_COMMITS=true\` is set, commits are signed (\`-S\`), with an optional \`forceUnsignedOnFailure\` fallback.`;
50
47
  /**
51
48
  * Registers the git_commit tool with the MCP server.
52
49
  * Uses the high-level server.tool() method for registration, schema validation, and routing.
@@ -120,11 +120,12 @@ export async function logGitHistory(input, context) {
120
120
  logger.warning(`Git log stderr: ${stderr.trim()}`, { ...context, operation });
121
121
  }
122
122
  }
123
- // If raw output was requested, return it directly
123
+ // If raw output was requested, return it directly in the message field, omitting commits
124
124
  if (isRawOutput) {
125
125
  const message = `Raw log output (showSignature=true):\n${stdout}`;
126
126
  logger.info(`${operation} completed successfully (raw output).`, { ...context, operation, path: targetPath });
127
- return { success: true, commits: [], message: message };
127
+ // Return without the 'commits' field
128
+ return { success: true, message: message };
128
129
  }
129
130
  // Otherwise, parse the structured output
130
131
  const commits = [];
@@ -20,7 +20,7 @@ export function initializeGitLogStateAccessors(getWdFn, getSidFn) {
20
20
  logger.info('State accessors initialized for git_log tool registration.');
21
21
  }
22
22
  const TOOL_NAME = 'git_log';
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.";
23
+ const TOOL_DESCRIPTION = "Shows commit logs for the repository. Supports limiting count, filtering by author, date range, and specific branch/file. Returns a JSON object containing a list of commit objects (`commits` array) by default. If `showSignature: true` is used, it returns a JSON object where the `commits` array is empty and the raw signature verification output is included in the `message` field.";
24
24
  /**
25
25
  * Registers the git_log tool with the MCP server.
26
26
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyanheads/git-mcp-server",
3
- "version": "2.0.3",
3
+ "version": "2.0.6",
4
4
  "description": "An MCP (Model Context Protocol) server providing tools to interact with Git repositories. Enables LLMs and AI agents to perform Git operations like clone, commit, push, pull, branch, diff, log, status, and more via the MCP standard.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -38,8 +38,8 @@
38
38
  "dependencies": {
39
39
  "@modelcontextprotocol/sdk": "^1.11.0",
40
40
  "@types/jsonwebtoken": "^9.0.9",
41
- "@types/node": "^22.15.4",
42
- "@types/sanitize-html": "^2.15.0",
41
+ "@types/node": "^22.15.9",
42
+ "@types/sanitize-html": "^2.16.0",
43
43
  "@types/validator": "^13.15.0",
44
44
  "chrono-node": "^2.8.0",
45
45
  "dotenv": "^16.5.0",