@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 CHANGED
@@ -1,8 +1,8 @@
1
1
  # Git MCP Server
2
2
 
3
3
  [![TypeScript](https://img.shields.io/badge/TypeScript-^5.8.3-blue.svg)](https://www.typescriptlang.org/)
4
- [![Model Context Protocol](https://img.shields.io/badge/MCP%20SDK-^1.11.2-green.svg)](https://modelcontextprotocol.io/)
5
- [![Version](https://img.shields.io/badge/Version-2.0.11-blue.svg)](./CHANGELOG.md)
4
+ [![Model Context Protocol](https://img.shields.io/badge/MCP%20SDK-^1.12.0-green.svg)](https://modelcontextprotocol.io/)
5
+ [![Version](https://img.shields.io/badge/Version-2.0.14-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)
@@ -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 | Description | Key Arguments |
157
- | :---------------------- | :------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------- |
158
- | `git_add` | Stages specified files or patterns. | `path?`, `files?` |
159
- | `git_branch` | Manages branches (list, create, delete, rename, show current). | `path?`, `mode`, `branchName?`, `newBranchName?`, `startPoint?`, `force?`, `all?`, `remote?` |
160
- | `git_checkout` | Switches branches or restores working tree files. | `path?`, `branchOrPath`, `newBranch?`, `force?` |
161
- | `git_cherry_pick` | Applies changes introduced by existing commits. | `path?`, `commitRef`, `mainline?`, `strategy?`, `noCommit?`, `signoff?` |
162
- | `git_clean` | Removes untracked files. **Requires `force: true`**. | `path?`, `force`, `dryRun?`, `directories?`, `ignored?` |
163
- | `git_clear_working_dir` | Clears the session-specific working directory. | (none) |
164
- | `git_clone` | Clones a repository into a specified absolute path. | `repositoryUrl`, `targetPath`, `branch?`, `depth?`, `quiet?` |
165
- | `git_commit` | Commits staged changes. Supports author override, signing control. | `path?`, `message`, `author?`, `allowEmpty?`, `amend?`, `forceUnsignedOnFailure?` |
166
- | `git_diff` | Shows changes between commits, working tree, etc. | `path?`, `commit1?`, `commit2?`, `staged?`, `file?` |
167
- | `git_fetch` | Downloads objects and refs from other repositories. | `path?`, `remote?`, `prune?`, `tags?`, `all?` |
168
- | `git_init` | Initializes a new Git repository at the specified absolute path. | `path`, `initialBranch?`, `bare?`, `quiet?` |
169
- | `git_log` | Shows commit logs. | `path?`, `maxCount?`, `author?`, `since?`, `until?`, `branchOrFile?` |
170
- | `git_merge` | Merges the specified branch into the current branch. | `path?`, `branch`, `commitMessage?`, `noFf?`, `squash?`, `abort?` |
171
- | `git_pull` | Fetches from and integrates with another repository or local branch. | `path?`, `remote?`, `branch?`, `rebase?`, `ffOnly?` |
172
- | `git_push` | Updates remote refs using local refs. | `path?`, `remote?`, `branch?`, `remoteBranch?`, `force?`, `forceWithLease?`, `setUpstream?`, `tags?`, `delete?` |
173
- | `git_rebase` | Reapplies commits on top of another base tip. | `path?`, `mode?`, `upstream?`, `branch?`, `interactive?`, `strategy?`, `strategyOption?`, `onto?` |
174
- | `git_remote` | Manages remote repositories (list, add, remove, show). | `path?`, `mode`, `name?`, `url?` |
175
- | `git_reset` | Resets current HEAD to a specified state. Supports soft, mixed, hard modes. **USE 'hard' WITH CAUTION**. | `path?`, `mode?`, `commit?` |
176
- | `git_set_working_dir` | Sets the default working directory for the current session. Requires absolute path. | `path`, `validateGitRepo?` |
177
- | `git_show` | Shows information about Git objects (commits, tags, etc.). | `path?`, `ref`, `filePath?` |
178
- | `git_stash` | Manages stashed changes (list, apply, pop, drop, save). | `path?`, `mode`, `stashRef?`, `message?` |
179
- | `git_status` | Gets repository status (branch, staged, modified, untracked files). | `path?` |
180
- | `git_tag` | Manages tags (list, create annotated/lightweight, delete). | `path?`, `mode`, `tagName?`, `message?`, `commitRef?`, `annotate?` |
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.8).**
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.11.0. Resource capabilities, previously available, have been temporarily removed during this major update.
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 { registerGitAddTool, initializeGitAddStateAccessors } from './tools/gitAdd/index.js';
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';
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 { registerGitCommitTool, initializeGitCommitStateAccessors } from './tools/gitCommit/index.js';
29
- import { registerGitDiffTool, initializeGitDiffStateAccessors } from './tools/gitDiff/index.js';
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';
34
- import { registerGitPullTool, initializeGitPullStateAccessors } from './tools/gitPull/index.js';
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';
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';
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 { startHttpTransport, getHttpSessionWorkingDirectory, setHttpSessionWorkingDirectory } from './transports/httpTransport.js';
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 command = `git -C "${targetPath}" diff`;
68
+ // Construct the standard git diff command
69
+ let standardDiffCommand = `git -C "${targetPath}" diff`;
67
70
  if (input.staged) {
68
- command += ' --staged'; // Or --cached
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
- command += ` ${safeCommit1}`;
76
+ standardDiffCommand += ` ${safeCommit1}`;
74
77
  }
75
78
  if (safeCommit2) {
76
- command += ` ${safeCommit2}`;
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
- command += ` -- "${safeFile}"`; // Use '--' to separate paths from revisions
86
+ standardDiffCommand += ` -- "${safeFile}"`; // Use '--' to separate paths from revisions
82
87
  }
83
- logger.debug(`Executing command: ${command}`, { ...context, operation });
84
- // Execute command. Diff output is primarily on stdout.
85
- // Increase maxBuffer as diffs can be large.
86
- const { stdout, stderr } = await execAsync(command, { maxBuffer: 1024 * 1024 * 20 }); // 20MB buffer
87
- if (stderr) {
88
- // Log stderr as warning, as it might contain non-fatal info
89
- logger.warning(`Git diff stderr: ${stderr}`, { ...context, operation });
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
- logger.error(`Failed to execute git diff command`, { ...context, operation, path: targetPath, error: error.message, stderr: error.stderr, stdout: error.stdout });
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 (input.initialBranch) {
66
- // Use -b for modern Git versions, older might need --initial-branch=
67
- // Sticking with -b as it's common now. Add quotes around branch name.
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
- // Optionally validate if it's a Git repository
55
- if (input.validateGitRepo) {
56
- logger.debug('Validating if path is a Git repository', { ...context, operation, path: sanitizedPath });
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
- // A common way to check is using 'git rev-parse --is-inside-work-tree'
59
- // or checking for the existence of a .git directory/file.
60
- // Using rev-parse is generally more robust.
61
- const { stdout } = await execAsync('git rev-parse --is-inside-work-tree', { cwd: sanitizedPath });
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 (error) {
69
- logger.warning('Path is not a valid Git repository', { ...context, operation, path: sanitizedPath, error: error.message });
70
- throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Path is not a valid Git repository: ${sanitizedPath}. Error: ${error.message}`, { context, operation });
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: `Working directory set to: ${sanitizedPath}`,
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';