@cyanheads/git-mcp-server 2.0.11 → 2.0.14
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 +33 -29
- package/dist/mcp-server/server.js +29 -23
- package/dist/mcp-server/tools/gitDiff/logic.js +85 -20
- package/dist/mcp-server/tools/gitDiff/registration.js +1 -1
- package/dist/mcp-server/tools/gitInit/logic.js +3 -5
- package/dist/mcp-server/tools/gitSetWorkingDir/logic.js +43 -16
- package/dist/mcp-server/tools/gitSetWorkingDir/registration.js +1 -1
- package/dist/mcp-server/tools/gitWorktree/index.js +7 -0
- package/dist/mcp-server/tools/gitWorktree/logic.js +239 -0
- package/dist/mcp-server/tools/gitWorktree/registration.js +67 -0
- package/dist/mcp-server/tools/gitWrapupInstructions/index.js +3 -0
- package/dist/mcp-server/tools/gitWrapupInstructions/logic.js +29 -0
- package/dist/mcp-server/tools/gitWrapupInstructions/registration.js +42 -0
- package/dist/mcp-server/transports/authentication/authMiddleware.js +127 -105
- package/dist/mcp-server/transports/httpTransport.js +14 -0
- package/package.json +33 -21
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Git MCP Server
|
|
2
2
|
|
|
3
3
|
[](https://www.typescriptlang.org/)
|
|
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)
|
|
@@ -129,6 +129,8 @@ Add to your MCP client settings (e.g., `cline_mcp_settings.json`):
|
|
|
129
129
|
}
|
|
130
130
|
```
|
|
131
131
|
|
|
132
|
+
**Note**: You can see [mcp.json](mcp.json) for an example MCP client configuration file that includes the Git MCP Server.\*
|
|
133
|
+
|
|
132
134
|
## Project Structure
|
|
133
135
|
|
|
134
136
|
The codebase follows a modular structure within the `src/` directory:
|
|
@@ -153,39 +155,41 @@ For a detailed file tree, run `npm run tree` or see [docs/tree.md](docs/tree.md)
|
|
|
153
155
|
|
|
154
156
|
The Git MCP Server provides a suite of tools for interacting with Git repositories, callable via the Model Context Protocol.
|
|
155
157
|
|
|
156
|
-
| Tool Name
|
|
157
|
-
|
|
|
158
|
-
| `git_add`
|
|
159
|
-
| `git_branch`
|
|
160
|
-
| `git_checkout`
|
|
161
|
-
| `git_cherry_pick`
|
|
162
|
-
| `git_clean`
|
|
163
|
-
| `git_clear_working_dir`
|
|
164
|
-
| `git_clone`
|
|
165
|
-
| `git_commit`
|
|
166
|
-
| `git_diff`
|
|
167
|
-
| `git_fetch`
|
|
168
|
-
| `git_init`
|
|
169
|
-
| `git_log`
|
|
170
|
-
| `git_merge`
|
|
171
|
-
| `git_pull`
|
|
172
|
-
| `git_push`
|
|
173
|
-
| `git_rebase`
|
|
174
|
-
| `git_remote`
|
|
175
|
-
| `git_reset`
|
|
176
|
-
| `git_set_working_dir`
|
|
177
|
-
| `git_show`
|
|
178
|
-
| `git_stash`
|
|
179
|
-
| `git_status`
|
|
180
|
-
| `git_tag`
|
|
158
|
+
| Tool Name | Description | Key Arguments |
|
|
159
|
+
| :------------------------ | :--------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------ |
|
|
160
|
+
| `git_add` | Stages specified files or patterns. | `path?`, `files?` |
|
|
161
|
+
| `git_branch` | Manages branches (list, create, delete, rename, show current). | `path?`, `mode`, `branchName?`, `newBranchName?`, `startPoint?`, `force?`, `all?`, `remote?` |
|
|
162
|
+
| `git_checkout` | Switches branches or restores working tree files. | `path?`, `branchOrPath`, `newBranch?`, `force?` |
|
|
163
|
+
| `git_cherry_pick` | Applies changes introduced by existing commits. | `path?`, `commitRef`, `mainline?`, `strategy?`, `noCommit?`, `signoff?` |
|
|
164
|
+
| `git_clean` | Removes untracked files. **Requires `force: true`**. | `path?`, `force`, `dryRun?`, `directories?`, `ignored?` |
|
|
165
|
+
| `git_clear_working_dir` | Clears the session-specific working directory. | (none) |
|
|
166
|
+
| `git_clone` | Clones a repository into a specified absolute path. | `repositoryUrl`, `targetPath`, `branch?`, `depth?`, `quiet?` |
|
|
167
|
+
| `git_commit` | Commits staged changes. Supports author override, signing control. | `path?`, `message`, `author?`, `allowEmpty?`, `amend?`, `forceUnsignedOnFailure?` |
|
|
168
|
+
| `git_diff` | Shows changes between commits, working tree, etc. | `path?`, `commit1?`, `commit2?`, `staged?`, `file?`, `includeUntracked?` |
|
|
169
|
+
| `git_fetch` | Downloads objects and refs from other repositories. | `path?`, `remote?`, `prune?`, `tags?`, `all?` |
|
|
170
|
+
| `git_init` | Initializes a new Git repository at the specified absolute path. Defaults to 'main' for initial branch. | `path`, `initialBranch?`, `bare?`, `quiet?` |
|
|
171
|
+
| `git_log` | Shows commit logs. | `path?`, `maxCount?`, `author?`, `since?`, `until?`, `branchOrFile?` |
|
|
172
|
+
| `git_merge` | Merges the specified branch into the current branch. | `path?`, `branch`, `commitMessage?`, `noFf?`, `squash?`, `abort?` |
|
|
173
|
+
| `git_pull` | Fetches from and integrates with another repository or local branch. | `path?`, `remote?`, `branch?`, `rebase?`, `ffOnly?` |
|
|
174
|
+
| `git_push` | Updates remote refs using local refs. | `path?`, `remote?`, `branch?`, `remoteBranch?`, `force?`, `forceWithLease?`, `setUpstream?`, `tags?`, `delete?` |
|
|
175
|
+
| `git_rebase` | Reapplies commits on top of another base tip. | `path?`, `mode?`, `upstream?`, `branch?`, `interactive?`, `strategy?`, `strategyOption?`, `onto?` |
|
|
176
|
+
| `git_remote` | Manages remote repositories (list, add, remove, show). | `path?`, `mode`, `name?`, `url?` |
|
|
177
|
+
| `git_reset` | Resets current HEAD to a specified state. Supports soft, mixed, hard modes. **USE 'hard' WITH CAUTION**. | `path?`, `mode?`, `commit?` |
|
|
178
|
+
| `git_set_working_dir` | Sets the default working directory. Can optionally initialize repo if not present. Requires absolute path. | `path`, `validateGitRepo?`, `initializeIfNotPresent?` |
|
|
179
|
+
| `git_show` | Shows information about Git objects (commits, tags, etc.). | `path?`, `ref`, `filePath?` |
|
|
180
|
+
| `git_stash` | Manages stashed changes (list, apply, pop, drop, save). | `path?`, `mode`, `stashRef?`, `message?` |
|
|
181
|
+
| `git_status` | Gets repository status (branch, staged, modified, untracked files). | `path?` |
|
|
182
|
+
| `git_tag` | Manages tags (list, create annotated/lightweight, delete). | `path?`, `mode`, `tagName?`, `message?`, `commitRef?`, `annotate?` |
|
|
183
|
+
| `git_worktree` | Manages Git worktrees (list, add, remove, move, prune). | `path?`, `mode`, `worktreePath?`, `commitish?`, `newBranch?`, `force?`, `detach?`, `newPath?`, `verbose?`, `dryRun?`, `expire?` |
|
|
184
|
+
| `git_wrapup_instructions` | Provides a standard Git wrap-up workflow. | `acknowledgement`, `updateAgentMetaFiles?` |
|
|
181
185
|
|
|
182
186
|
_Note: The `path` parameter for most tools defaults to the session's working directory if set via `git_set_working_dir`._
|
|
183
187
|
|
|
184
188
|
## Resources
|
|
185
189
|
|
|
186
|
-
**MCP Resources are not implemented in this version (v2.0.
|
|
190
|
+
**MCP Resources are not implemented in this version (v2.0.14).**
|
|
187
191
|
|
|
188
|
-
This version focuses on the refactored Git tools implementation based on the latest `mcp-ts-template` and MCP SDK v1.
|
|
192
|
+
This version focuses on the refactored Git tools implementation based on the latest `mcp-ts-template` and MCP SDK v1.12.0. Resource capabilities, previously available, have been temporarily removed during this major update.
|
|
189
193
|
|
|
190
194
|
If you require MCP Resource access (e.g., for reading file content directly via the server), please use the stable **[v1.2.4 release](https://github.com/cyanheads/git-mcp-server/releases/tag/v1.2.4)**.
|
|
191
195
|
|
|
@@ -18,31 +18,33 @@ 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
20
|
// Import registration AND state initialization functions for ALL Git tools (alphabetized)
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
21
|
+
import { initializeGitAddStateAccessors, registerGitAddTool } from './tools/gitAdd/index.js';
|
|
22
|
+
import { initializeGitBranchStateAccessors, registerGitBranchTool } from './tools/gitBranch/index.js';
|
|
23
|
+
import { initializeGitCheckoutStateAccessors, registerGitCheckoutTool } from './tools/gitCheckout/index.js';
|
|
24
|
+
import { initializeGitCherryPickStateAccessors, registerGitCherryPickTool } from './tools/gitCherryPick/index.js';
|
|
25
|
+
import { initializeGitCleanStateAccessors, registerGitCleanTool } from './tools/gitClean/index.js';
|
|
26
|
+
import { initializeGitClearWorkingDirStateAccessors, registerGitClearWorkingDirTool } from './tools/gitClearWorkingDir/index.js';
|
|
27
27
|
import { registerGitCloneTool } from './tools/gitClone/index.js'; // No initializer needed/available
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
33
|
-
import {
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
36
|
-
import {
|
|
37
|
-
import {
|
|
38
|
-
import {
|
|
39
|
-
import {
|
|
40
|
-
import {
|
|
41
|
-
import {
|
|
42
|
-
import {
|
|
43
|
-
import {
|
|
28
|
+
import { initializeGitCommitStateAccessors, registerGitCommitTool } from './tools/gitCommit/index.js';
|
|
29
|
+
import { initializeGitDiffStateAccessors, registerGitDiffTool } from './tools/gitDiff/index.js';
|
|
30
|
+
import { initializeGitFetchStateAccessors, registerGitFetchTool } from './tools/gitFetch/index.js';
|
|
31
|
+
import { initializeGitInitStateAccessors, registerGitInitTool } from './tools/gitInit/index.js';
|
|
32
|
+
import { initializeGitLogStateAccessors, registerGitLogTool } from './tools/gitLog/index.js';
|
|
33
|
+
import { initializeGitMergeStateAccessors, registerGitMergeTool } from './tools/gitMerge/index.js';
|
|
34
|
+
import { initializeGitPullStateAccessors, registerGitPullTool } from './tools/gitPull/index.js';
|
|
35
|
+
import { initializeGitPushStateAccessors, registerGitPushTool } from './tools/gitPush/index.js';
|
|
36
|
+
import { initializeGitRebaseStateAccessors, registerGitRebaseTool } from './tools/gitRebase/index.js';
|
|
37
|
+
import { initializeGitRemoteStateAccessors, registerGitRemoteTool } from './tools/gitRemote/index.js';
|
|
38
|
+
import { initializeGitResetStateAccessors, registerGitResetTool } from './tools/gitReset/index.js';
|
|
39
|
+
import { initializeGitSetWorkingDirStateAccessors, registerGitSetWorkingDirTool } from './tools/gitSetWorkingDir/index.js';
|
|
40
|
+
import { initializeGitShowStateAccessors, registerGitShowTool } from './tools/gitShow/index.js';
|
|
41
|
+
import { initializeGitStashStateAccessors, registerGitStashTool } from './tools/gitStash/index.js';
|
|
42
|
+
import { initializeGitStatusStateAccessors, registerGitStatusTool } from './tools/gitStatus/index.js';
|
|
43
|
+
import { initializeGitTagStateAccessors, registerGitTagTool } from './tools/gitTag/index.js';
|
|
44
|
+
import { initializeGitWorktreeStateAccessors, registerGitWorktreeTool } from './tools/gitWorktree/index.js';
|
|
45
|
+
import { registerGitWrapupInstructionsTool } from './tools/gitWrapupInstructions/index.js';
|
|
44
46
|
// Import transport setup functions AND state accessors
|
|
45
|
-
import {
|
|
47
|
+
import { getHttpSessionWorkingDirectory, setHttpSessionWorkingDirectory, startHttpTransport } from './transports/httpTransport.js';
|
|
46
48
|
import { connectStdioTransport, getStdioWorkingDirectory, setStdioWorkingDirectory } from './transports/stdioTransport.js';
|
|
47
49
|
/**
|
|
48
50
|
* Creates and configures a new instance of the McpServer.
|
|
@@ -157,6 +159,8 @@ async function createMcpServerInstance() {
|
|
|
157
159
|
initializeGitStashStateAccessors(getWorkingDirectory, getSessionIdFromContext);
|
|
158
160
|
initializeGitStatusStateAccessors(getWorkingDirectory, getSessionIdFromContext);
|
|
159
161
|
initializeGitTagStateAccessors(getWorkingDirectory, getSessionIdFromContext);
|
|
162
|
+
initializeGitWorktreeStateAccessors(getWorkingDirectory, getSessionIdFromContext);
|
|
163
|
+
// No state accessor initialization needed for gitWrapupInstructionsTool
|
|
160
164
|
logger.debug('State accessors initialized successfully.', context);
|
|
161
165
|
}
|
|
162
166
|
catch (initError) {
|
|
@@ -195,6 +199,8 @@ async function createMcpServerInstance() {
|
|
|
195
199
|
await registerGitStashTool(server);
|
|
196
200
|
await registerGitStatusTool(server);
|
|
197
201
|
await registerGitTagTool(server);
|
|
202
|
+
await registerGitWorktreeTool(server);
|
|
203
|
+
await registerGitWrapupInstructionsTool(server);
|
|
198
204
|
// Add calls to register other resources/tools here if needed in the future.
|
|
199
205
|
logger.info('Git tools registered successfully', context);
|
|
200
206
|
}
|
|
@@ -15,6 +15,7 @@ const GitDiffInputBaseSchema = z.object({
|
|
|
15
15
|
commit2: z.string().optional().describe("Second commit, branch, or ref for comparison. If omitted, compares commit1 against the working tree or index."),
|
|
16
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."),
|
|
17
17
|
file: z.string().optional().describe("Limit the diff output to a specific file path."),
|
|
18
|
+
includeUntracked: z.boolean().optional().default(false).describe("Include untracked files in the diff output (shows their full content as new files). This is a non-standard extension."),
|
|
18
19
|
// Add options like --name-only, --stat, context lines (-U<n>) if needed
|
|
19
20
|
});
|
|
20
21
|
// Export the shape for registration
|
|
@@ -61,42 +62,106 @@ export async function diffGitChanges(input, context) {
|
|
|
61
62
|
const safeCommit1 = input.commit1?.replace(/[`$&;*()|<>]/g, '');
|
|
62
63
|
const safeCommit2 = input.commit2?.replace(/[`$&;*()|<>]/g, '');
|
|
63
64
|
const safeFile = input.file?.replace(/[`$&;*()|<>]/g, '');
|
|
65
|
+
let untrackedFilesDiff = '';
|
|
66
|
+
let untrackedFilesCount = 0;
|
|
64
67
|
try {
|
|
65
|
-
// Construct the git diff command
|
|
66
|
-
let
|
|
68
|
+
// Construct the standard git diff command
|
|
69
|
+
let standardDiffCommand = `git -C "${targetPath}" diff`;
|
|
67
70
|
if (input.staged) {
|
|
68
|
-
|
|
71
|
+
standardDiffCommand += ' --staged'; // Or --cached
|
|
69
72
|
}
|
|
70
73
|
else {
|
|
71
74
|
// Add commit references if not doing staged diff
|
|
72
75
|
if (safeCommit1) {
|
|
73
|
-
|
|
76
|
+
standardDiffCommand += ` ${safeCommit1}`;
|
|
74
77
|
}
|
|
75
78
|
if (safeCommit2) {
|
|
76
|
-
|
|
79
|
+
standardDiffCommand += ` ${safeCommit2}`;
|
|
77
80
|
}
|
|
78
81
|
}
|
|
79
|
-
// Add file path limiter if provided
|
|
82
|
+
// Add file path limiter if provided for standard diff
|
|
83
|
+
// Note: `input.file` will not apply to the untracked files part unless we explicitly filter them.
|
|
84
|
+
// For simplicity, `includeUntracked` will show all untracked files if `input.file` is also set.
|
|
80
85
|
if (safeFile) {
|
|
81
|
-
|
|
86
|
+
standardDiffCommand += ` -- "${safeFile}"`; // Use '--' to separate paths from revisions
|
|
82
87
|
}
|
|
83
|
-
logger.debug(`Executing command: ${
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
logger.debug(`Executing standard diff command: ${standardDiffCommand}`, { ...context, operation });
|
|
89
|
+
const { stdout: standardStdout, stderr: standardStderr } = await execAsync(standardDiffCommand, { maxBuffer: 1024 * 1024 * 20 });
|
|
90
|
+
if (standardStderr) {
|
|
91
|
+
logger.warning(`Git diff (standard) stderr: ${standardStderr}`, { ...context, operation });
|
|
92
|
+
}
|
|
93
|
+
let combinedDiffOutput = standardStdout;
|
|
94
|
+
// Handle untracked files if requested
|
|
95
|
+
if (input.includeUntracked) {
|
|
96
|
+
logger.debug('Including untracked files.', { ...context, operation });
|
|
97
|
+
const listUntrackedCommand = `git -C "${targetPath}" ls-files --others --exclude-standard`;
|
|
98
|
+
try {
|
|
99
|
+
const { stdout: untrackedFilesStdOut } = await execAsync(listUntrackedCommand);
|
|
100
|
+
const untrackedFiles = untrackedFilesStdOut.trim().split('\n').filter(f => f); // Filter out empty lines
|
|
101
|
+
if (untrackedFiles.length > 0) {
|
|
102
|
+
logger.info(`Found ${untrackedFiles.length} untracked files.`, { ...context, operation, untrackedFiles });
|
|
103
|
+
let individualUntrackedDiffs = '';
|
|
104
|
+
for (const untrackedFile of untrackedFiles) {
|
|
105
|
+
// Sanitize each untracked file path before using in command
|
|
106
|
+
const safeUntrackedFile = untrackedFile.replace(/[`$&;*()|<>]/g, '');
|
|
107
|
+
// Skip if file path becomes empty after sanitization (unlikely but safe)
|
|
108
|
+
if (!safeUntrackedFile)
|
|
109
|
+
continue;
|
|
110
|
+
const untrackedDiffCommand = `git -C "${targetPath}" diff --no-index /dev/null "${safeUntrackedFile}"`;
|
|
111
|
+
logger.debug(`Executing diff for untracked file: ${untrackedDiffCommand}`, { ...context, operation, file: safeUntrackedFile });
|
|
112
|
+
try {
|
|
113
|
+
const { stdout: untrackedFileDiffOut } = await execAsync(untrackedDiffCommand);
|
|
114
|
+
individualUntrackedDiffs += untrackedFileDiffOut;
|
|
115
|
+
untrackedFilesCount++;
|
|
116
|
+
}
|
|
117
|
+
catch (untrackedError) {
|
|
118
|
+
// For `git diff --no-index`, a non-zero exit code (usually 1) means differences were found.
|
|
119
|
+
// The actual diff output will be in untrackedError.stdout.
|
|
120
|
+
if (untrackedError.stdout) {
|
|
121
|
+
individualUntrackedDiffs += untrackedError.stdout;
|
|
122
|
+
untrackedFilesCount++;
|
|
123
|
+
// Log stderr if it exists, as it might contain actual error messages despite stdout having the diff
|
|
124
|
+
if (untrackedError.stderr) {
|
|
125
|
+
logger.warning(`Stderr while diffing untracked file ${safeUntrackedFile} (diff captured from stdout): ${untrackedError.stderr}`, { ...context, operation, file: safeUntrackedFile });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
// If stdout is empty, then it's a more genuine failure.
|
|
130
|
+
logger.warning(`Failed to diff untracked file: ${safeUntrackedFile}. Error: ${untrackedError.message}`, { ...context, operation, file: safeUntrackedFile, errorDetails: { stderr: untrackedError.stderr, stdout: untrackedError.stdout, code: untrackedError.code } });
|
|
131
|
+
individualUntrackedDiffs += `\n--- Diff for untracked file ${safeUntrackedFile} failed: ${untrackedError.message}\n`;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (individualUntrackedDiffs) {
|
|
136
|
+
// Add a separator if standard diff also had output
|
|
137
|
+
if (combinedDiffOutput.trim()) {
|
|
138
|
+
combinedDiffOutput += '\n';
|
|
139
|
+
}
|
|
140
|
+
combinedDiffOutput += individualUntrackedDiffs;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
logger.info('No untracked files found.', { ...context, operation });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch (lsFilesError) {
|
|
148
|
+
logger.warning(`Failed to list untracked files. Error: ${lsFilesError.message}`, { ...context, operation, error: lsFilesError.stderr || lsFilesError.stdout });
|
|
149
|
+
// Proceed without untracked files if listing fails
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const isNoChanges = combinedDiffOutput.trim() === '';
|
|
153
|
+
const finalDiffOutput = isNoChanges ? 'No changes found.' : combinedDiffOutput;
|
|
154
|
+
let message = isNoChanges ? 'No changes found.' : 'Diff generated successfully.';
|
|
155
|
+
if (untrackedFilesCount > 0) {
|
|
156
|
+
message += ` Included ${untrackedFilesCount} untracked file(s).`;
|
|
90
157
|
}
|
|
91
|
-
const rawDiffOutput = stdout;
|
|
92
|
-
const isNoChanges = rawDiffOutput.trim() === '';
|
|
93
|
-
const finalDiffOutput = isNoChanges ? 'No changes found.' : rawDiffOutput;
|
|
94
|
-
const message = isNoChanges ? 'No changes found.' : 'Diff generated successfully.';
|
|
95
158
|
logger.info(`${operation} completed successfully. ${message}`, { ...context, operation, path: targetPath });
|
|
96
|
-
return { success: true, diff: finalDiffOutput, message };
|
|
159
|
+
return { success: true, diff: finalDiffOutput, message, untrackedFilesProcessed: untrackedFilesCount };
|
|
97
160
|
}
|
|
98
161
|
catch (error) {
|
|
99
|
-
|
|
162
|
+
// This catch block now primarily handles errors from the *standard* diff command
|
|
163
|
+
// or catastrophic failures before/after untracked file processing.
|
|
164
|
+
logger.error(`Failed to execute git diff operation`, { ...context, operation, path: targetPath, error: error.message, stderr: error.stderr, stdout: error.stdout });
|
|
100
165
|
const errorMessage = error.stderr || error.stdout || error.message || '';
|
|
101
166
|
// Handle specific error cases
|
|
102
167
|
if (errorMessage.toLowerCase().includes('not a git repository')) {
|
|
@@ -21,7 +21,7 @@ export function initializeGitDiffStateAccessors(getWdFn, getSidFn) {
|
|
|
21
21
|
logger.info('State accessors initialized for git_diff tool registration.');
|
|
22
22
|
}
|
|
23
23
|
const TOOL_NAME = 'git_diff';
|
|
24
|
-
const TOOL_DESCRIPTION = "Shows changes between commits, commit and working tree, etc. Can show staged changes or diff specific files. Returns the diff output as plain text.";
|
|
24
|
+
const TOOL_DESCRIPTION = "Shows changes between commits, commit and working tree, etc. Can show staged changes or diff specific files. An optional 'includeUntracked' parameter (boolean) can be used to also show the content of untracked files. Returns the diff output as plain text.";
|
|
25
25
|
/**
|
|
26
26
|
* Registers the git_diff tool with the MCP server.
|
|
27
27
|
*
|
|
@@ -62,11 +62,9 @@ export async function gitInitLogic(input, context) {
|
|
|
62
62
|
if (input.bare) {
|
|
63
63
|
command += ' --bare';
|
|
64
64
|
}
|
|
65
|
-
if
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
command += ` -b "${input.initialBranch.replace(/"/g, '\\"')}"`;
|
|
69
|
-
}
|
|
65
|
+
// Determine the initial branch name, defaulting to 'main' if not provided
|
|
66
|
+
const branchNameToUse = input.initialBranch || 'main';
|
|
67
|
+
command += ` -b "${branchNameToUse.replace(/"/g, '\\"')}"`;
|
|
70
68
|
// Add the target directory path at the end
|
|
71
69
|
command += ` "${targetPath}"`;
|
|
72
70
|
logger.debug(`Executing command: ${command}`, { ...context, operation });
|
|
@@ -9,6 +9,7 @@ const execAsync = promisify(exec);
|
|
|
9
9
|
export const GitSetWorkingDirInputSchema = z.object({
|
|
10
10
|
path: z.string().min(1, "Path cannot be empty.").describe("The absolute path to set as the default working directory for the current session. Set this before using other git_* tools."),
|
|
11
11
|
validateGitRepo: z.boolean().default(true).describe("Whether to validate that the path is a Git repository"),
|
|
12
|
+
initializeIfNotPresent: z.boolean().optional().default(false).describe("If true and the directory is not a Git repository, attempt to initialize it with 'git init'.")
|
|
12
13
|
});
|
|
13
14
|
/**
|
|
14
15
|
* Logic for the git_set_working_dir tool.
|
|
@@ -51,25 +52,37 @@ export async function gitSetWorkingDirLogic(input, context // Assuming context p
|
|
|
51
52
|
logger.error('Failed to stat directory', error, { ...context, operation, path: sanitizedPath });
|
|
52
53
|
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to access path: ${error.message}`, { context, operation });
|
|
53
54
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
let isGitRepo = false;
|
|
56
|
+
let initializedRepo = false;
|
|
57
|
+
try {
|
|
58
|
+
const { stdout } = await execAsync('git rev-parse --is-inside-work-tree', { cwd: sanitizedPath });
|
|
59
|
+
if (stdout.trim() === 'true') {
|
|
60
|
+
isGitRepo = true;
|
|
61
|
+
logger.debug('Path is already a Git repository', { ...context, operation, path: sanitizedPath });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
logger.debug('Path is not a Git repository (rev-parse failed or returned non-true)', { ...context, operation, path: sanitizedPath, error: error.message });
|
|
66
|
+
isGitRepo = false;
|
|
67
|
+
}
|
|
68
|
+
if (!isGitRepo && input.initializeIfNotPresent) {
|
|
69
|
+
logger.info(`Path is not a Git repository. Attempting to initialize (initializeIfNotPresent=true) with initial branch 'main'.`, { ...context, operation, path: sanitizedPath });
|
|
57
70
|
try {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
if (stdout.trim() !== 'true') {
|
|
63
|
-
// This case should ideally not happen if rev-parse succeeds, but good to check.
|
|
64
|
-
throw new Error('Not a Git repository (rev-parse returned non-true)');
|
|
65
|
-
}
|
|
66
|
-
logger.debug('Path validated as Git repository', { ...context, operation, path: sanitizedPath });
|
|
71
|
+
await execAsync('git init --initial-branch=main', { cwd: sanitizedPath });
|
|
72
|
+
initializedRepo = true;
|
|
73
|
+
isGitRepo = true; // Now it is a git repo
|
|
74
|
+
logger.info('Successfully initialized Git repository with initial branch "main".', { ...context, operation, path: sanitizedPath });
|
|
67
75
|
}
|
|
68
|
-
catch (
|
|
69
|
-
logger.
|
|
70
|
-
throw new McpError(BaseErrorCode.
|
|
76
|
+
catch (initError) {
|
|
77
|
+
logger.error('Failed to initialize Git repository', initError, { ...context, operation, path: sanitizedPath });
|
|
78
|
+
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to initialize Git repository at ${sanitizedPath}: ${initError.message}`, { context, operation });
|
|
71
79
|
}
|
|
72
80
|
}
|
|
81
|
+
// After potential initialization, if validateGitRepo is true, it must now be a Git repo.
|
|
82
|
+
if (input.validateGitRepo && !isGitRepo) {
|
|
83
|
+
logger.warning('Path is not a valid Git repository and initialization was not performed or failed.', { ...context, operation, path: sanitizedPath });
|
|
84
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Path is not a valid Git repository: ${sanitizedPath}.`, { context, operation });
|
|
85
|
+
}
|
|
73
86
|
// --- Update Session State ---
|
|
74
87
|
// This part needs access to the session state mechanism defined in server.ts
|
|
75
88
|
// We assume the context provides a way to set the working directory for the current session.
|
|
@@ -82,9 +95,23 @@ export async function gitSetWorkingDirLogic(input, context // Assuming context p
|
|
|
82
95
|
// This indicates an internal logic error in how state is passed/managed.
|
|
83
96
|
throw new McpError(BaseErrorCode.INTERNAL_ERROR, 'Failed to update session state.', { context, operation });
|
|
84
97
|
}
|
|
98
|
+
let message = `Working directory set to: ${sanitizedPath}`;
|
|
99
|
+
if (initializedRepo) {
|
|
100
|
+
message += ' (New Git repository initialized).';
|
|
101
|
+
}
|
|
102
|
+
else if (isGitRepo && input.validateGitRepo) { // Only state "Existing" if validation was on and it passed
|
|
103
|
+
message += ' (Existing Git repository).';
|
|
104
|
+
}
|
|
105
|
+
else if (isGitRepo && !input.validateGitRepo) { // It is a git repo, but we weren't asked to validate it
|
|
106
|
+
message += ' (Is a Git repository, validation skipped).';
|
|
107
|
+
}
|
|
108
|
+
else if (!isGitRepo && !input.validateGitRepo && !input.initializeIfNotPresent) { // Not a git repo, validation off, no init request
|
|
109
|
+
message += ' (Not a Git repository, validation skipped, no initialization requested).';
|
|
110
|
+
}
|
|
85
111
|
return {
|
|
86
112
|
success: true,
|
|
87
|
-
message:
|
|
113
|
+
message: message,
|
|
88
114
|
path: sanitizedPath,
|
|
115
|
+
initialized: initializedRepo,
|
|
89
116
|
};
|
|
90
117
|
}
|
|
@@ -19,7 +19,7 @@ setWdFn, getSidFn) {
|
|
|
19
19
|
logger.info('State accessors initialized for git_set_working_dir tool registration.');
|
|
20
20
|
}
|
|
21
21
|
const TOOL_NAME = 'git_set_working_dir';
|
|
22
|
-
const TOOL_DESCRIPTION = "Sets the default working directory for the current session. Subsequent Git tool calls within this session can use '.' for the `path` parameter, which will resolve to this directory. Optionally validates if the path is a Git repository (`validateGitRepo: true`). Returns the result as a JSON object. IMPORTANT: The provided path must be absolute.";
|
|
22
|
+
const TOOL_DESCRIPTION = "Sets the default working directory for the current session. Subsequent Git tool calls within this session can use '.' for the `path` parameter, which will resolve to this directory. Optionally validates if the path is a Git repository (`validateGitRepo: true`). Can optionally initialize a Git repository with 'git init' if it's not already one and `initializeIfNotPresent: true` is set. Returns the result as a JSON object. IMPORTANT: The provided path must be absolute.";
|
|
23
23
|
/**
|
|
24
24
|
* Registers the git_set_working_dir tool with the MCP server.
|
|
25
25
|
*
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Barrel file for the git_worktree tool.
|
|
3
|
+
* Exports the registration function and state accessor initialization function.
|
|
4
|
+
*/
|
|
5
|
+
export { registerGitWorktreeTool, initializeGitWorktreeStateAccessors } from './registration.js';
|
|
6
|
+
// Export types if needed elsewhere, e.g.:
|
|
7
|
+
// export type { GitWorktreeInput, GitWorktreeResult } from './logic.js';
|