@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 +1 -1
- package/dist/mcp-server/server.js +22 -22
- package/dist/mcp-server/tools/gitCommit/logic.js +38 -2
- package/dist/mcp-server/tools/gitCommit/registration.js +3 -6
- package/dist/mcp-server/tools/gitLog/logic.js +3 -2
- package/dist/mcp-server/tools/gitLog/registration.js +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.typescriptlang.org/)
|
|
4
4
|
[](https://modelcontextprotocol.io/)
|
|
5
|
-
[](./CHANGELOG.md)
|
|
6
6
|
[](https://opensource.org/licenses/Apache-2.0)
|
|
7
7
|
[](https://github.com/cyanheads/git-mcp-server/issues)
|
|
8
8
|
[](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 (
|
|
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';
|
|
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'; //
|
|
28
|
-
import { registerGitCommitTool, initializeGitCommitStateAccessors } from './tools/gitCommit/index.js';
|
|
29
|
-
import { registerGitDiffTool, initializeGitDiffStateAccessors } from './tools/gitDiff/index.js';
|
|
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';
|
|
32
|
-
import { registerGitLogTool, initializeGitLogStateAccessors } from './tools/gitLog/index.js';
|
|
33
|
-
import { registerGitMergeTool, initializeGitMergeStateAccessors } from './tools/gitMerge/index.js';
|
|
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';
|
|
37
|
-
import { registerGitRemoteTool, initializeGitRemoteStateAccessors } from './tools/gitRemote/index.js';
|
|
38
|
-
import { registerGitResetTool, initializeGitResetStateAccessors } from './tools/gitReset/index.js';
|
|
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';
|
|
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';
|
|
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
|
|
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);
|
|
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
|
-
|
|
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
|
-
**
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
+
"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.
|
|
42
|
-
"@types/sanitize-html": "^2.
|
|
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",
|