#
tokens: 44181/50000 25/25 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .clinerules
├── .github
│   └── workflows
│       └── npm-publish.yml
├── .gitignore
├── bun.lock
├── LICENSE
├── memory-bank
│   ├── activeContext.md
│   ├── productContext.md
│   ├── progress.md
│   ├── projectbrief.md
│   ├── systemPatterns.md
│   └── techContext.md
├── package-lock.json
├── package.json
├── readme.md
└── src
    ├── handlers
    │   ├── advanced-operations.js
    │   ├── branch-operations.js
    │   ├── commit-operations.js
    │   ├── common.js
    │   ├── config-operations.js
    │   ├── directory-operations.js
    │   ├── index.js
    │   ├── other-operations.js
    │   ├── remote-operations.js
    │   ├── stash-operations.js
    │   └── tag-operations.js
    ├── index.js
    ├── server.js
    └── utils
        └── git.js
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
 1 | # Dependency directories
 2 | node_modules/
 3 | jspm_packages/
 4 | 
 5 | # Logs
 6 | logs
 7 | *.log
 8 | npm-debug.log*
 9 | yarn-debug.log*
10 | yarn-error.log*
11 | lerna-debug.log*
12 | 
13 | # Runtime data
14 | pids
15 | *.pid
16 | *.seed
17 | *.pid.lock
18 | 
19 | # Directory for instrumented libs generated by jscoverage/JSCover
20 | lib-cov
21 | 
22 | # Coverage directory used by tools like istanbul
23 | coverage
24 | *.lcov
25 | 
26 | # nyc test coverage
27 | .nyc_output
28 | 
29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
30 | .grunt
31 | 
32 | # Bower dependency directory (https://bower.io/)
33 | bower_components
34 | 
35 | # node-waf configuration
36 | .lock-wscript
37 | 
38 | # Compiled binary addons (https://nodejs.org/api/addons.html)
39 | build/Release
40 | 
41 | # TypeScript cache
42 | *.tsbuildinfo
43 | 
44 | # Optional npm cache directory
45 | .npm
46 | 
47 | # Optional eslint cache
48 | .eslintcache
49 | 
50 | # Optional REPL history
51 | .node_repl_history
52 | 
53 | # Output of 'npm pack'
54 | *.tgz
55 | 
56 | # Yarn Integrity file
57 | .yarn-integrity
58 | 
59 | # dotenv environment variables file
60 | .env
61 | .env.test
62 | 
63 | # parcel-bundler cache (https://parceljs.org/)
64 | .cache
65 | 
66 | # Temporary folders
67 | tmp/
68 | temp/
69 | 
70 | # IDE folders
71 | .idea/
72 | .vscode/
73 | *.swp
74 | *.swo
75 | 
```

--------------------------------------------------------------------------------
/.clinerules:
--------------------------------------------------------------------------------

```
 1 | # Git Commands MCP Project Rules
 2 | 
 3 | ## Package Version Management
 4 | 
 5 | 1. **Increment Version Before Pushing**: 
 6 |    - Always increment the version number in `package.json` before pushing to the repository
 7 |    - This is critical as pushes to the repository trigger an npm package release through the CI/CD pipeline
 8 |    - Current version format is semantic versioning (major.minor.patch)
 9 | 
10 | 2. **Version Update Workflow**:
11 |    - Check current version in package.json
12 |    - Increment appropriate segment based on changes:
13 |      - Patch (0.1.x): Bug fixes and minor changes
14 |      - Minor (0.x.0): New features, backward compatible
15 |      - Major (x.0.0): Breaking changes
16 |    - Stage and commit version change separately
17 |    - Sample commit message: "Bump version to X.Y.Z for npm release"
18 | 
19 | ## Repository Configuration
20 | 
21 | 1. **Git Remote Setup**:
22 |    - Repository uses SSH for authentication: `[email protected]:bsreeram08/git-commands-mcp.git`
23 |    - SSH keys must be properly configured for push access
24 | 
25 | 2. **Branch Structure**:
26 |    - Main development happens on `master` branch
27 |    - Use feature branches for new development
28 | 
29 | ## CI/CD Pipeline
30 | 
31 | 1. **GitHub Actions**:
32 |    - The repository has an npm-publish workflow in `.github/workflows/npm-publish.yml`
33 |    - This workflow triggers on pushes to the repository
34 |    - It builds and publishes the package to npm registry automatically
35 | 
36 | 2. **Release Checklist**:
37 |    - Update version in package.json
38 |    - Ensure all changes are committed
39 |    - Push to repository
40 |    - Verify GitHub Actions workflow completes successfully
41 |    - Check npm registry for the updated package
42 | 
43 | ## Development Patterns
44 | 
45 | 1. **Tool Handler Registration**:
46 |    - When adding new Git handlers, ensure they are added to both:
47 |      - `this.handlersMap` for functional registration
48 |      - `this.toolsList` for exposure through the MCP interface
49 |    - Update appropriate handler category in `this.handlerCategories`
50 | 
51 | 2. **Code Organization**:
52 |    - Handlers are organized in `src/handlers/index.js`
53 |    - Server setup is in `src/server.js`
54 |    - Main entry point is `src/index.js`
55 | 
56 | 3. **Naming Conventions**:
57 |    - Git tool handlers follow pattern: `git_[action]_[resource]`
58 |    - Handler implementations follow pattern: `handleGit[Action][Resource]`
59 | 
```

--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------

```markdown
  1 | # MCP Git Repo Browser (Node.js)
  2 | 
  3 | A Node.js implementation of a Git repository browser using the Model Context Protocol (MCP).
  4 | 
  5 | [![GitHub Repository](https://img.shields.io/badge/GitHub-Repository-blue.svg)](https://github.com/bsreeram08/git-commands-mcp)
  6 | [![npm package](https://img.shields.io/npm/v/git-commands-mcp.svg)](https://www.npmjs.com/package/git-commands-mcp)
  7 | 
  8 | ## Installation
  9 | 
 10 | ### NPM (Recommended)
 11 | 
 12 | ```bash
 13 | npm install -g git-commands-mcp
 14 | ```
 15 | 
 16 | ### Manual Installation
 17 | 
 18 | ```bash
 19 | git clone https://github.com/bsreeram08/git-commands-mcp.git
 20 | cd git-commands-mcp
 21 | npm install
 22 | ```
 23 | 
 24 | ## Configuration
 25 | 
 26 | Add this to your MCP settings configuration file:
 27 | 
 28 | ```json
 29 | {
 30 |   "mcpServers": {
 31 |     "git-commands-mcp": {
 32 |       "command": "git-commands-mcp"
 33 |     }
 34 |   }
 35 | }
 36 | ```
 37 | 
 38 | For manual installation, use:
 39 | 
 40 | ```json
 41 | {
 42 |   "mcpServers": {
 43 |     "git-commands-mcp": {
 44 |       "command": "node",
 45 |       "args": ["/path/to/git-commands-mcp/src/index.js"]
 46 |     }
 47 |   }
 48 | }
 49 | ```
 50 | 
 51 | ## Features
 52 | 
 53 | The server provides the following tools:
 54 | 
 55 | ### Basic Repository Operations
 56 | 
 57 | 1. `git_directory_structure`: Returns a tree-like representation of a repository's directory structure
 58 | 
 59 |    - Input: Repository URL
 60 |    - Output: ASCII tree representation of the repository structure
 61 | 
 62 | 2. `git_read_files`: Reads and returns the contents of specified files in a repository
 63 | 
 64 |    - Input: Repository URL and list of file paths
 65 |    - Output: Dictionary mapping file paths to their contents
 66 | 
 67 | 3. `git_search_code`: Searches for patterns in repository code
 68 |    - Input: Repository URL, search pattern, optional file patterns, case sensitivity, and context lines
 69 |    - Output: JSON with search results including matching lines and context
 70 | 
 71 | ### Branch Operations
 72 | 
 73 | 4. `git_branch_diff`: Compare two branches and show files changed between them
 74 |    - Input: Repository URL, source branch, target branch, and optional show_patch flag
 75 |    - Output: JSON with commit count and diff summary
 76 | 
 77 | ### Commit Operations
 78 | 
 79 | 5. `git_commit_history`: Get commit history for a branch with optional filtering
 80 | 
 81 |    - Input: Repository URL, branch name, max count, author filter, since date, until date, and message grep
 82 |    - Output: JSON with commit details
 83 | 
 84 | 6. `git_commits_details`: Get detailed information about commits including full messages and diffs
 85 | 
 86 |    - Input: Repository URL, branch name, max count, include_diff flag, author filter, since date, until date, and message grep
 87 |    - Output: JSON with detailed commit information
 88 | 
 89 | 7. `git_local_changes`: Get uncommitted changes in the working directory
 90 |    - Input: Local repository path
 91 |    - Output: JSON with status information and diffs
 92 | 
 93 | ## Project Structure
 94 | 
 95 | ```
 96 | git-commands-mcp/
 97 | ├── src/
 98 | │   ├── index.js         # Entry point
 99 | │   ├── server.js        # Main server implementation
100 | │   ├── handlers/        # Tool handlers
101 | │   │   └── index.js     # Tool implementation functions
102 | │   └── utils/           # Utility functions
103 | │       └── git.js       # Git-related helper functions
104 | ├── package.json
105 | └── readme.md
106 | ```
107 | 
108 | ## Implementation Details
109 | 
110 | - Uses Node.js native modules (crypto, path, os) for core functionality
111 | - Leverages fs-extra for enhanced file operations
112 | - Uses simple-git for Git repository operations
113 | - Implements clean error handling and resource cleanup
114 | - Creates deterministic temporary directories based on repository URL hashes
115 | - Reuses cloned repositories when possible for efficiency
116 | - Modular code structure for better maintainability
117 | 
118 | ## Requirements
119 | 
120 | - Node.js 14.x or higher
121 | - Git installed on the system
122 | 
123 | ## Usage
124 | 
125 | If installed globally via npm:
126 | 
127 | ```bash
128 | git-commands-mcp
129 | ```
130 | 
131 | If installed manually:
132 | 
133 | ```bash
134 | node src/index.js
135 | ```
136 | 
137 | The server runs on stdio, making it compatible with MCP clients.
138 | 
139 | ## CI/CD
140 | 
141 | This project uses GitHub Actions for continuous integration and deployment:
142 | 
143 | ### Automatic NPM Publishing
144 | 
145 | The repository is configured with a GitHub Actions workflow that automatically publishes the package to npm when changes are pushed to the master branch.
146 | 
147 | #### Setting up NPM_AUTOMATION_TOKEN
148 | 
149 | To enable automatic publishing, you need to add an npm Automation token as a GitHub secret (this works even with accounts that have 2FA enabled):
150 | 
151 | 1. Generate an npm Automation token:
152 | 
153 |    - Log in to your npm account on [npmjs.com](https://www.npmjs.com/)
154 |    - Go to your profile settings
155 |    - Select "Access Tokens"
156 |    - Click "Generate New Token"
157 |    - Select "Automation" token type
158 |    - Set the appropriate permissions (needs "Read and write" for packages)
159 |    - Copy the generated token
160 | 
161 | 2. Add the token to your GitHub repository:
162 |    - Go to your GitHub repository
163 |    - Navigate to "Settings" > "Secrets and variables" > "Actions"
164 |    - Click "New repository secret"
165 |    - Name: `NPM_AUTOMATION_TOKEN`
166 |    - Value: Paste your npm Automation token
167 |    - Click "Add secret"
168 | 
169 | Once configured, any push to the master branch will trigger the workflow to publish the package to npm.
170 | 
171 | ## License
172 | 
173 | MIT License - see the [LICENSE](LICENSE) file for details.
174 | 
175 | ## Links
176 | 
177 | - [GitHub Repository](https://github.com/bsreeram08/git-commands-mcp)
178 | - [NPM Package](https://www.npmjs.com/package/git-commands-mcp)
179 | - [Report Issues](https://github.com/bsreeram08/git-commands-mcp/issues)
180 | 
```

--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------

```javascript
1 | #!/usr/bin/env node
2 | import { GitRepoBrowserServer } from "./server.js";
3 | 
4 | const server = new GitRepoBrowserServer();
5 | server.run().catch(console.error);
6 | 
```

--------------------------------------------------------------------------------
/src/handlers/common.js:
--------------------------------------------------------------------------------

```javascript
 1 | import path from "path";
 2 | import fs from "fs-extra";
 3 | import { simpleGit } from "simple-git";
 4 | import { exec } from "child_process";
 5 | import { promisify } from "util";
 6 | import { cloneRepo, getDirectoryTree } from "../utils/git.js";
 7 | 
 8 | const execPromise = promisify(exec);
 9 | 
10 | export { path, fs, simpleGit, execPromise, cloneRepo, getDirectoryTree };
11 | 
```

--------------------------------------------------------------------------------
/.github/workflows/npm-publish.yml:
--------------------------------------------------------------------------------

```yaml
 1 | name: NPM Publish
 2 | 
 3 | on:
 4 |   push:
 5 |     branches:
 6 |       - master
 7 | 
 8 | jobs:
 9 |   publish:
10 |     runs-on: ubuntu-latest
11 |     steps:
12 |       - name: Checkout code
13 |         uses: actions/checkout@v3
14 | 
15 |       - name: Setup Node.js
16 |         uses: actions/setup-node@v3
17 |         with:
18 |           node-version: "16.x"
19 |           registry-url: "https://registry.npmjs.org"
20 | 
21 |       - name: Install dependencies
22 |         run: npm ci
23 | 
24 |       - name: Publish to npm
25 |         # Use --access=public if it's a scoped package (@username/package-name)
26 |         run: npm publish --provenance
27 |         env:
28 |           # Use an Automation token to avoid 2FA issues
29 |           NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTOMATION_TOKEN }}
30 | 
```

--------------------------------------------------------------------------------
/memory-bank/projectbrief.md:
--------------------------------------------------------------------------------

```markdown
 1 | # Project Brief: Git Commands MCP Server
 2 | 
 3 | ## Core Goal
 4 | 
 5 | To provide a Model Context Protocol (MCP) server that exposes common and advanced Git commands as tools. This allows users (or AI agents) to interact with Git repositories programmatically through the MCP interface.
 6 | 
 7 | ## Key Features
 8 | 
 9 | - Expose a range of Git operations (cloning, committing, branching, merging, diffing, etc.) as distinct MCP tools.
10 | - Operate on both remote repositories (via URL) and local repositories (via path).
11 | - Return structured information from Git commands.
12 | 
13 | ## Current Scope
14 | 
15 | The server currently implements a variety of Git commands using the `simple-git` library, which wraps the local Git executable.
16 | 
17 | ## Project Status
18 | 
19 | Actively developed. A recent Pull Request proposes adding Docker and Smithery configuration for deployment, but there are concerns about compatibility due to the server's reliance on local Git execution via `simple-git`.
20 | 
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "git-commands-mcp",
 3 |   "version": "0.1.4",
 4 |   "description": "A Node.js implementation of a Git repository browser using the Model Context Protocol (MCP)",
 5 |   "main": "src/index.js",
 6 |   "type": "module",
 7 |   "bin": {
 8 |     "git-commands-mcp": "src/index.js"
 9 |   },
10 |   "scripts": {
11 |     "start": "node src/index.js",
12 |     "prepublishOnly": "npm ci && echo \"No tests specified - publishing anyway\""
13 |   },
14 |   "author": "sreeram balamurugan",
15 |   "license": "MIT",
16 |   "repository": {
17 |     "type": "git",
18 |     "url": "git+https://github.com/bsreeram08/git-commands-mcp.git"
19 |   },
20 |   "bugs": {
21 |     "url": "https://github.com/bsreeram08/git-commands-mcp/issues"
22 |   },
23 |   "homepage": "https://github.com/bsreeram08/git-commands-mcp#readme",
24 |   "keywords": [
25 |     "git",
26 |     "mcp",
27 |     "model-context-protocol",
28 |     "repository",
29 |     "browser"
30 |   ],
31 |   "dependencies": {
32 |     "@modelcontextprotocol/sdk": "1.5.0",
33 |     "@surfai/docs-mcp": "^1.0.1",
34 |     "fs-extra": "^11.3.0",
35 |     "simple-git": "^3.27.0"
36 |   },
37 |   "engines": {
38 |     "node": ">=14.0.0"
39 |   }
40 | }
41 | 
```

--------------------------------------------------------------------------------
/src/handlers/config-operations.js:
--------------------------------------------------------------------------------

```javascript
 1 | import { simpleGit } from "./common.js";
 2 | 
 3 | /**
 4 |  * Configures git settings for the repository
 5 |  * @param {string} repoPath - Path to the local repository
 6 |  * @param {string} scope - Configuration scope (local, global, system)
 7 |  * @param {string} key - Configuration key
 8 |  * @param {string} value - Configuration value
 9 |  * @returns {Object} - Configuration result
10 |  */
11 | export async function handleGitConfig({
12 |   repo_path,
13 |   scope = "local",
14 |   key,
15 |   value,
16 | }) {
17 |   try {
18 |     const git = simpleGit(repo_path);
19 | 
20 |     // Set the configuration
21 |     await git.addConfig(key, value, false, scope);
22 | 
23 |     return {
24 |       content: [
25 |         {
26 |           type: "text",
27 |           text: JSON.stringify(
28 |             {
29 |               success: true,
30 |               message: `Set ${scope} config ${key}=${value}`,
31 |               key: key,
32 |               value: value,
33 |               scope: scope,
34 |             },
35 |             null,
36 |             2
37 |           ),
38 |         },
39 |       ],
40 |     };
41 |   } catch (error) {
42 |     return {
43 |       content: [
44 |         {
45 |           type: "text",
46 |           text: JSON.stringify(
47 |             { error: `Failed to set git config: ${error.message}` },
48 |             null,
49 |             2
50 |           ),
51 |         },
52 |       ],
53 |       isError: true,
54 |     };
55 |   }
56 | }
57 | 
```

--------------------------------------------------------------------------------
/src/handlers/tag-operations.js:
--------------------------------------------------------------------------------

```javascript
 1 | import { simpleGit } from "./common.js";
 2 | 
 3 | /**
 4 |  * Creates a tag
 5 |  * @param {string} repoPath - Path to the local repository
 6 |  * @param {string} tagName - Name of the tag
 7 |  * @param {string} message - Tag message (for annotated tags)
 8 |  * @param {boolean} annotated - Whether to create an annotated tag
 9 |  * @returns {Object} - Tag creation result
10 |  */
11 | export async function handleGitCreateTag({
12 |   repo_path,
13 |   tag_name,
14 |   message = "",
15 |   annotated = true,
16 | }) {
17 |   try {
18 |     const git = simpleGit(repo_path);
19 | 
20 |     if (annotated) {
21 |       await git.addAnnotatedTag(tag_name, message);
22 |     } else {
23 |       await git.addTag(tag_name);
24 |     }
25 | 
26 |     return {
27 |       content: [
28 |         {
29 |           type: "text",
30 |           text: JSON.stringify(
31 |             {
32 |               success: true,
33 |               message: `Created ${
34 |                 annotated ? "annotated " : ""
35 |               }tag: ${tag_name}`,
36 |               tag: tag_name,
37 |             },
38 |             null,
39 |             2
40 |           ),
41 |         },
42 |       ],
43 |     };
44 |   } catch (error) {
45 |     return {
46 |       content: [
47 |         {
48 |           type: "text",
49 |           text: JSON.stringify(
50 |             { error: `Failed to create tag: ${error.message}` },
51 |             null,
52 |             2
53 |           ),
54 |         },
55 |       ],
56 |       isError: true,
57 |     };
58 |   }
59 | }
60 | 
```

--------------------------------------------------------------------------------
/memory-bank/techContext.md:
--------------------------------------------------------------------------------

```markdown
 1 | # Tech Context: Git Commands MCP Server
 2 | 
 3 | ## Core Technologies
 4 | 
 5 | - **Language:** Node.js (JavaScript)
 6 | - **Package Manager:** Likely npm (presence of `package.json`, `package-lock.json`) or potentially Bun (presence of `bun.lock`). Requires clarification if both are used or if one is primary.
 7 | - **Core Library:** `simple-git` - A Node.js wrapper for the Git command-line interface. This is the primary mechanism for interacting with Git.
 8 | - **MCP Framework:** Relies on an underlying MCP server implementation (details likely in `src/server.js` or dependencies) to handle MCP communication.
 9 | 
10 | ## Development Setup
11 | 
12 | - Requires Node.js runtime installed.
13 | - Requires Git executable installed and accessible in the system PATH.
14 | - Dependencies are managed via `package.json` (and potentially `bun.lock`). Installation likely via `npm install` or `bun install`.
15 | - Server is typically run using a command like `node src/index.js` or a script defined in `package.json`.
16 | 
17 | ## Technical Constraints
18 | 
19 | - **Dependency on Local Git:** The use of `simple-git` inherently ties the server's execution environment to having a functional Git installation.
20 | - **Filesystem Access Requirement:** Tools operating on `repo_path` require direct access to the host filesystem, which is problematic in isolated environments like Docker containers.
21 | - **Authentication:** Handling authentication for remote private repositories relies on the Git configuration (e.g., SSH keys, credential helpers) available in the execution environment. This is difficult to replicate securely and consistently within a generic container.
22 | 
23 | ## Tool Usage Patterns
24 | 
25 | - MCP tools are defined with JSON schemas for input validation.
26 | - Handlers parse inputs and construct `simple-git` commands.
27 | - Error handling wraps potential exceptions from `simple-git`.
28 | 
```

--------------------------------------------------------------------------------
/src/handlers/index.js:
--------------------------------------------------------------------------------

```javascript
 1 | // Import handlers from individual files
 2 | import {
 3 |   handleGitDirectoryStructure,
 4 |   handleGitReadFiles,
 5 |   handleGitSearchCode,
 6 |   handleGitLocalChanges,
 7 | } from "./directory-operations.js";
 8 | import {
 9 |   handleGitCommitHistory,
10 |   handleGitCommitsDetails,
11 |   handleGitCommit,
12 |   handleGitTrack,
13 | } from "./commit-operations.js";
14 | import {
15 |   handleGitBranchDiff,
16 |   handleGitCheckoutBranch,
17 |   handleGitDeleteBranch,
18 |   handleGitMergeBranch,
19 | } from "./branch-operations.js";
20 | import {
21 |   handleGitPush,
22 |   handleGitPull,
23 |   handleGitRemote,
24 | } from "./remote-operations.js";
25 | import { handleGitStash } from "./stash-operations.js";
26 | import { handleGitCreateTag } from "./tag-operations.js";
27 | import { handleGitRebase, handleGitReset } from "./advanced-operations.js";
28 | import { handleGitConfig } from "./config-operations.js";
29 | import {
30 |   handleGitArchive,
31 |   handleGitAttributes,
32 |   handleGitBlame,
33 |   handleGitClean,
34 |   handleGitHooks,
35 |   handleGitLFS,
36 |   handleGitLFSFetch,
37 |   handleGitRevert,
38 | } from "./other-operations.js";
39 | 
40 | // Re-export all handlers
41 | export {
42 |   // Directory operations
43 |   handleGitDirectoryStructure,
44 |   handleGitReadFiles,
45 |   handleGitSearchCode,
46 |   handleGitLocalChanges,
47 | 
48 |   // Commit operations
49 |   handleGitCommitHistory,
50 |   handleGitCommitsDetails,
51 |   handleGitCommit,
52 |   handleGitTrack,
53 | 
54 |   // Branch operations
55 |   handleGitBranchDiff,
56 |   handleGitCheckoutBranch,
57 |   handleGitDeleteBranch,
58 |   handleGitMergeBranch,
59 | 
60 |   // Remote operations
61 |   handleGitPush,
62 |   handleGitPull,
63 |   handleGitRemote,
64 | 
65 |   // Stash operations
66 |   handleGitStash,
67 | 
68 |   // Tag operations
69 |   handleGitCreateTag,
70 | 
71 |   // Advanced operations
72 |   handleGitRebase,
73 |   handleGitReset,
74 | 
75 |   // Config operations
76 |   handleGitConfig,
77 | 
78 |   // Other operations
79 |   handleGitArchive,
80 |   handleGitAttributes,
81 |   handleGitBlame,
82 |   handleGitClean,
83 |   handleGitHooks,
84 |   handleGitLFS,
85 |   handleGitLFSFetch,
86 |   handleGitRevert,
87 | };
88 | 
```

--------------------------------------------------------------------------------
/src/utils/git.js:
--------------------------------------------------------------------------------

```javascript
 1 | import { simpleGit } from "simple-git";
 2 | import fs from "fs-extra";
 3 | import path from "path";
 4 | import os from "os";
 5 | import crypto from "crypto";
 6 | 
 7 | /**
 8 |  * Clones a Git repository or reuses an existing clone
 9 |  * @param {string} repoUrl - The URL of the Git repository to clone
10 |  * @returns {Promise<string>} - Path to the cloned repository
11 |  */
12 | export async function cloneRepo(repoUrl) {
13 |   // Create deterministic directory name based on repo URL
14 |   const repoHash = crypto
15 |     .createHash("sha256")
16 |     .update(repoUrl)
17 |     .digest("hex")
18 |     .slice(0, 12);
19 |   const tempDir = path.join(os.tmpdir(), `github_tools_${repoHash}`);
20 | 
21 |   // Check if directory exists and is a valid git repo
22 |   if (await fs.pathExists(tempDir)) {
23 |     try {
24 |       const git = simpleGit(tempDir);
25 |       const remotes = await git.getRemotes(true);
26 |       if (remotes.length > 0 && remotes[0].refs.fetch === repoUrl) {
27 |         // Pull latest changes
28 |         await git.pull();
29 |         return tempDir;
30 |       }
31 |     } catch (error) {
32 |       // If there's any error with existing repo, clean it up
33 |       await fs.remove(tempDir);
34 |     }
35 |   }
36 | 
37 |   // Create directory and clone repository
38 |   await fs.ensureDir(tempDir);
39 |   try {
40 |     await simpleGit().clone(repoUrl, tempDir);
41 |     return tempDir;
42 |   } catch (error) {
43 |     // Clean up on error
44 |     await fs.remove(tempDir);
45 |     throw new Error(`Failed to clone repository: ${error.message}`);
46 |   }
47 | }
48 | 
49 | /**
50 |  * Generates a tree representation of a directory structure
51 |  * @param {string} dirPath - Path to the directory
52 |  * @param {string} prefix - Prefix for the current line (used for recursion)
53 |  * @returns {Promise<string>} - ASCII tree representation of the directory
54 |  */
55 | export async function getDirectoryTree(dirPath, prefix = "") {
56 |   let output = "";
57 |   const entries = await fs.readdir(dirPath);
58 |   entries.sort();
59 | 
60 |   for (let i = 0; i < entries.length; i++) {
61 |     const entry = entries[i];
62 |     if (entry.startsWith(".git")) continue;
63 | 
64 |     const isLast = i === entries.length - 1;
65 |     const currentPrefix = isLast ? "└── " : "├── ";
66 |     const nextPrefix = isLast ? "    " : "│   ";
67 |     const entryPath = path.join(dirPath, entry);
68 | 
69 |     output += prefix + currentPrefix + entry + "\n";
70 | 
71 |     const stats = await fs.stat(entryPath);
72 |     if (stats.isDirectory()) {
73 |       output += await getDirectoryTree(entryPath, prefix + nextPrefix);
74 |     }
75 |   }
76 | 
77 |   return output;
78 | }
79 | 
```

--------------------------------------------------------------------------------
/memory-bank/activeContext.md:
--------------------------------------------------------------------------------

```markdown
 1 | # Active Context: Git Commands MCP Server (2025-05-02)
 2 | 
 3 | ## Current Focus
 4 | 
 5 | Completed a full code review of the `git-commands-mcp` server (`src/` directory) to understand its implementation details before evaluating the Smithery deployment PR. The review confirms the initial concern regarding containerization compatibility.
 6 | 
 7 | ## Recent Changes
 8 | 
 9 | - Memory Bank initialized with core documentation files.
10 | - Reviewed all source files in `src/`: `index.js`, `server.js`, `utils/git.js`, and all handler files in `src/handlers/`.
11 | 
12 | ## Next Steps
13 | 
14 | 1.  **Update Memory Bank:** Refine `productContext.md`, `systemPatterns.md`, and `progress.md` based on the detailed code review findings.
15 | 2.  **Present Findings:** Communicate the detailed analysis of `repo_url` vs. `repo_path` tool implementation and the resulting container incompatibility to the user.
16 | 3.  **Discuss Smithery PR:** Re-engage on the Smithery PR, specifically asking for the `Dockerfile` and config file contents, now with the full context of the server's limitations.
17 | 4.  **Evaluate Options:** Discuss potential paths forward regarding the Smithery deployment (e.g., deploying a limited subset of tools, modifying the server, or concluding it's unsuitable for this deployment model).
18 | 
19 | ## Active Decisions & Considerations
20 | 
21 | - **Confirmation of Incompatibility:** The code review confirms that tools using `repo_path` directly interact with the local filesystem path provided, making them incompatible with standard container isolation.
22 | - **`repo_url` Tool Feasibility:** Tools using `repo_url` operate on temporary clones within the server's environment. These _could_ work in a container if prerequisites (Git, network, permissions, auth) are met, but auth remains a major hurdle.
23 | - **Deployment Scope:** Any potential Smithery deployment would likely be limited to the `repo_url`-based tools, significantly reducing the server's advertised functionality.
24 | 
25 | ## Key Learnings/Insights
26 | 
27 | - **Explicit Design Distinction:** The codebase clearly separates `repo_url` tools (using `cloneRepo` for temporary local copies) from `repo_path` tools (using `simpleGit` or `fs` directly on the provided path).
28 | - **Filesystem Dependency:** Many tools, including hooks and attributes management, rely on direct `fs` access within the target `repo_path`, further cementing the local execution dependency.
29 | - **`execPromise` Usage:** Some tools (`git_search_code`, `git_lfs`, `git_lfs_fetch`) use direct command execution (`execPromise`), adding another layer of dependency on the execution environment's PATH and installed tools (like `git lfs`).
30 | 
```

--------------------------------------------------------------------------------
/memory-bank/productContext.md:
--------------------------------------------------------------------------------

```markdown
 1 | # Product Context: Git Commands MCP Server
 2 | 
 3 | ## Problem Solved
 4 | 
 5 | Interacting with Git repositories often requires manual command-line usage or complex scripting. This MCP server aims to simplify Git interactions by providing a standardized, programmatic interface via MCP tools. This is particularly useful for:
 6 | 
 7 | - **AI Agents:** Enabling AI agents like Cline to perform Git operations as part of development tasks.
 8 | - **Automation:** Facilitating automated workflows that involve Git (e.g., CI/CD-like tasks, repository management).
 9 | - **Integration:** Providing a consistent way to integrate Git functionality into other applications or services that understand MCP.
10 | 
11 | ## How It Should Work
12 | 
13 | - Users (or agents) invoke specific Git tools provided by the server (e.g., `git_directory_structure`, `git_commit`, `git_branch_diff`).
14 | - The server receives the request, validates parameters, and routes to the appropriate handler.
15 | - **`repo_url`-based Tools:** For tools operating on remote repositories (identified by `repo_url` parameter), the server uses a utility (`cloneRepo` in `src/utils/git.js`) to clone the repository into a temporary directory within the server's own execution environment. Subsequent operations for that request (e.g., reading files, getting history, diffing branches) are performed on this temporary local clone using `simple-git`, `fs`, or direct `git` commands (`execPromise`). This _might_ work in a container if prerequisites (Git installed, network, permissions, auth) are met.
16 | - **`repo_path`-based Tools:** For tools operating on local repositories (identified by `repo_path` parameter), the server initializes `simple-git` directly with the provided path or uses `fs`/`execPromise` to interact with files/commands within that path. This requires the server process to have direct read/write access to the specified filesystem path. **This mode is fundamentally incompatible with standard containerized deployment (like Docker/Smithery) due to filesystem isolation.**
17 | - The server processes the output from the underlying Git operation (via `simple-git`, `fs`, or `execPromise`) and returns a structured JSON response to the caller.
18 | 
19 | ## User Experience Goals
20 | 
21 | - **Simplicity:** Abstract away the complexities of Git command-line syntax.
22 | - **Reliability:** Execute Git commands accurately and handle errors gracefully.
23 | - **Discoverability:** Clearly define available tools and their parameters through the MCP schema.
24 | - **Flexibility:** Support a wide range of common Git operations for both local and remote workflows.
25 | 
26 | ## Target Users
27 | 
28 | - AI Development Agents (like Cline)
29 | - Developers building automation scripts
30 | - Platform engineers integrating Git operations
31 | 
```

--------------------------------------------------------------------------------
/memory-bank/progress.md:
--------------------------------------------------------------------------------

```markdown
 1 | # Progress & Status: Git Commands MCP Server (2025-05-02)
 2 | 
 3 | ## What Works
 4 | 
 5 | - The MCP server successfully exposes a wide range of Git commands as tools, defined in `src/server.js`.
 6 | - **`repo_url`-based Tools:** These tools (e.g., `git_directory_structure`, `git_read_files`, `git_commit_history`) function by cloning the remote repo into a temporary directory within the server's environment (`os.tmpdir()`) and operating on that clone. This works reliably when the server runs locally with network access and appropriate credentials (if needed).
 7 | - **`repo_path`-based Tools:** These tools (e.g., `git_commit`, `git_push`, `git_local_changes`, `git_checkout_branch`) function correctly _only when the server process has direct filesystem access_ to the specified `repo_path`.
 8 | 
 9 | ## What's Left to Build / Current Tasks
10 | 
11 | - **Evaluate Smithery Deployment PR:** Analyze the feasibility of the proposed Docker/Smithery deployment in light of the confirmed incompatibility of `repo_path`-based tools with containerization. This requires reviewing the PR's `Dockerfile` and Smithery config file.
12 | - **Address Container Compatibility:** Decide how to handle the incompatibility issue. Options include:
13 |   - Deploying only the `repo_url`-based tools.
14 |   - Modifying the server architecture (significant effort).
15 |   - Rejecting the containerized deployment approach for this server.
16 | 
17 | ## Current Status
18 | 
19 | - **Code Review Complete:** Full review of `src/` directory completed.
20 | - **Memory Bank Updated:** Core memory bank files created and refined based on code review.
21 | - **Blocked:** Further action on the Smithery PR is blocked pending review of its specific files (`Dockerfile`, config) and a decision on how to handle the `repo_path` tool incompatibility.
22 | 
23 | ## Known Issues
24 | 
25 | - **Fundamental Container Incompatibility (`repo_path` tools):** Tools requiring `repo_path` cannot function in a standard isolated container (like Docker/Smithery) because the container lacks access to the user-specified host filesystem paths.
26 | - **Container Prerequisites (`repo_url` tools):** For `repo_url` tools to work in a container, the container needs:
27 |   - Git installed.
28 |   - Network access.
29 |   - Write permissions to its temporary directory.
30 |   - A mechanism to handle authentication for private repositories (major challenge).
31 | - **Dependency on Local Tools:** Some handlers rely on `git lfs` being installed locally.
32 | 
33 | ## Evolution of Decisions
34 | 
35 | - The initial design leveraging `simple-git` and direct filesystem access (`repo_path`) is effective for local use but unsuitable for standard containerized deployment.
36 | - The `cloneRepo` utility for `repo_url` tools provides a potential (but limited) path for containerization, focusing only on remote repository interactions.
37 | - The Smithery PR necessitates a decision on whether to adapt the server, limit its deployed scope, or abandon containerization for this specific MCP.
38 | 
```

--------------------------------------------------------------------------------
/src/handlers/advanced-operations.js:
--------------------------------------------------------------------------------

```javascript
  1 | import { simpleGit } from "./common.js";
  2 | 
  3 | /**
  4 |  * Handles git rebase operations
  5 |  * @param {string} repoPath - Path to the local repository
  6 |  * @param {string} onto - Branch or commit to rebase onto
  7 |  * @param {boolean} interactive - Whether to perform an interactive rebase
  8 |  * @returns {Object} - Rebase result
  9 |  */
 10 | export async function handleGitRebase({
 11 |   repo_path,
 12 |   onto,
 13 |   interactive = false,
 14 | }) {
 15 |   try {
 16 |     // For interactive rebase, we need to use exec as simple-git doesn't support it well
 17 |     if (interactive) {
 18 |       return {
 19 |         content: [
 20 |           {
 21 |             type: "text",
 22 |             text: JSON.stringify(
 23 |               { error: "Interactive rebase not supported through API" },
 24 |               null,
 25 |               2
 26 |             ),
 27 |           },
 28 |         ],
 29 |         isError: true,
 30 |       };
 31 |     }
 32 | 
 33 |     const git = simpleGit(repo_path);
 34 |     const rebaseResult = await git.rebase([onto]);
 35 | 
 36 |     return {
 37 |       content: [
 38 |         {
 39 |           type: "text",
 40 |           text: JSON.stringify(
 41 |             {
 42 |               success: true,
 43 |               message: `Rebased onto ${onto}`,
 44 |               result: rebaseResult,
 45 |             },
 46 |             null,
 47 |             2
 48 |           ),
 49 |         },
 50 |       ],
 51 |     };
 52 |   } catch (error) {
 53 |     return {
 54 |       content: [
 55 |         {
 56 |           type: "text",
 57 |           text: JSON.stringify(
 58 |             {
 59 |               error: `Failed to rebase: ${error.message}`,
 60 |               conflicts: error.git ? error.git.conflicts : null,
 61 |             },
 62 |             null,
 63 |             2
 64 |           ),
 65 |         },
 66 |       ],
 67 |       isError: true,
 68 |     };
 69 |   }
 70 | }
 71 | 
 72 | /**
 73 |  * Resets repository to specified commit or state
 74 |  * @param {string} repoPath - Path to the local repository
 75 |  * @param {string} mode - Reset mode (soft, mixed, hard)
 76 |  * @param {string} to - Commit or reference to reset to
 77 |  * @returns {Object} - Reset result
 78 |  */
 79 | export async function handleGitReset({
 80 |   repo_path,
 81 |   mode = "mixed",
 82 |   to = "HEAD",
 83 | }) {
 84 |   try {
 85 |     const git = simpleGit(repo_path);
 86 | 
 87 |     // Check valid mode
 88 |     if (!["soft", "mixed", "hard"].includes(mode)) {
 89 |       return {
 90 |         content: [
 91 |           {
 92 |             type: "text",
 93 |             text: JSON.stringify(
 94 |               {
 95 |                 error: `Invalid reset mode: ${mode}. Use 'soft', 'mixed', or 'hard'.`,
 96 |               },
 97 |               null,
 98 |               2
 99 |             ),
100 |           },
101 |         ],
102 |         isError: true,
103 |       };
104 |     }
105 | 
106 |     // Perform the reset
107 |     await git.reset([`--${mode}`, to]);
108 | 
109 |     return {
110 |       content: [
111 |         {
112 |           type: "text",
113 |           text: JSON.stringify(
114 |             {
115 |               success: true,
116 |               message: `Reset (${mode}) to ${to}`,
117 |               mode: mode,
118 |               target: to,
119 |             },
120 |             null,
121 |             2
122 |           ),
123 |         },
124 |       ],
125 |     };
126 |   } catch (error) {
127 |     return {
128 |       content: [
129 |         {
130 |           type: "text",
131 |           text: JSON.stringify(
132 |             { error: `Failed to reset repository: ${error.message}` },
133 |             null,
134 |             2
135 |           ),
136 |         },
137 |       ],
138 |       isError: true,
139 |     };
140 |   }
141 | }
142 | 
```

--------------------------------------------------------------------------------
/src/handlers/stash-operations.js:
--------------------------------------------------------------------------------

```javascript
  1 | import { simpleGit } from "./common.js";
  2 | 
  3 | /**
  4 |  * Creates or applies a stash
  5 |  * @param {string} repoPath - Path to the local repository
  6 |  * @param {string} action - Stash action (save, pop, apply, list, drop)
  7 |  * @param {string} message - Stash message (for save action)
  8 |  * @param {number} index - Stash index (for pop, apply, drop actions)
  9 |  * @returns {Object} - Stash operation result
 10 |  */
 11 | export async function handleGitStash({
 12 |   repo_path,
 13 |   action = "save",
 14 |   message = "",
 15 |   index = 0,
 16 | }) {
 17 |   try {
 18 |     const git = simpleGit(repo_path);
 19 | 
 20 |     let result;
 21 |     switch (action) {
 22 |       case "save":
 23 |         result = await git.stash(["save", message]);
 24 |         return {
 25 |           content: [
 26 |             {
 27 |               type: "text",
 28 |               text: JSON.stringify(
 29 |                 {
 30 |                   success: true,
 31 |                   message: "Changes stashed successfully",
 32 |                   stash_message: message,
 33 |                 },
 34 |                 null,
 35 |                 2
 36 |               ),
 37 |             },
 38 |           ],
 39 |         };
 40 | 
 41 |       case "pop":
 42 |         result = await git.stash(["pop", index.toString()]);
 43 |         return {
 44 |           content: [
 45 |             {
 46 |               type: "text",
 47 |               text: JSON.stringify(
 48 |                 {
 49 |                   success: true,
 50 |                   message: `Applied and dropped stash@{${index}}`,
 51 |                 },
 52 |                 null,
 53 |                 2
 54 |               ),
 55 |             },
 56 |           ],
 57 |         };
 58 | 
 59 |       case "apply":
 60 |         result = await git.stash(["apply", index.toString()]);
 61 |         return {
 62 |           content: [
 63 |             {
 64 |               type: "text",
 65 |               text: JSON.stringify(
 66 |                 {
 67 |                   success: true,
 68 |                   message: `Applied stash@{${index}}`,
 69 |                 },
 70 |                 null,
 71 |                 2
 72 |               ),
 73 |             },
 74 |           ],
 75 |         };
 76 | 
 77 |       case "list":
 78 |         result = await git.stash(["list"]);
 79 |         // Parse the stash list
 80 |         const stashList = result
 81 |           .trim()
 82 |           .split("\n")
 83 |           .filter((line) => line.trim() !== "")
 84 |           .map((line) => {
 85 |             const match = line.match(/stash@\{(\d+)\}: (.*)/);
 86 |             if (match) {
 87 |               return {
 88 |                 index: parseInt(match[1]),
 89 |                 description: match[2],
 90 |               };
 91 |             }
 92 |             return null;
 93 |           })
 94 |           .filter((item) => item !== null);
 95 | 
 96 |         return {
 97 |           content: [
 98 |             {
 99 |               type: "text",
100 |               text: JSON.stringify(
101 |                 {
102 |                   success: true,
103 |                   stashes: stashList,
104 |                 },
105 |                 null,
106 |                 2
107 |               ),
108 |             },
109 |           ],
110 |         };
111 | 
112 |       case "drop":
113 |         result = await git.stash(["drop", index.toString()]);
114 |         return {
115 |           content: [
116 |             {
117 |               type: "text",
118 |               text: JSON.stringify(
119 |                 {
120 |                   success: true,
121 |                   message: `Dropped stash@{${index}}`,
122 |                 },
123 |                 null,
124 |                 2
125 |               ),
126 |             },
127 |           ],
128 |         };
129 | 
130 |       default:
131 |         return {
132 |           content: [
133 |             {
134 |               type: "text",
135 |               text: JSON.stringify(
136 |                 { error: `Unknown stash action: ${action}` },
137 |                 null,
138 |                 2
139 |               ),
140 |             },
141 |           ],
142 |           isError: true,
143 |         };
144 |     }
145 |   } catch (error) {
146 |     return {
147 |       content: [
148 |         {
149 |           type: "text",
150 |           text: JSON.stringify(
151 |             { error: `Failed to perform stash operation: ${error.message}` },
152 |             null,
153 |             2
154 |           ),
155 |         },
156 |       ],
157 |       isError: true,
158 |     };
159 |   }
160 | }
161 | 
```

--------------------------------------------------------------------------------
/memory-bank/systemPatterns.md:
--------------------------------------------------------------------------------

```markdown
 1 | # System Patterns: Git Commands MCP Server
 2 | 
 3 | ## Core Architecture
 4 | 
 5 | The server follows a standard MCP server pattern:
 6 | 
 7 | 1.  **Initialization:** Sets up an MCP server instance (`src/server.js`).
 8 | 2.  **Tool Registration:** Defines and registers available Git tools with their input schemas (`src/handlers/index.js`). Each tool corresponds to a specific Git operation.
 9 | 3.  **Request Handling:** Listens for incoming MCP requests. When a tool is invoked, the server routes the request to the appropriate handler function.
10 | 4.  **Git Interaction:** Handler functions utilize the `simple-git` library (`src/utils/git.js`) to interact with the Git command-line executable.
11 | 5.  **Response Formatting:** Handler functions process the output from `simple-git` (or handle errors) and return a structured JSON response conforming to the MCP standard.
12 | 
13 | ## Key Design Patterns
14 | 
15 | - **Handler Mapping:** A map (`this.handlersMap` in `src/handlers/index.js`) associates tool names (e.g., `git_clone`) with their corresponding implementation functions (e.g., `handleGitClone`).
16 | - **Tool Listing:** A separate list (`this.toolsList` in `src/handlers/index.js`) defines the tools exposed via the MCP interface, including their schemas. This ensures separation between internal implementation and external interface definition.
17 | - **Categorization:** Handlers are grouped into categories (`this.handlerCategories` in `src/handlers/index.js`) for organization, although this is primarily for internal code structure.
18 | - **Wrapper Library:** Abstraction of direct Git command execution through the `simple-git` library. This simplifies handler logic but introduces a dependency on the local Git environment.
19 | 
20 | ## Critical Implementation Paths
21 | 
22 | - **`repo_path`-based Operations:** Tools accepting a `repo_path` parameter (e.g., `git_commit`, `git_push`, `git_local_changes`, `git_checkout_branch`) initialize `simpleGit` directly with this path or use `fs`/`execPromise` within this path. This requires the server process to have direct read/write access to the specified local filesystem path. **This path is incompatible with standard container isolation.**
23 | - **`repo_url`-based Operations:** Tools accepting a `repo_url` parameter (e.g., `git_directory_structure`, `git_read_files`, `git_commit_history`) use the `cloneRepo` utility (`src/utils/git.js`). This clones the remote repo into a temporary directory within the server's execution environment (`os.tmpdir()`) and performs operations on that temporary clone. **This path _might_ be adaptable to containerization if prerequisites are met.**
24 | - **Direct Command Execution:** Some tools (`git_search_code`, `git_lfs`, `git_lfs_fetch`) use `execPromise` to run `git` or `git lfs` commands directly, relying on these being available in the server environment's PATH.
25 | 
26 | ## Dependencies
27 | 
28 | - **Local Git Installation:** `simple-git` and direct `git` commands require a functional Git executable available in the system's PATH where the server runs.
29 | - **Node.js `fs` Module:** Used for direct file operations in some handlers (e.g., `handleGitHooks`, `handleGitAttributes`, reading files from temporary clones).
30 | - **Node.js `os` Module:** Used by `cloneRepo` to determine the temporary directory location.
31 | - **Node.js `crypto` Module:** Used by `cloneRepo` to generate deterministic temporary directory names.
32 | - **Filesystem Access (`repo_path` tools):** Require direct read/write access to the user-specified local repository paths.
33 | - **Filesystem Access (`repo_url` tools):** Require write access to the server's temporary directory (`os.tmpdir()`).
34 | - **Network Access (`repo_url` tools):** Require network connectivity to clone remote Git repositories.
35 | - **Authentication (`repo_url` tools):** Cloning private remote repositories requires credentials (e.g., SSH keys, HTTPS tokens) to be configured and accessible within the server's execution environment. This is a major challenge for containerized deployments.
36 | - **Optional Tools:** `git lfs` commands require the `git-lfs` extension to be installed in the server's environment.
37 | 
```

--------------------------------------------------------------------------------
/src/handlers/branch-operations.js:
--------------------------------------------------------------------------------

```javascript
  1 | import { simpleGit, cloneRepo } from "./common.js";
  2 | 
  3 | /**
  4 |  * Handles the git_branch_diff tool request
  5 |  * @param {Object} params - Tool parameters
  6 |  * @param {string} params.repo_url - Repository URL
  7 |  * @param {string} params.source_branch - Source branch name
  8 |  * @param {string} params.target_branch - Target branch name
  9 |  * @param {boolean} params.show_patch - Whether to include diff patches
 10 |  * @returns {Object} - Tool response
 11 |  */
 12 | export async function handleGitBranchDiff({
 13 |   repo_url,
 14 |   source_branch,
 15 |   target_branch,
 16 |   show_patch = false,
 17 | }) {
 18 |   try {
 19 |     const repoPath = await cloneRepo(repo_url);
 20 |     const git = simpleGit(repoPath);
 21 | 
 22 |     // Make sure both branches exist locally
 23 |     const branches = await git.branch();
 24 |     if (!branches.all.includes(source_branch)) {
 25 |       await git.fetch("origin", source_branch);
 26 |       await git.checkout(source_branch);
 27 |     }
 28 | 
 29 |     if (!branches.all.includes(target_branch)) {
 30 |       await git.fetch("origin", target_branch);
 31 |     }
 32 | 
 33 |     // Get the diff between branches
 34 |     const diffOptions = ["--name-status"];
 35 |     if (show_patch) {
 36 |       diffOptions.push("--patch");
 37 |     }
 38 | 
 39 |     const diff = await git.diff([
 40 |       ...diffOptions,
 41 |       `${target_branch}...${source_branch}`,
 42 |     ]);
 43 | 
 44 |     // Get commit range information
 45 |     const logSummary = await git.log({
 46 |       from: target_branch,
 47 |       to: source_branch,
 48 |     });
 49 | 
 50 |     const result = {
 51 |       commits_count: logSummary.total,
 52 |       diff_summary: diff,
 53 |     };
 54 | 
 55 |     return {
 56 |       content: [
 57 |         {
 58 |           type: "text",
 59 |           text: JSON.stringify(result, null, 2),
 60 |         },
 61 |       ],
 62 |     };
 63 |   } catch (error) {
 64 |     return {
 65 |       content: [
 66 |         {
 67 |           type: "text",
 68 |           text: JSON.stringify(
 69 |             { error: `Failed to get branch diff: ${error.message}` },
 70 |             null,
 71 |             2
 72 |           ),
 73 |         },
 74 |       ],
 75 |       isError: true,
 76 |     };
 77 |   }
 78 | }
 79 | 
 80 | /**
 81 |  * Creates and checks out a new branch
 82 |  * @param {string} repoPath - Path to the local repository
 83 |  * @param {string} branchName - Name of the new branch
 84 |  * @param {string} startPoint - Starting point for the branch (optional)
 85 |  * @returns {Object} - Branch creation result
 86 |  */
 87 | export async function handleGitCheckoutBranch({
 88 |   repo_path,
 89 |   branch_name,
 90 |   start_point = null,
 91 |   create = false,
 92 | }) {
 93 |   try {
 94 |     const git = simpleGit(repo_path);
 95 | 
 96 |     if (create) {
 97 |       // Create and checkout a new branch
 98 |       if (start_point) {
 99 |         await git.checkoutBranch(branch_name, start_point);
100 |       } else {
101 |         await git.checkoutLocalBranch(branch_name);
102 |       }
103 | 
104 |       return {
105 |         content: [
106 |           {
107 |             type: "text",
108 |             text: JSON.stringify(
109 |               {
110 |                 success: true,
111 |                 message: `Created and checked out new branch: ${branch_name}`,
112 |                 branch: branch_name,
113 |               },
114 |               null,
115 |               2
116 |             ),
117 |           },
118 |         ],
119 |       };
120 |     } else {
121 |       // Just checkout an existing branch
122 |       await git.checkout(branch_name);
123 | 
124 |       return {
125 |         content: [
126 |           {
127 |             type: "text",
128 |             text: JSON.stringify(
129 |               {
130 |                 success: true,
131 |                 message: `Checked out branch: ${branch_name}`,
132 |                 branch: branch_name,
133 |               },
134 |               null,
135 |               2
136 |             ),
137 |           },
138 |         ],
139 |       };
140 |     }
141 |   } catch (error) {
142 |     return {
143 |       content: [
144 |         {
145 |           type: "text",
146 |           text: JSON.stringify(
147 |             { error: `Failed to checkout branch: ${error.message}` },
148 |             null,
149 |             2
150 |           ),
151 |         },
152 |       ],
153 |       isError: true,
154 |     };
155 |   }
156 | }
157 | 
158 | /**
159 |  * Deletes a branch
160 |  * @param {string} repoPath - Path to the local repository
161 |  * @param {string} branchName - Name of the branch to delete
162 |  * @param {boolean} force - Whether to force deletion
163 |  * @returns {Object} - Branch deletion result
164 |  */
165 | export async function handleGitDeleteBranch({
166 |   repo_path,
167 |   branch_name,
168 |   force = false,
169 | }) {
170 |   try {
171 |     const git = simpleGit(repo_path);
172 | 
173 |     // Get current branch to prevent deleting the active branch
174 |     const currentBranch = await git.branch();
175 |     if (currentBranch.current === branch_name) {
176 |       return {
177 |         content: [
178 |           {
179 |             type: "text",
180 |             text: JSON.stringify(
181 |               { error: "Cannot delete the currently checked out branch" },
182 |               null,
183 |               2
184 |             ),
185 |           },
186 |         ],
187 |         isError: true,
188 |       };
189 |     }
190 | 
191 |     // Delete the branch
192 |     if (force) {
193 |       await git.deleteLocalBranch(branch_name, true);
194 |     } else {
195 |       await git.deleteLocalBranch(branch_name);
196 |     }
197 | 
198 |     return {
199 |       content: [
200 |         {
201 |           type: "text",
202 |           text: JSON.stringify(
203 |             {
204 |               success: true,
205 |               message: `Deleted branch: ${branch_name}`,
206 |               branch: branch_name,
207 |             },
208 |             null,
209 |             2
210 |           ),
211 |         },
212 |       ],
213 |     };
214 |   } catch (error) {
215 |     return {
216 |       content: [
217 |         {
218 |           type: "text",
219 |           text: JSON.stringify(
220 |             { error: `Failed to delete branch: ${error.message}` },
221 |             null,
222 |             2
223 |           ),
224 |         },
225 |       ],
226 |       isError: true,
227 |     };
228 |   }
229 | }
230 | 
231 | /**
232 |  * Merges a source branch into the current branch
233 |  * @param {string} repoPath - Path to the local repository
234 |  * @param {string} sourceBranch - Branch to merge from
235 |  * @param {string} targetBranch - Branch to merge into (optional, uses current branch if not provided)
236 |  * @param {boolean} noFastForward - Whether to create a merge commit even if fast-forward is possible
237 |  * @returns {Object} - Merge result
238 |  */
239 | export async function handleGitMergeBranch({
240 |   repo_path,
241 |   source_branch,
242 |   target_branch = null,
243 |   no_fast_forward = false,
244 | }) {
245 |   try {
246 |     const git = simpleGit(repo_path);
247 | 
248 |     // If target branch is specified, checkout to it first
249 |     if (target_branch) {
250 |       await git.checkout(target_branch);
251 |     }
252 | 
253 |     // Perform the merge
254 |     let mergeOptions = [];
255 |     if (no_fast_forward) {
256 |       mergeOptions = ["--no-ff"];
257 |     }
258 | 
259 |     const mergeResult = await git.merge([...mergeOptions, source_branch]);
260 | 
261 |     return {
262 |       content: [
263 |         {
264 |           type: "text",
265 |           text: JSON.stringify(
266 |             {
267 |               success: true,
268 |               result: mergeResult,
269 |               message: `Merged ${source_branch} into ${
270 |                 target_branch || "current branch"
271 |               }`,
272 |             },
273 |             null,
274 |             2
275 |           ),
276 |         },
277 |       ],
278 |     };
279 |   } catch (error) {
280 |     return {
281 |       content: [
282 |         {
283 |           type: "text",
284 |           text: JSON.stringify(
285 |             {
286 |               error: `Failed to merge branches: ${error.message}`,
287 |               conflicts: error.git ? error.git.conflicts : null,
288 |             },
289 |             null,
290 |             2
291 |           ),
292 |         },
293 |       ],
294 |       isError: true,
295 |     };
296 |   }
297 | }
298 | 
```

--------------------------------------------------------------------------------
/src/handlers/directory-operations.js:
--------------------------------------------------------------------------------

```javascript
  1 | import {
  2 |   execPromise,
  3 |   cloneRepo,
  4 |   getDirectoryTree,
  5 |   simpleGit,
  6 |   path,
  7 |   fs,
  8 | } from "./common.js";
  9 | 
 10 | /**
 11 |  * Handles the git_directory_structure tool request
 12 |  * @param {Object} params - Tool parameters
 13 |  * @param {string} params.repo_url - Repository URL
 14 |  * @returns {Object} - Tool response
 15 |  */
 16 | export async function handleGitDirectoryStructure({ repo_url }) {
 17 |   try {
 18 |     const repoPath = await cloneRepo(repo_url);
 19 |     const tree = await getDirectoryTree(repoPath);
 20 |     return {
 21 |       content: [
 22 |         {
 23 |           type: "text",
 24 |           text: tree,
 25 |         },
 26 |       ],
 27 |     };
 28 |   } catch (error) {
 29 |     return {
 30 |       content: [
 31 |         {
 32 |           type: "text",
 33 |           text: `Error: ${error.message}`,
 34 |         },
 35 |       ],
 36 |       isError: true,
 37 |     };
 38 |   }
 39 | }
 40 | 
 41 | /**
 42 |  * Handles the git_read_files tool request
 43 |  * @param {Object} params - Tool parameters
 44 |  * @param {string} params.repo_url - Repository URL
 45 |  * @param {string[]} params.file_paths - File paths to read
 46 |  * @returns {Object} - Tool response
 47 |  */
 48 | export async function handleGitReadFiles({ repo_url, file_paths }) {
 49 |   try {
 50 |     const repoPath = await cloneRepo(repo_url);
 51 |     const results = {};
 52 | 
 53 |     for (const filePath of file_paths) {
 54 |       const fullPath = path.join(repoPath, filePath);
 55 |       try {
 56 |         if (await fs.pathExists(fullPath)) {
 57 |           results[filePath] = await fs.readFile(fullPath, "utf8");
 58 |         } else {
 59 |           results[filePath] = "Error: File not found";
 60 |         }
 61 |       } catch (error) {
 62 |         results[filePath] = `Error reading file: ${error.message}`;
 63 |       }
 64 |     }
 65 | 
 66 |     return {
 67 |       content: [
 68 |         {
 69 |           type: "text",
 70 |           text: JSON.stringify(results, null, 2),
 71 |         },
 72 |       ],
 73 |     };
 74 |   } catch (error) {
 75 |     return {
 76 |       content: [
 77 |         {
 78 |           type: "text",
 79 |           text: JSON.stringify(
 80 |             { error: `Failed to process repository: ${error.message}` },
 81 |             null,
 82 |             2
 83 |           ),
 84 |         },
 85 |       ],
 86 |       isError: true,
 87 |     };
 88 |   }
 89 | }
 90 | 
 91 | /**
 92 |  * Handles the git_search_code tool request
 93 |  * @param {Object} params - Tool parameters
 94 |  * @param {string} params.repo_url - Repository URL
 95 |  * @param {string} params.pattern - Search pattern (regex or string)
 96 |  * @param {string[]} params.file_patterns - Optional file patterns to filter (e.g., "*.js")
 97 |  * @param {boolean} params.case_sensitive - Whether the search is case sensitive
 98 |  * @param {number} params.context_lines - Number of context lines to include
 99 |  * @returns {Object} - Tool response
100 |  */
101 | export async function handleGitSearchCode({
102 |   repo_url,
103 |   pattern,
104 |   file_patterns = [],
105 |   case_sensitive = false,
106 |   context_lines = 2,
107 | }) {
108 |   try {
109 |     const repoPath = await cloneRepo(repo_url);
110 | 
111 |     // Build the grep command
112 |     let grepCommand = `cd "${repoPath}" && git grep`;
113 | 
114 |     // Add options
115 |     if (!case_sensitive) {
116 |       grepCommand += " -i";
117 |     }
118 | 
119 |     // Add context lines
120 |     grepCommand += ` -n -C${context_lines}`;
121 | 
122 |     // Add pattern (escape quotes in the pattern)
123 |     const escapedPattern = pattern.replace(/"/g, '\\"');
124 |     grepCommand += ` "${escapedPattern}"`;
125 | 
126 |     // Add file patterns if provided
127 |     if (file_patterns && file_patterns.length > 0) {
128 |       grepCommand += ` -- ${file_patterns.join(" ")}`;
129 |     }
130 | 
131 |     // Execute the command
132 |     const { stdout, stderr } = await execPromise(grepCommand);
133 | 
134 |     if (stderr) {
135 |       console.error(`Search error: ${stderr}`);
136 |     }
137 | 
138 |     // Process the results
139 |     const results = [];
140 |     if (stdout) {
141 |       // Split by file sections (git grep output format)
142 |       const fileMatches = stdout.split(/^(?=\S[^:]*:)/m);
143 | 
144 |       for (const fileMatch of fileMatches) {
145 |         if (!fileMatch.trim()) continue;
146 | 
147 |         // Extract file name and matches
148 |         const lines = fileMatch.split("\n");
149 |         const firstLine = lines[0];
150 |         const fileNameMatch = firstLine.match(/^([^:]+):/);
151 | 
152 |         if (fileNameMatch) {
153 |           const fileName = fileNameMatch[1];
154 |           const matches = [];
155 | 
156 |           // Process each line
157 |           let currentMatch = null;
158 |           let contextLines = [];
159 | 
160 |           for (let i = 0; i < lines.length; i++) {
161 |             const line = lines[i];
162 |             // Skip empty lines
163 |             if (!line.trim()) continue;
164 | 
165 |             // Check if this is a line number indicator
166 |             const lineNumberMatch = line.match(/^([^-][^:]+):(\d+):(.*)/);
167 | 
168 |             if (lineNumberMatch) {
169 |               // If we have a previous match, add it to the results
170 |               if (currentMatch) {
171 |                 currentMatch.context_after = contextLines;
172 |                 matches.push(currentMatch);
173 |                 contextLines = [];
174 |               }
175 | 
176 |               // Start a new match
177 |               currentMatch = {
178 |                 file: fileName,
179 |                 line_number: parseInt(lineNumberMatch[2]),
180 |                 content: lineNumberMatch[3],
181 |                 context_before: contextLines,
182 |                 context_after: [],
183 |               };
184 |               contextLines = [];
185 |             } else {
186 |               // This is a context line
187 |               const contextMatch = line.match(/^([^:]+)-(\d+)-(.*)/);
188 |               if (contextMatch) {
189 |                 contextLines.push({
190 |                   line_number: parseInt(contextMatch[2]),
191 |                   content: contextMatch[3],
192 |                 });
193 |               }
194 |             }
195 |           }
196 | 
197 |           // Add the last match if there is one
198 |           if (currentMatch) {
199 |             currentMatch.context_after = contextLines;
200 |             matches.push(currentMatch);
201 |           }
202 | 
203 |           if (matches.length > 0) {
204 |             results.push({
205 |               file: fileName,
206 |               matches: matches,
207 |             });
208 |           }
209 |         }
210 |       }
211 |     }
212 | 
213 |     return {
214 |       content: [
215 |         {
216 |           type: "text",
217 |           text: JSON.stringify(
218 |             {
219 |               pattern: pattern,
220 |               case_sensitive: case_sensitive,
221 |               context_lines: context_lines,
222 |               file_patterns: file_patterns,
223 |               results: results,
224 |               total_matches: results.reduce(
225 |                 (sum, file) => sum + file.matches.length,
226 |                 0
227 |               ),
228 |               total_files: results.length,
229 |             },
230 |             null,
231 |             2
232 |           ),
233 |         },
234 |       ],
235 |     };
236 |   } catch (error) {
237 |     return {
238 |       content: [
239 |         {
240 |           type: "text",
241 |           text: JSON.stringify(
242 |             { error: `Failed to search repository: ${error.message}` },
243 |             null,
244 |             2
245 |           ),
246 |         },
247 |       ],
248 |       isError: true,
249 |     };
250 |   }
251 | }
252 | 
253 | /**
254 |  * Handles the git_local_changes tool request
255 |  * @param {Object} params - Tool parameters
256 |  * @param {string} params.repo_path - Local repository path
257 |  * @returns {Object} - Tool response
258 |  */
259 | export async function handleGitLocalChanges({ repo_path }) {
260 |   try {
261 |     // Use the provided local repo path
262 |     const git = simpleGit(repo_path);
263 | 
264 |     // Get status information
265 |     const status = await git.status();
266 | 
267 |     // Get detailed diff for modified files
268 |     let diffs = {};
269 |     for (const file of status.modified) {
270 |       diffs[file] = await git.diff([file]);
271 |     }
272 | 
273 |     return {
274 |       content: [
275 |         {
276 |           type: "text",
277 |           text: JSON.stringify(
278 |             {
279 |               branch: status.current,
280 |               staged_files: status.staged,
281 |               modified_files: status.modified,
282 |               new_files: status.not_added,
283 |               deleted_files: status.deleted,
284 |               conflicted_files: status.conflicted,
285 |               diffs: diffs,
286 |             },
287 |             null,
288 |             2
289 |           ),
290 |         },
291 |       ],
292 |     };
293 |   } catch (error) {
294 |     return {
295 |       content: [
296 |         {
297 |           type: "text",
298 |           text: JSON.stringify(
299 |             { error: `Failed to get local changes: ${error.message}` },
300 |             null,
301 |             2
302 |           ),
303 |         },
304 |       ],
305 |       isError: true,
306 |     };
307 |   }
308 | }
309 | 
```

--------------------------------------------------------------------------------
/src/handlers/commit-operations.js:
--------------------------------------------------------------------------------

```javascript
  1 | import { path, fs, simpleGit, cloneRepo } from "./common.js";
  2 | 
  3 | /**
  4 |  * Handles the git_commit_history tool request
  5 |  * @param {Object} params - Tool parameters
  6 |  * @param {string} params.repo_url - Repository URL
  7 |  * @param {string} params.branch - Branch name
  8 |  * @param {number} params.max_count - Maximum number of commits
  9 |  * @param {string} params.author - Author filter
 10 |  * @param {string} params.since - Date filter (after)
 11 |  * @param {string} params.until - Date filter (before)
 12 |  * @param {string} params.grep - Message content filter
 13 |  * @returns {Object} - Tool response
 14 |  */
 15 | export async function handleGitCommitHistory({
 16 |   repo_url,
 17 |   branch = "main",
 18 |   max_count = 10,
 19 |   author,
 20 |   since,
 21 |   until,
 22 |   grep,
 23 | }) {
 24 |   try {
 25 |     const repoPath = await cloneRepo(repo_url);
 26 |     const git = simpleGit(repoPath);
 27 | 
 28 |     // Prepare log options
 29 |     const logOptions = {
 30 |       maxCount: max_count,
 31 |     };
 32 | 
 33 |     if (author) {
 34 |       logOptions["--author"] = author;
 35 |     }
 36 | 
 37 |     if (since) {
 38 |       logOptions["--since"] = since;
 39 |     }
 40 | 
 41 |     if (until) {
 42 |       logOptions["--until"] = until;
 43 |     }
 44 | 
 45 |     if (grep) {
 46 |       logOptions["--grep"] = grep;
 47 |     }
 48 | 
 49 |     // Make sure branch exists locally
 50 |     const branches = await git.branch();
 51 |     if (!branches.all.includes(branch)) {
 52 |       await git.fetch("origin", branch);
 53 |     }
 54 | 
 55 |     // Get commit history
 56 |     const log = await git.log(logOptions, branch);
 57 | 
 58 |     // Format the commits
 59 |     const commits = log.all.map((commit) => ({
 60 |       hash: commit.hash,
 61 |       author: commit.author_name,
 62 |       email: commit.author_email,
 63 |       date: commit.date,
 64 |       message: commit.message,
 65 |       body: commit.body || "",
 66 |     }));
 67 | 
 68 |     return {
 69 |       content: [
 70 |         {
 71 |           type: "text",
 72 |           text: JSON.stringify({ commits }, null, 2),
 73 |         },
 74 |       ],
 75 |     };
 76 |   } catch (error) {
 77 |     return {
 78 |       content: [
 79 |         {
 80 |           type: "text",
 81 |           text: JSON.stringify(
 82 |             { error: `Failed to get commit history: ${error.message}` },
 83 |             null,
 84 |             2
 85 |           ),
 86 |         },
 87 |       ],
 88 |       isError: true,
 89 |     };
 90 |   }
 91 | }
 92 | 
 93 | /**
 94 |  * Handles the git_commits_details tool request
 95 |  * @param {Object} params - Tool parameters
 96 |  * @param {string} params.repo_url - Repository URL
 97 |  * @param {string} params.branch - Branch name
 98 |  * @param {number} params.max_count - Maximum number of commits
 99 |  * @param {boolean} params.include_diff - Whether to include diffs
100 |  * @param {string} params.author - Author filter
101 |  * @param {string} params.since - Date filter (after)
102 |  * @param {string} params.until - Date filter (before)
103 |  * @param {string} params.grep - Message content filter
104 |  * @returns {Object} - Tool response
105 |  */
106 | export async function handleGitCommitsDetails({
107 |   repo_url,
108 |   branch = "main",
109 |   max_count = 10,
110 |   include_diff = false,
111 |   author,
112 |   since,
113 |   until,
114 |   grep,
115 | }) {
116 |   try {
117 |     const repoPath = await cloneRepo(repo_url);
118 |     const git = simpleGit(repoPath);
119 | 
120 |     // Ensure branch exists locally
121 |     const branches = await git.branch();
122 |     if (!branches.all.includes(branch)) {
123 |       await git.fetch("origin", branch);
124 |     }
125 | 
126 |     // Prepare log options with full details
127 |     const logOptions = {
128 |       maxCount: max_count,
129 |       "--format": "fuller", // Get more detailed commit info
130 |     };
131 | 
132 |     if (author) {
133 |       logOptions["--author"] = author;
134 |     }
135 | 
136 |     if (since) {
137 |       logOptions["--since"] = since;
138 |     }
139 | 
140 |     if (until) {
141 |       logOptions["--until"] = until;
142 |     }
143 | 
144 |     if (grep) {
145 |       logOptions["--grep"] = grep;
146 |     }
147 | 
148 |     // Get commit history with full details
149 |     const log = await git.log(logOptions, branch);
150 | 
151 |     // Enhance with additional details
152 |     const commitsDetails = [];
153 | 
154 |     for (const commit of log.all) {
155 |       const commitDetails = {
156 |         hash: commit.hash,
157 |         author: commit.author_name,
158 |         author_email: commit.author_email,
159 |         committer: commit.committer_name,
160 |         committer_email: commit.committer_email,
161 |         date: commit.date,
162 |         message: commit.message,
163 |         body: commit.body || "",
164 |         refs: commit.refs,
165 |       };
166 | 
167 |       // Get the commit diff if requested
168 |       if (include_diff) {
169 |         if (commit.parents && commit.parents.length > 0) {
170 |           // For normal commits with parents
171 |           const diff = await git.diff([`${commit.hash}^..${commit.hash}`]);
172 |           commitDetails.diff = diff;
173 |         } else {
174 |           // For initial commits with no parents
175 |           const diff = await git.diff([
176 |             "4b825dc642cb6eb9a060e54bf8d69288fbee4904",
177 |             commit.hash,
178 |           ]);
179 |           commitDetails.diff = diff;
180 |         }
181 | 
182 |         // Get list of changed files
183 |         const showResult = await git.show([
184 |           "--name-status",
185 |           "--oneline",
186 |           commit.hash,
187 |         ]);
188 | 
189 |         // Parse the changed files from the result
190 |         const fileLines = showResult
191 |           .split("\n")
192 |           .slice(1) // Skip the first line (commit summary)
193 |           .filter(Boolean); // Remove empty lines
194 | 
195 |         commitDetails.changed_files = fileLines
196 |           .map((line) => {
197 |             const match = line.match(/^([AMDTRC])\s+(.+)$/);
198 |             if (match) {
199 |               return {
200 |                 status: match[1],
201 |                 file: match[2],
202 |               };
203 |             }
204 |             return null;
205 |           })
206 |           .filter(Boolean);
207 |       }
208 | 
209 |       commitsDetails.push(commitDetails);
210 |     }
211 | 
212 |     return {
213 |       content: [
214 |         {
215 |           type: "text",
216 |           text: JSON.stringify(
217 |             {
218 |               commits: commitsDetails,
219 |             },
220 |             null,
221 |             2
222 |           ),
223 |         },
224 |       ],
225 |     };
226 |   } catch (error) {
227 |     return {
228 |       content: [
229 |         {
230 |           type: "text",
231 |           text: JSON.stringify(
232 |             { error: `Failed to get commit details: ${error.message}` },
233 |             null,
234 |             2
235 |           ),
236 |         },
237 |       ],
238 |       isError: true,
239 |     };
240 |   }
241 | }
242 | 
243 | /**
244 |  * Creates a commit with the specified message
245 |  * @param {string} repoPath - Path to the local repository
246 |  * @param {string} message - Commit message
247 |  * @returns {Object} - Commit result
248 |  */
249 | export async function handleGitCommit({ repo_path, message }) {
250 |   try {
251 |     const git = simpleGit(repo_path);
252 | 
253 |     // Create the commit (only commit what's in the staging area)
254 |     const commitResult = await git.commit(message);
255 | 
256 |     return {
257 |       content: [
258 |         {
259 |           type: "text",
260 |           text: JSON.stringify(
261 |             {
262 |               success: true,
263 |               commit_hash: commitResult.commit,
264 |               commit_message: message,
265 |               summary: commitResult.summary,
266 |             },
267 |             null,
268 |             2
269 |           ),
270 |         },
271 |       ],
272 |     };
273 |   } catch (error) {
274 |     return {
275 |       content: [
276 |         {
277 |           type: "text",
278 |           text: JSON.stringify(
279 |             { error: `Failed to create commit: ${error.message}` },
280 |             null,
281 |             2
282 |           ),
283 |         },
284 |       ],
285 |       isError: true,
286 |     };
287 |   }
288 | }
289 | 
290 | /**
291 |  * Tracks (stages) specific files or all files
292 |  * @param {string} repoPath - Path to the local repository
293 |  * @param {string[]} files - Array of file paths to track/stage (use ["."] for all files)
294 |  * @returns {Object} - Tracking result
295 |  */
296 | export async function handleGitTrack({ repo_path, files = ["."] }) {
297 |   try {
298 |     const git = simpleGit(repo_path);
299 | 
300 |     // Add the specified files to the staging area
301 |     await git.add(files);
302 | 
303 |     // Get status to show what files were tracked
304 |     const status = await git.status();
305 | 
306 |     return {
307 |       content: [
308 |         {
309 |           type: "text",
310 |           text: JSON.stringify(
311 |             {
312 |               success: true,
313 |               message: `Tracked ${
314 |                 files.length === 1 && files[0] === "."
315 |                   ? "all files"
316 |                   : files.length + " files"
317 |               }`,
318 |               staged: status.staged,
319 |               not_staged: status.not_added,
320 |               modified: status.modified,
321 |             },
322 |             null,
323 |             2
324 |           ),
325 |         },
326 |       ],
327 |     };
328 |   } catch (error) {
329 |     return {
330 |       content: [
331 |         {
332 |           type: "text",
333 |           text: JSON.stringify(
334 |             { error: `Failed to track files: ${error.message}` },
335 |             null,
336 |             2
337 |           ),
338 |         },
339 |       ],
340 |       isError: true,
341 |     };
342 |   }
343 | }
344 | 
```

--------------------------------------------------------------------------------
/src/handlers/remote-operations.js:
--------------------------------------------------------------------------------

```javascript
  1 | import { simpleGit } from "./common.js";
  2 | 
  3 | /**
  4 |  * Pushes changes to a remote repository
  5 |  * @param {string} repoPath - Path to the local repository
  6 |  * @param {string} remote - Remote name (default: origin)
  7 |  * @param {string} branch - Branch to push (default: current branch)
  8 |  * @param {boolean} force - Whether to force push
  9 |  * @returns {Object} - Push result
 10 |  */
 11 | export async function handleGitPush({
 12 |   repo_path,
 13 |   remote = "origin",
 14 |   branch = null,
 15 |   force = false,
 16 | }) {
 17 |   try {
 18 |     const git = simpleGit(repo_path);
 19 | 
 20 |     // If no branch specified, get the current branch
 21 |     if (!branch) {
 22 |       const branchInfo = await git.branch();
 23 |       branch = branchInfo.current;
 24 |     }
 25 | 
 26 |     // Perform the push
 27 |     let pushOptions = [];
 28 |     if (force) {
 29 |       pushOptions.push("--force");
 30 |     }
 31 | 
 32 |     const pushResult = await git.push(remote, branch, pushOptions);
 33 | 
 34 |     return {
 35 |       content: [
 36 |         {
 37 |           type: "text",
 38 |           text: JSON.stringify(
 39 |             {
 40 |               success: true,
 41 |               result: pushResult,
 42 |               message: `Pushed ${branch} to ${remote}`,
 43 |             },
 44 |             null,
 45 |             2
 46 |           ),
 47 |         },
 48 |       ],
 49 |     };
 50 |   } catch (error) {
 51 |     return {
 52 |       content: [
 53 |         {
 54 |           type: "text",
 55 |           text: JSON.stringify(
 56 |             { error: `Failed to push changes: ${error.message}` },
 57 |             null,
 58 |             2
 59 |           ),
 60 |         },
 61 |       ],
 62 |       isError: true,
 63 |     };
 64 |   }
 65 | }
 66 | 
 67 | /**
 68 |  * Pulls changes from a remote repository
 69 |  * @param {string} repoPath - Path to the local repository
 70 |  * @param {string} remote - Remote name (default: origin)
 71 |  * @param {string} branch - Branch to pull (default: current branch)
 72 |  * @param {boolean} rebase - Whether to rebase instead of merge
 73 |  * @returns {Object} - Pull result
 74 |  */
 75 | export async function handleGitPull({
 76 |   repo_path,
 77 |   remote = "origin",
 78 |   branch = null,
 79 |   rebase = false,
 80 | }) {
 81 |   try {
 82 |     const git = simpleGit(repo_path);
 83 | 
 84 |     // If no branch specified, use current branch
 85 |     if (!branch) {
 86 |       const branchInfo = await git.branch();
 87 |       branch = branchInfo.current;
 88 |     }
 89 | 
 90 |     // Set up pull options
 91 |     const pullOptions = {};
 92 |     if (rebase) {
 93 |       pullOptions["--rebase"] = null;
 94 |     }
 95 | 
 96 |     // Perform the pull
 97 |     const pullResult = await git.pull(remote, branch, pullOptions);
 98 | 
 99 |     return {
100 |       content: [
101 |         {
102 |           type: "text",
103 |           text: JSON.stringify(
104 |             {
105 |               success: true,
106 |               result: pullResult,
107 |               message: `Pulled from ${remote}/${branch}`,
108 |             },
109 |             null,
110 |             2
111 |           ),
112 |         },
113 |       ],
114 |     };
115 |   } catch (error) {
116 |     return {
117 |       content: [
118 |         {
119 |           type: "text",
120 |           text: JSON.stringify(
121 |             {
122 |               error: `Failed to pull changes: ${error.message}`,
123 |               conflicts: error.git ? error.git.conflicts : null,
124 |             },
125 |             null,
126 |             2
127 |           ),
128 |         },
129 |       ],
130 |       isError: true,
131 |     };
132 |   }
133 | }
134 | 
135 | /**
136 |  * Manages Git remotes
137 |  * @param {string} repoPath - Path to the local repository
138 |  * @param {string} action - Remote action (list, add, remove, set-url, prune, get-url, rename, show)
139 |  * @param {string} name - Remote name
140 |  * @param {string} url - Remote URL (for add and set-url)
141 |  * @param {string} newName - New remote name (for rename)
142 |  * @param {boolean} pushUrl - Whether to set push URL instead of fetch URL (for set-url)
143 |  * @returns {Object} - Operation result
144 |  */
145 | export async function handleGitRemote({
146 |   repo_path,
147 |   action,
148 |   name = "",
149 |   url = "",
150 |   new_name = "",
151 |   push_url = false,
152 | }) {
153 |   try {
154 |     const git = simpleGit(repo_path);
155 | 
156 |     switch (action) {
157 |       case "list":
158 |         // Get all remotes with their URLs
159 |         const remotes = await git.remote(["-v"]);
160 | 
161 |         // Parse the output
162 |         const remotesList = [];
163 |         const lines = remotes.trim().split("\n");
164 | 
165 |         for (const line of lines) {
166 |           const match = line.match(/^([^\s]+)\s+([^\s]+)\s+\(([^)]+)\)$/);
167 |           if (match) {
168 |             const remoteName = match[1];
169 |             const remoteUrl = match[2];
170 |             const purpose = match[3];
171 | 
172 |             // Check if this remote is already in our list
173 |             const existingRemote = remotesList.find(
174 |               (r) => r.name === remoteName
175 |             );
176 | 
177 |             if (existingRemote) {
178 |               if (purpose === "fetch") {
179 |                 existingRemote.fetch_url = remoteUrl;
180 |               } else if (purpose === "push") {
181 |                 existingRemote.push_url = remoteUrl;
182 |               }
183 |             } else {
184 |               const remote = { name: remoteName };
185 | 
186 |               if (purpose === "fetch") {
187 |                 remote.fetch_url = remoteUrl;
188 |               } else if (purpose === "push") {
189 |                 remote.push_url = remoteUrl;
190 |               }
191 | 
192 |               remotesList.push(remote);
193 |             }
194 |           }
195 |         }
196 | 
197 |         return {
198 |           content: [
199 |             {
200 |               type: "text",
201 |               text: JSON.stringify(
202 |                 {
203 |                   success: true,
204 |                   remotes: remotesList,
205 |                 },
206 |                 null,
207 |                 2
208 |               ),
209 |             },
210 |           ],
211 |         };
212 | 
213 |       case "add":
214 |         if (!name) {
215 |           return {
216 |             content: [
217 |               {
218 |                 type: "text",
219 |                 text: JSON.stringify(
220 |                   { error: "Remote name is required for add action" },
221 |                   null,
222 |                   2
223 |                 ),
224 |               },
225 |             ],
226 |             isError: true,
227 |           };
228 |         }
229 | 
230 |         if (!url) {
231 |           return {
232 |             content: [
233 |               {
234 |                 type: "text",
235 |                 text: JSON.stringify(
236 |                   { error: "Remote URL is required for add action" },
237 |                   null,
238 |                   2
239 |                 ),
240 |               },
241 |             ],
242 |             isError: true,
243 |           };
244 |         }
245 | 
246 |         // Add the remote
247 |         await git.remote(["add", name, url]);
248 | 
249 |         return {
250 |           content: [
251 |             {
252 |               type: "text",
253 |               text: JSON.stringify(
254 |                 {
255 |                   success: true,
256 |                   message: `Added remote '${name}' with URL '${url}'`,
257 |                   name: name,
258 |                   url: url,
259 |                 },
260 |                 null,
261 |                 2
262 |               ),
263 |             },
264 |           ],
265 |         };
266 | 
267 |       case "remove":
268 |         if (!name) {
269 |           return {
270 |             content: [
271 |               {
272 |                 type: "text",
273 |                 text: JSON.stringify(
274 |                   { error: "Remote name is required for remove action" },
275 |                   null,
276 |                   2
277 |                 ),
278 |               },
279 |             ],
280 |             isError: true,
281 |           };
282 |         }
283 | 
284 |         // Remove the remote
285 |         await git.remote(["remove", name]);
286 | 
287 |         return {
288 |           content: [
289 |             {
290 |               type: "text",
291 |               text: JSON.stringify(
292 |                 {
293 |                   success: true,
294 |                   message: `Removed remote '${name}'`,
295 |                   name: name,
296 |                 },
297 |                 null,
298 |                 2
299 |               ),
300 |             },
301 |           ],
302 |         };
303 | 
304 |       case "set-url":
305 |         if (!name) {
306 |           return {
307 |             content: [
308 |               {
309 |                 type: "text",
310 |                 text: JSON.stringify(
311 |                   { error: "Remote name is required for set-url action" },
312 |                   null,
313 |                   2
314 |                 ),
315 |               },
316 |             ],
317 |             isError: true,
318 |           };
319 |         }
320 | 
321 |         if (!url) {
322 |           return {
323 |             content: [
324 |               {
325 |                 type: "text",
326 |                 text: JSON.stringify(
327 |                   { error: "Remote URL is required for set-url action" },
328 |                   null,
329 |                   2
330 |                 ),
331 |               },
332 |             ],
333 |             isError: true,
334 |           };
335 |         }
336 | 
337 |         // Set the remote URL (fetch or push)
338 |         const args = ["set-url"];
339 |         if (push_url) {
340 |           args.push("--push");
341 |         }
342 |         args.push(name, url);
343 | 
344 |         await git.remote(args);
345 | 
346 |         return {
347 |           content: [
348 |             {
349 |               type: "text",
350 |               text: JSON.stringify(
351 |                 {
352 |                   success: true,
353 |                   message: `Updated ${
354 |                     push_url ? "push" : "fetch"
355 |                   } URL for remote '${name}' to '${url}'`,
356 |                   name: name,
357 |                   url: url,
358 |                   type: push_url ? "push" : "fetch",
359 |                 },
360 |                 null,
361 |                 2
362 |               ),
363 |             },
364 |           ],
365 |         };
366 | 
367 |       case "get-url":
368 |         if (!name) {
369 |           return {
370 |             content: [
371 |               {
372 |                 type: "text",
373 |                 text: JSON.stringify(
374 |                   { error: "Remote name is required for get-url action" },
375 |                   null,
376 |                   2
377 |                 ),
378 |               },
379 |             ],
380 |             isError: true,
381 |           };
382 |         }
383 | 
384 |         // Get the remote URL(s)
385 |         const getUrlArgs = ["get-url"];
386 |         if (push_url) {
387 |           getUrlArgs.push("--push");
388 |         }
389 |         getUrlArgs.push(name);
390 | 
391 |         const remoteUrl = await git.remote(getUrlArgs);
392 |         const urls = remoteUrl.trim().split("\n");
393 | 
394 |         return {
395 |           content: [
396 |             {
397 |               type: "text",
398 |               text: JSON.stringify(
399 |                 {
400 |                   success: true,
401 |                   name: name,
402 |                   urls: urls,
403 |                   type: push_url ? "push" : "fetch",
404 |                 },
405 |                 null,
406 |                 2
407 |               ),
408 |             },
409 |           ],
410 |         };
411 | 
412 |       case "rename":
413 |         if (!name) {
414 |           return {
415 |             content: [
416 |               {
417 |                 type: "text",
418 |                 text: JSON.stringify(
419 |                   { error: "Remote name is required for rename action" },
420 |                   null,
421 |                   2
422 |                 ),
423 |               },
424 |             ],
425 |             isError: true,
426 |           };
427 |         }
428 | 
429 |         if (!new_name) {
430 |           return {
431 |             content: [
432 |               {
433 |                 type: "text",
434 |                 text: JSON.stringify(
435 |                   { error: "New remote name is required for rename action" },
436 |                   null,
437 |                   2
438 |                 ),
439 |               },
440 |             ],
441 |             isError: true,
442 |           };
443 |         }
444 | 
445 |         // Rename the remote
446 |         await git.remote(["rename", name, new_name]);
447 | 
448 |         return {
449 |           content: [
450 |             {
451 |               type: "text",
452 |               text: JSON.stringify(
453 |                 {
454 |                   success: true,
455 |                   message: `Renamed remote '${name}' to '${new_name}'`,
456 |                   old_name: name,
457 |                   new_name: new_name,
458 |                 },
459 |                 null,
460 |                 2
461 |               ),
462 |             },
463 |           ],
464 |         };
465 | 
466 |       case "prune":
467 |         if (!name) {
468 |           return {
469 |             content: [
470 |               {
471 |                 type: "text",
472 |                 text: JSON.stringify(
473 |                   { error: "Remote name is required for prune action" },
474 |                   null,
475 |                   2
476 |                 ),
477 |               },
478 |             ],
479 |             isError: true,
480 |           };
481 |         }
482 | 
483 |         // Prune the remote
484 |         await git.remote(["prune", name]);
485 | 
486 |         return {
487 |           content: [
488 |             {
489 |               type: "text",
490 |               text: JSON.stringify(
491 |                 {
492 |                   success: true,
493 |                   message: `Pruned remote '${name}'`,
494 |                   name: name,
495 |                 },
496 |                 null,
497 |                 2
498 |               ),
499 |             },
500 |           ],
501 |         };
502 | 
503 |       case "show":
504 |         if (!name) {
505 |           return {
506 |             content: [
507 |               {
508 |                 type: "text",
509 |                 text: JSON.stringify(
510 |                   { error: "Remote name is required for show action" },
511 |                   null,
512 |                   2
513 |                 ),
514 |               },
515 |             ],
516 |             isError: true,
517 |           };
518 |         }
519 | 
520 |         // Show remote details
521 |         const showOutput = await git.raw(["remote", "show", name]);
522 | 
523 |         // Parse the output to extract useful information
524 |         const remoteLines = showOutput.trim().split("\n");
525 |         const remoteInfo = {
526 |           name: name,
527 |           fetch_url: "",
528 |           push_url: "",
529 |           head_branch: "",
530 |           remote_branches: [],
531 |           local_branches: [],
532 |         };
533 | 
534 |         for (const line of remoteLines) {
535 |           const trimmed = line.trim();
536 | 
537 |           if (trimmed.startsWith("Fetch URL:")) {
538 |             remoteInfo.fetch_url = trimmed
539 |               .substring("Fetch URL:".length)
540 |               .trim();
541 |           } else if (trimmed.startsWith("Push  URL:")) {
542 |             remoteInfo.push_url = trimmed.substring("Push  URL:".length).trim();
543 |           } else if (trimmed.startsWith("HEAD branch:")) {
544 |             remoteInfo.head_branch = trimmed
545 |               .substring("HEAD branch:".length)
546 |               .trim();
547 |           } else if (trimmed.startsWith("Remote branch")) {
548 |             // Skip the "Remote branches:" line
549 |           } else if (trimmed.startsWith("Local branch")) {
550 |             // Skip the "Local branches:" line
551 |           } else if (trimmed.includes("merges with remote")) {
552 |             const parts = trimmed.split("merges with remote");
553 |             if (parts.length === 2) {
554 |               const localBranch = parts[0].trim();
555 |               const remoteBranch = parts[1].trim();
556 |               remoteInfo.local_branches.push({
557 |                 local: localBranch,
558 |                 remote: remoteBranch,
559 |               });
560 |             }
561 |           } else if (trimmed.includes("tracked")) {
562 |             const branch = trimmed.split(" ")[0].trim();
563 |             if (branch) {
564 |               remoteInfo.remote_branches.push(branch);
565 |             }
566 |           }
567 |         }
568 | 
569 |         return {
570 |           content: [
571 |             {
572 |               type: "text",
573 |               text: JSON.stringify(
574 |                 {
575 |                   success: true,
576 |                   remote: remoteInfo,
577 |                   raw_output: showOutput,
578 |                 },
579 |                 null,
580 |                 2
581 |               ),
582 |             },
583 |           ],
584 |         };
585 | 
586 |       default:
587 |         return {
588 |           content: [
589 |             {
590 |               type: "text",
591 |               text: JSON.stringify(
592 |                 { error: `Unknown remote action: ${action}` },
593 |                 null,
594 |                 2
595 |               ),
596 |             },
597 |           ],
598 |           isError: true,
599 |         };
600 |     }
601 |   } catch (error) {
602 |     return {
603 |       content: [
604 |         {
605 |           type: "text",
606 |           text: JSON.stringify(
607 |             { error: `Failed to manage remote: ${error.message}` },
608 |             null,
609 |             2
610 |           ),
611 |         },
612 |       ],
613 |       isError: true,
614 |     };
615 |   }
616 | }
617 | 
```

--------------------------------------------------------------------------------
/src/handlers/other-operations.js:
--------------------------------------------------------------------------------

```javascript
   1 | import { path, fs, simpleGit, execPromise } from "./common.js";
   2 | 
   3 | /**
   4 |  * Manages Git hooks in the repository
   5 |  * @param {string} repoPath - Path to the local repository
   6 |  * @param {string} action - Hook action (list, get, create, enable, disable)
   7 |  * @param {string} hookName - Name of the hook (e.g., "pre-commit", "post-merge")
   8 |  * @param {string} script - Script content for the hook (for create action)
   9 |  * @returns {Object} - Hook operation result
  10 |  */
  11 | export async function handleGitHooks({
  12 |   repo_path,
  13 |   action,
  14 |   hook_name = "",
  15 |   script = "",
  16 | }) {
  17 |   try {
  18 |     // Path to the hooks directory
  19 |     const hooksDir = path.join(repo_path, ".git", "hooks");
  20 | 
  21 |     switch (action) {
  22 |       case "list":
  23 |         // Get all available hooks
  24 |         const files = await fs.readdir(hooksDir);
  25 |         const hooks = [];
  26 | 
  27 |         for (const file of files) {
  28 |           // Filter out sample hooks
  29 |           if (!file.endsWith(".sample")) {
  30 |             const hookPath = path.join(hooksDir, file);
  31 |             const stats = await fs.stat(hookPath);
  32 | 
  33 |             hooks.push({
  34 |               name: file,
  35 |               path: hookPath,
  36 |               size: stats.size,
  37 |               executable: (stats.mode & 0o111) !== 0, // Check if executable
  38 |             });
  39 |           }
  40 |         }
  41 | 
  42 |         return {
  43 |           content: [
  44 |             {
  45 |               type: "text",
  46 |               text: JSON.stringify(
  47 |                 {
  48 |                   success: true,
  49 |                   hooks: hooks,
  50 |                 },
  51 |                 null,
  52 |                 2
  53 |               ),
  54 |             },
  55 |           ],
  56 |         };
  57 | 
  58 |       case "get":
  59 |         if (!hook_name) {
  60 |           return {
  61 |             content: [
  62 |               {
  63 |                 type: "text",
  64 |                 text: JSON.stringify(
  65 |                   { error: "Hook name is required for get action" },
  66 |                   null,
  67 |                   2
  68 |                 ),
  69 |               },
  70 |             ],
  71 |             isError: true,
  72 |           };
  73 |         }
  74 | 
  75 |         const hookPath = path.join(hooksDir, hook_name);
  76 | 
  77 |         // Check if hook exists
  78 |         if (!(await fs.pathExists(hookPath))) {
  79 |           return {
  80 |             content: [
  81 |               {
  82 |                 type: "text",
  83 |                 text: JSON.stringify(
  84 |                   { error: `Hook '${hook_name}' does not exist` },
  85 |                   null,
  86 |                   2
  87 |                 ),
  88 |               },
  89 |             ],
  90 |             isError: true,
  91 |           };
  92 |         }
  93 | 
  94 |         // Read hook content
  95 |         const hookContent = await fs.readFile(hookPath, "utf8");
  96 |         const stats = await fs.stat(hookPath);
  97 | 
  98 |         return {
  99 |           content: [
 100 |             {
 101 |               type: "text",
 102 |               text: JSON.stringify(
 103 |                 {
 104 |                   success: true,
 105 |                   name: hook_name,
 106 |                   content: hookContent,
 107 |                   executable: (stats.mode & 0o111) !== 0,
 108 |                 },
 109 |                 null,
 110 |                 2
 111 |               ),
 112 |             },
 113 |           ],
 114 |         };
 115 | 
 116 |       case "create":
 117 |         if (!hook_name) {
 118 |           return {
 119 |             content: [
 120 |               {
 121 |                 type: "text",
 122 |                 text: JSON.stringify(
 123 |                   { error: "Hook name is required for create action" },
 124 |                   null,
 125 |                   2
 126 |                 ),
 127 |               },
 128 |             ],
 129 |             isError: true,
 130 |           };
 131 |         }
 132 | 
 133 |         if (!script) {
 134 |           return {
 135 |             content: [
 136 |               {
 137 |                 type: "text",
 138 |                 text: JSON.stringify(
 139 |                   { error: "Script content is required for create action" },
 140 |                   null,
 141 |                   2
 142 |                 ),
 143 |               },
 144 |             ],
 145 |             isError: true,
 146 |           };
 147 |         }
 148 | 
 149 |         const createHookPath = path.join(hooksDir, hook_name);
 150 | 
 151 |         // Write hook content
 152 |         await fs.writeFile(createHookPath, script);
 153 | 
 154 |         // Make hook executable
 155 |         await fs.chmod(createHookPath, 0o755);
 156 | 
 157 |         return {
 158 |           content: [
 159 |             {
 160 |               type: "text",
 161 |               text: JSON.stringify(
 162 |                 {
 163 |                   success: true,
 164 |                   message: `Created hook '${hook_name}'`,
 165 |                   name: hook_name,
 166 |                   executable: true,
 167 |                 },
 168 |                 null,
 169 |                 2
 170 |               ),
 171 |             },
 172 |           ],
 173 |         };
 174 | 
 175 |       default:
 176 |         return {
 177 |           content: [
 178 |             {
 179 |               type: "text",
 180 |               text: JSON.stringify(
 181 |                 { error: `Unknown hook action: ${action}` },
 182 |                 null,
 183 |                 2
 184 |               ),
 185 |             },
 186 |           ],
 187 |           isError: true,
 188 |         };
 189 |     }
 190 |   } catch (error) {
 191 |     return {
 192 |       content: [
 193 |         {
 194 |           type: "text",
 195 |           text: JSON.stringify(
 196 |             { error: `Failed to manage hook: ${error.message}` },
 197 |             null,
 198 |             2
 199 |           ),
 200 |         },
 201 |       ],
 202 |       isError: true,
 203 |     };
 204 |   }
 205 | }
 206 | 
 207 | /**
 208 |  * Reverts a commit
 209 |  * @param {string} repoPath - Path to the local repository
 210 |  * @param {string} commit - Commit hash or reference to revert
 211 |  * @param {boolean} noCommit - Whether to stage changes without committing
 212 |  * @returns {Object} - Revert result
 213 |  */
 214 | export async function handleGitRevert({
 215 |   repo_path,
 216 |   commit,
 217 |   no_commit = false,
 218 | }) {
 219 |   try {
 220 |     const git = simpleGit(repo_path);
 221 | 
 222 |     if (!commit) {
 223 |       return {
 224 |         content: [
 225 |           {
 226 |             type: "text",
 227 |             text: JSON.stringify(
 228 |               { error: "Commit reference is required" },
 229 |               null,
 230 |               2
 231 |             ),
 232 |           },
 233 |         ],
 234 |         isError: true,
 235 |       };
 236 |     }
 237 | 
 238 |     // Build the revert command
 239 |     const revertOptions = [];
 240 |     if (no_commit) {
 241 |       revertOptions.push("--no-commit");
 242 |     }
 243 | 
 244 |     // Perform the revert
 245 |     const result = await git.raw(["revert", ...revertOptions, commit]);
 246 | 
 247 |     return {
 248 |       content: [
 249 |         {
 250 |           type: "text",
 251 |           text: JSON.stringify(
 252 |             {
 253 |               success: true,
 254 |               message: `Reverted commit ${commit}`,
 255 |               commit: commit,
 256 |               result: result,
 257 |             },
 258 |             null,
 259 |             2
 260 |           ),
 261 |         },
 262 |       ],
 263 |     };
 264 |   } catch (error) {
 265 |     return {
 266 |       content: [
 267 |         {
 268 |           type: "text",
 269 |           text: JSON.stringify(
 270 |             {
 271 |               error: `Failed to revert commit: ${error.message}`,
 272 |               conflicts: error.git ? error.git.conflicts : null,
 273 |             },
 274 |             null,
 275 |             2
 276 |           ),
 277 |         },
 278 |       ],
 279 |       isError: true,
 280 |     };
 281 |   }
 282 | }
 283 | 
 284 | /**
 285 |  * Performs Git clean operations
 286 |  * @param {string} repoPath - Path to the local repository
 287 |  * @param {boolean} directories - Whether to remove directories as well
 288 |  * @param {boolean} force - Whether to force clean
 289 |  * @param {boolean} dryRun - Whether to perform a dry run (show what would be done)
 290 |  * @returns {Object} - Clean result
 291 |  */
 292 | export async function handleGitClean({
 293 |   repo_path,
 294 |   directories = false,
 295 |   force = false,
 296 |   dry_run = true,
 297 | }) {
 298 |   try {
 299 |     const git = simpleGit(repo_path);
 300 | 
 301 |     // At least one of force or dry_run must be true for safety
 302 |     if (!force && !dry_run) {
 303 |       return {
 304 |         content: [
 305 |           {
 306 |             type: "text",
 307 |             text: JSON.stringify(
 308 |               { error: "For safety, either force or dry_run must be true" },
 309 |               null,
 310 |               2
 311 |             ),
 312 |           },
 313 |         ],
 314 |         isError: true,
 315 |       };
 316 |     }
 317 | 
 318 |     // Build the clean command
 319 |     const cleanOptions = [];
 320 | 
 321 |     if (directories) {
 322 |       cleanOptions.push("-d");
 323 |     }
 324 | 
 325 |     if (force) {
 326 |       cleanOptions.push("-f");
 327 |     }
 328 | 
 329 |     if (dry_run) {
 330 |       cleanOptions.push("-n");
 331 |     }
 332 | 
 333 |     // Get the files that would be removed
 334 |     const preview = await git.clean([
 335 |       "--dry-run",
 336 |       ...(directories ? ["-d"] : []),
 337 |     ]);
 338 |     const filesToRemove = preview
 339 |       .split("\n")
 340 |       .filter((line) => line.startsWith("Would remove"))
 341 |       .map((line) => line.replace("Would remove ", "").trim());
 342 | 
 343 |     if (!dry_run) {
 344 |       // Perform the actual clean
 345 |       await git.clean(cleanOptions);
 346 |     }
 347 | 
 348 |     return {
 349 |       content: [
 350 |         {
 351 |           type: "text",
 352 |           text: JSON.stringify(
 353 |             {
 354 |               success: true,
 355 |               message: dry_run
 356 |                 ? `Would remove ${filesToRemove.length} files/directories`
 357 |                 : `Removed ${filesToRemove.length} files/directories`,
 358 |               files: filesToRemove,
 359 |               dry_run: dry_run,
 360 |             },
 361 |             null,
 362 |             2
 363 |           ),
 364 |         },
 365 |       ],
 366 |     };
 367 |   } catch (error) {
 368 |     return {
 369 |       content: [
 370 |         {
 371 |           type: "text",
 372 |           text: JSON.stringify(
 373 |             { error: `Failed to clean repository: ${error.message}` },
 374 |             null,
 375 |             2
 376 |           ),
 377 |         },
 378 |       ],
 379 |       isError: true,
 380 |     };
 381 |   }
 382 | }
 383 | 
 384 | /**
 385 |  * Updates Git LFS objects
 386 |  * @param {string} repoPath - Path to the local repository
 387 |  * @param {boolean} dryRun - Whether to perform a dry run
 388 |  * @param {boolean} pointers - Whether to convert pointers to objects
 389 |  * @returns {Object} - LFS objects update result
 390 |  */
 391 | export async function handleGitLFSFetch({
 392 |   repo_path,
 393 |   dry_run = false,
 394 |   pointers = false,
 395 | }) {
 396 |   try {
 397 |     // Build the command
 398 |     let command = `cd "${repo_path}" && git lfs fetch`;
 399 | 
 400 |     if (dry_run) {
 401 |       command += " --dry-run";
 402 |     }
 403 | 
 404 |     if (pointers) {
 405 |       command += " --pointers";
 406 |     }
 407 | 
 408 |     // Execute the command
 409 |     const { stdout, stderr } = await execPromise(command);
 410 | 
 411 |     // Parse the output
 412 |     const output = stdout.trim();
 413 |     const errors = stderr.trim();
 414 | 
 415 |     if (errors && !output) {
 416 |       return {
 417 |         content: [
 418 |           {
 419 |             type: "text",
 420 |             text: JSON.stringify({ error: errors }, null, 2),
 421 |           },
 422 |         ],
 423 |         isError: true,
 424 |       };
 425 |     }
 426 | 
 427 |     return {
 428 |       content: [
 429 |         {
 430 |           type: "text",
 431 |           text: JSON.stringify(
 432 |             {
 433 |               success: true,
 434 |               message: "Git LFS fetch completed",
 435 |               output: output,
 436 |               dry_run: dry_run,
 437 |             },
 438 |             null,
 439 |             2
 440 |           ),
 441 |         },
 442 |       ],
 443 |     };
 444 |   } catch (error) {
 445 |     // Special handling for "git lfs not installed" error
 446 |     if (error.message.includes("git: lfs is not a git command")) {
 447 |       return {
 448 |         content: [
 449 |           {
 450 |             type: "text",
 451 |             text: JSON.stringify(
 452 |               { error: "Git LFS is not installed on the system" },
 453 |               null,
 454 |               2
 455 |             ),
 456 |           },
 457 |         ],
 458 |         isError: true,
 459 |       };
 460 |     }
 461 | 
 462 |     return {
 463 |       content: [
 464 |         {
 465 |           type: "text",
 466 |           text: JSON.stringify(
 467 |             { error: `Failed to fetch LFS objects: ${error.message}` },
 468 |             null,
 469 |             2
 470 |           ),
 471 |         },
 472 |       ],
 473 |       isError: true,
 474 |     };
 475 |   }
 476 | }
 477 | 
 478 | /**
 479 |  * Gets blame information for a file
 480 |  * @param {string} repoPath - Path to the local repository
 481 |  * @param {string} filePath - Path to the file
 482 |  * @param {string} rev - Revision to blame (default: HEAD)
 483 |  * @returns {Object} - Blame result
 484 |  */
 485 | export async function handleGitBlame({ repo_path, file_path, rev = "HEAD" }) {
 486 |   try {
 487 |     const git = simpleGit(repo_path);
 488 | 
 489 |     // Run git blame
 490 |     const blameResult = await git.raw([
 491 |       "blame",
 492 |       "--line-porcelain",
 493 |       rev,
 494 |       "--",
 495 |       file_path,
 496 |     ]);
 497 | 
 498 |     // Parse the output
 499 |     const lines = blameResult.split("\n");
 500 |     const blameInfo = [];
 501 | 
 502 |     let currentCommit = null;
 503 | 
 504 |     for (let i = 0; i < lines.length; i++) {
 505 |       const line = lines[i];
 506 | 
 507 |       // Start of a new blame entry
 508 |       if (line.match(/^[0-9a-f]{40}/)) {
 509 |         if (currentCommit) {
 510 |           blameInfo.push(currentCommit);
 511 |         }
 512 | 
 513 |         const parts = line.split(" ");
 514 |         currentCommit = {
 515 |           hash: parts[0],
 516 |           originalLine: parseInt(parts[1]),
 517 |           finalLine: parseInt(parts[2]),
 518 |           lineCount: parseInt(parts[3] || 1),
 519 |           author: "",
 520 |           authorMail: "",
 521 |           authorTime: 0,
 522 |           subject: "",
 523 |           content: "",
 524 |         };
 525 |       } else if (line.startsWith("author ") && currentCommit) {
 526 |         currentCommit.author = line.substring(7);
 527 |       } else if (line.startsWith("author-mail ") && currentCommit) {
 528 |         currentCommit.authorMail = line.substring(12).replace(/[<>]/g, "");
 529 |       } else if (line.startsWith("author-time ") && currentCommit) {
 530 |         currentCommit.authorTime = parseInt(line.substring(12));
 531 |       } else if (line.startsWith("summary ") && currentCommit) {
 532 |         currentCommit.subject = line.substring(8);
 533 |       } else if (line.startsWith("\t") && currentCommit) {
 534 |         // This is the content line
 535 |         currentCommit.content = line.substring(1);
 536 |         blameInfo.push(currentCommit);
 537 |         currentCommit = null;
 538 |       }
 539 |     }
 540 | 
 541 |     // Add the last commit if there is one
 542 |     if (currentCommit) {
 543 |       blameInfo.push(currentCommit);
 544 |     }
 545 | 
 546 |     return {
 547 |       content: [
 548 |         {
 549 |           type: "text",
 550 |           text: JSON.stringify(
 551 |             {
 552 |               success: true,
 553 |               file: file_path,
 554 |               blame: blameInfo,
 555 |             },
 556 |             null,
 557 |             2
 558 |           ),
 559 |         },
 560 |       ],
 561 |     };
 562 |   } catch (error) {
 563 |     return {
 564 |       content: [
 565 |         {
 566 |           type: "text",
 567 |           text: JSON.stringify(
 568 |             { error: `Failed to get blame information: ${error.message}` },
 569 |             null,
 570 |             2
 571 |           ),
 572 |         },
 573 |       ],
 574 |       isError: true,
 575 |     };
 576 |   }
 577 | }
 578 | 
 579 | /**
 580 |  * Manages git attributes for files
 581 |  * @param {string} repoPath - Path to the local repository
 582 |  * @param {string} action - Action (get, set, list)
 583 |  * @param {string} pattern - File pattern
 584 |  * @param {string} attribute - Attribute to set
 585 |  * @returns {Object} - Operation result
 586 |  */
 587 | export async function handleGitAttributes({
 588 |   repo_path,
 589 |   action,
 590 |   pattern = "",
 591 |   attribute = "",
 592 | }) {
 593 |   try {
 594 |     const attributesPath = path.join(repo_path, ".gitattributes");
 595 | 
 596 |     switch (action) {
 597 |       case "list":
 598 |         // Check if .gitattributes exists
 599 |         if (!(await fs.pathExists(attributesPath))) {
 600 |           return {
 601 |             content: [
 602 |               {
 603 |                 type: "text",
 604 |                 text: JSON.stringify(
 605 |                   {
 606 |                     success: true,
 607 |                     attributes: [],
 608 |                     message: ".gitattributes file does not exist",
 609 |                   },
 610 |                   null,
 611 |                   2
 612 |                 ),
 613 |               },
 614 |             ],
 615 |           };
 616 |         }
 617 | 
 618 |         // Read and parse .gitattributes
 619 |         const content = await fs.readFile(attributesPath, "utf8");
 620 |         const lines = content
 621 |           .split("\n")
 622 |           .filter((line) => line.trim() && !line.startsWith("#"));
 623 | 
 624 |         const attributes = lines.map((line) => {
 625 |           const parts = line.trim().split(/\s+/);
 626 |           return {
 627 |             pattern: parts[0],
 628 |             attributes: parts.slice(1),
 629 |           };
 630 |         });
 631 | 
 632 |         return {
 633 |           content: [
 634 |             {
 635 |               type: "text",
 636 |               text: JSON.stringify(
 637 |                 {
 638 |                   success: true,
 639 |                   attributes: attributes,
 640 |                 },
 641 |                 null,
 642 |                 2
 643 |               ),
 644 |             },
 645 |           ],
 646 |         };
 647 | 
 648 |       case "get":
 649 |         if (!pattern) {
 650 |           return {
 651 |             content: [
 652 |               {
 653 |                 type: "text",
 654 |                 text: JSON.stringify(
 655 |                   { error: "Pattern is required for get action" },
 656 |                   null,
 657 |                   2
 658 |                 ),
 659 |               },
 660 |             ],
 661 |             isError: true,
 662 |           };
 663 |         }
 664 | 
 665 |         // Check if .gitattributes exists
 666 |         if (!(await fs.pathExists(attributesPath))) {
 667 |           return {
 668 |             content: [
 669 |               {
 670 |                 type: "text",
 671 |                 text: JSON.stringify(
 672 |                   {
 673 |                     success: true,
 674 |                     pattern: pattern,
 675 |                     attributes: [],
 676 |                     message: ".gitattributes file does not exist",
 677 |                   },
 678 |                   null,
 679 |                   2
 680 |                 ),
 681 |               },
 682 |             ],
 683 |           };
 684 |         }
 685 | 
 686 |         // Read and find pattern
 687 |         const getContent = await fs.readFile(attributesPath, "utf8");
 688 |         const getLines = getContent.split("\n");
 689 | 
 690 |         const matchingLines = getLines.filter((line) => {
 691 |           const parts = line.trim().split(/\s+/);
 692 |           return parts[0] === pattern;
 693 |         });
 694 | 
 695 |         if (matchingLines.length === 0) {
 696 |           return {
 697 |             content: [
 698 |               {
 699 |                 type: "text",
 700 |                 text: JSON.stringify(
 701 |                   {
 702 |                     success: true,
 703 |                     pattern: pattern,
 704 |                     attributes: [],
 705 |                     message: `No attributes found for pattern '${pattern}'`,
 706 |                   },
 707 |                   null,
 708 |                   2
 709 |                 ),
 710 |               },
 711 |             ],
 712 |           };
 713 |         }
 714 | 
 715 |         // Parse attributes
 716 |         const patternAttributes = matchingLines
 717 |           .map((line) => {
 718 |             const parts = line.trim().split(/\s+/);
 719 |             return parts.slice(1);
 720 |           })
 721 |           .flat();
 722 | 
 723 |         return {
 724 |           content: [
 725 |             {
 726 |               type: "text",
 727 |               text: JSON.stringify(
 728 |                 {
 729 |                   success: true,
 730 |                   pattern: pattern,
 731 |                   attributes: patternAttributes,
 732 |                 },
 733 |                 null,
 734 |                 2
 735 |               ),
 736 |             },
 737 |           ],
 738 |         };
 739 | 
 740 |       case "set":
 741 |         if (!pattern) {
 742 |           return {
 743 |             content: [
 744 |               {
 745 |                 type: "text",
 746 |                 text: JSON.stringify(
 747 |                   { error: "Pattern is required for set action" },
 748 |                   null,
 749 |                   2
 750 |                 ),
 751 |               },
 752 |             ],
 753 |             isError: true,
 754 |           };
 755 |         }
 756 | 
 757 |         if (!attribute) {
 758 |           return {
 759 |             content: [
 760 |               {
 761 |                 type: "text",
 762 |                 text: JSON.stringify(
 763 |                   { error: "Attribute is required for set action" },
 764 |                   null,
 765 |                   2
 766 |                 ),
 767 |               },
 768 |             ],
 769 |             isError: true,
 770 |           };
 771 |         }
 772 | 
 773 |         // Check if .gitattributes exists, create if not
 774 |         if (!(await fs.pathExists(attributesPath))) {
 775 |           await fs.writeFile(attributesPath, "");
 776 |         }
 777 | 
 778 |         // Read current content
 779 |         const setContent = await fs.readFile(attributesPath, "utf8");
 780 |         const setLines = setContent.split("\n");
 781 | 
 782 |         // Check if pattern already exists
 783 |         const patternIndex = setLines.findIndex((line) => {
 784 |           const parts = line.trim().split(/\s+/);
 785 |           return parts[0] === pattern;
 786 |         });
 787 | 
 788 |         if (patternIndex !== -1) {
 789 |           // Update existing pattern
 790 |           const parts = setLines[patternIndex].trim().split(/\s+/);
 791 | 
 792 |           // Check if attribute already exists
 793 |           if (!parts.includes(attribute)) {
 794 |             parts.push(attribute);
 795 |             setLines[patternIndex] = parts.join(" ");
 796 |           }
 797 |         } else {
 798 |           // Add new pattern
 799 |           setLines.push(`${pattern} ${attribute}`);
 800 |         }
 801 | 
 802 |         // Write back to file
 803 |         await fs.writeFile(
 804 |           attributesPath,
 805 |           setLines.filter(Boolean).join("\n") + "\n"
 806 |         );
 807 | 
 808 |         return {
 809 |           content: [
 810 |             {
 811 |               type: "text",
 812 |               text: JSON.stringify(
 813 |                 {
 814 |                   success: true,
 815 |                   message: `Set attribute '${attribute}' for pattern '${pattern}'`,
 816 |                   pattern: pattern,
 817 |                   attribute: attribute,
 818 |                 },
 819 |                 null,
 820 |                 2
 821 |               ),
 822 |             },
 823 |           ],
 824 |         };
 825 | 
 826 |       default:
 827 |         return {
 828 |           content: [
 829 |             {
 830 |               type: "text",
 831 |               text: JSON.stringify(
 832 |                 { error: `Unknown attributes action: ${action}` },
 833 |                 null,
 834 |                 2
 835 |               ),
 836 |             },
 837 |           ],
 838 |           isError: true,
 839 |         };
 840 |     }
 841 |   } catch (error) {
 842 |     return {
 843 |       content: [
 844 |         {
 845 |           type: "text",
 846 |           text: JSON.stringify(
 847 |             { error: `Failed to manage git attributes: ${error.message}` },
 848 |             null,
 849 |             2
 850 |           ),
 851 |         },
 852 |       ],
 853 |       isError: true,
 854 |     };
 855 |   }
 856 | }
 857 | 
 858 | /**
 859 |  * Creates a git archive (zip or tar)
 860 |  * @param {string} repoPath - Path to the local repository
 861 |  * @param {string} outputPath - Output path for the archive
 862 |  * @param {string} format - Archive format (zip or tar)
 863 |  * @param {string} prefix - Prefix for files in the archive
 864 |  * @param {string} treeish - Tree-ish to archive (default: HEAD)
 865 |  * @returns {Object} - Archive result
 866 |  */
 867 | export async function handleGitArchive({
 868 |   repo_path,
 869 |   output_path,
 870 |   format = "zip",
 871 |   prefix = "",
 872 |   treeish = "HEAD",
 873 | }) {
 874 |   try {
 875 |     const git = simpleGit(repo_path);
 876 | 
 877 |     // Validate format
 878 |     if (!["zip", "tar"].includes(format)) {
 879 |       return {
 880 |         content: [
 881 |           {
 882 |             type: "text",
 883 |             text: JSON.stringify(
 884 |               {
 885 |                 error: `Invalid archive format: ${format}. Use 'zip' or 'tar'.`,
 886 |               },
 887 |               null,
 888 |               2
 889 |             ),
 890 |           },
 891 |         ],
 892 |         isError: true,
 893 |       };
 894 |     }
 895 | 
 896 |     // Build archive command
 897 |     const archiveArgs = ["archive", `--format=${format}`];
 898 | 
 899 |     if (prefix) {
 900 |       archiveArgs.push(`--prefix=${prefix}/`);
 901 |     }
 902 | 
 903 |     archiveArgs.push("-o", output_path, treeish);
 904 | 
 905 |     // Create archive
 906 |     await git.raw(archiveArgs);
 907 | 
 908 |     // Check if archive was created
 909 |     if (!(await fs.pathExists(output_path))) {
 910 |       return {
 911 |         content: [
 912 |           {
 913 |             type: "text",
 914 |             text: JSON.stringify(
 915 |               { error: "Failed to create archive: output file not found" },
 916 |               null,
 917 |               2
 918 |             ),
 919 |           },
 920 |         ],
 921 |         isError: true,
 922 |       };
 923 |     }
 924 | 
 925 |     // Get file size
 926 |     const stats = await fs.stat(output_path);
 927 | 
 928 |     return {
 929 |       content: [
 930 |         {
 931 |           type: "text",
 932 |           text: JSON.stringify(
 933 |             {
 934 |               success: true,
 935 |               message: `Created ${format} archive at ${output_path}`,
 936 |               format: format,
 937 |               output_path: output_path,
 938 |               size_bytes: stats.size,
 939 |               treeish: treeish,
 940 |             },
 941 |             null,
 942 |             2
 943 |           ),
 944 |         },
 945 |       ],
 946 |     };
 947 |   } catch (error) {
 948 |     return {
 949 |       content: [
 950 |         {
 951 |           type: "text",
 952 |           text: JSON.stringify(
 953 |             { error: `Failed to create archive: ${error.message}` },
 954 |             null,
 955 |             2
 956 |           ),
 957 |         },
 958 |       ],
 959 |       isError: true,
 960 |     };
 961 |   }
 962 | }
 963 | 
 964 | /**
 965 |  * Manages Git LFS (Large File Storage)
 966 |  * @param {string} repoPath - Path to the local repository
 967 |  * @param {string} action - LFS action (install, track, untrack, list)
 968 |  * @param {string|string[]} patterns - File patterns for track/untrack
 969 |  * @returns {Object} - Operation result
 970 |  */
 971 | export async function handleGitLFS({ repo_path, action, patterns = [] }) {
 972 |   try {
 973 |     // Make sure patterns is an array
 974 |     const patternsArray = Array.isArray(patterns) ? patterns : [patterns];
 975 | 
 976 |     switch (action) {
 977 |       case "install":
 978 |         // Install Git LFS in the repository
 979 |         const { stdout: installOutput } = await execPromise(
 980 |           `cd "${repo_path}" && git lfs install`
 981 |         );
 982 | 
 983 |         return {
 984 |           content: [
 985 |             {
 986 |               type: "text",
 987 |               text: JSON.stringify(
 988 |                 {
 989 |                   success: true,
 990 |                   message: "Git LFS installed successfully",
 991 |                   output: installOutput.trim(),
 992 |                 },
 993 |                 null,
 994 |                 2
 995 |               ),
 996 |             },
 997 |           ],
 998 |         };
 999 | 
1000 |       case "track":
1001 |         if (patternsArray.length === 0) {
1002 |           return {
1003 |             content: [
1004 |               {
1005 |                 type: "text",
1006 |                 text: JSON.stringify(
1007 |                   {
1008 |                     error: "At least one pattern is required for track action",
1009 |                   },
1010 |                   null,
1011 |                   2
1012 |                 ),
1013 |               },
1014 |             ],
1015 |             isError: true,
1016 |           };
1017 |         }
1018 | 
1019 |         // Track files with LFS
1020 |         const trackResults = [];
1021 | 
1022 |         for (const pattern of patternsArray) {
1023 |           const { stdout: trackOutput } = await execPromise(
1024 |             `cd "${repo_path}" && git lfs track "${pattern}"`
1025 |           );
1026 |           trackResults.push({
1027 |             pattern: pattern,
1028 |             output: trackOutput.trim(),
1029 |           });
1030 |         }
1031 | 
1032 |         return {
1033 |           content: [
1034 |             {
1035 |               type: "text",
1036 |               text: JSON.stringify(
1037 |                 {
1038 |                   success: true,
1039 |                   message: `Tracked ${patternsArray.length} pattern(s) with Git LFS`,
1040 |                   patterns: patternsArray,
1041 |                   results: trackResults,
1042 |                 },
1043 |                 null,
1044 |                 2
1045 |               ),
1046 |             },
1047 |           ],
1048 |         };
1049 | 
1050 |       case "untrack":
1051 |         if (patternsArray.length === 0) {
1052 |           return {
1053 |             content: [
1054 |               {
1055 |                 type: "text",
1056 |                 text: JSON.stringify(
1057 |                   {
1058 |                     error:
1059 |                       "At least one pattern is required for untrack action",
1060 |                   },
1061 |                   null,
1062 |                   2
1063 |                 ),
1064 |               },
1065 |             ],
1066 |             isError: true,
1067 |           };
1068 |         }
1069 | 
1070 |         // Untrack files from LFS
1071 |         const untrackResults = [];
1072 | 
1073 |         for (const pattern of patternsArray) {
1074 |           const { stdout: untrackOutput } = await execPromise(
1075 |             `cd "${repo_path}" && git lfs untrack "${pattern}"`
1076 |           );
1077 |           untrackResults.push({
1078 |             pattern: pattern,
1079 |             output: untrackOutput.trim(),
1080 |           });
1081 |         }
1082 | 
1083 |         return {
1084 |           content: [
1085 |             {
1086 |               type: "text",
1087 |               text: JSON.stringify(
1088 |                 {
1089 |                   success: true,
1090 |                   message: `Untracked ${patternsArray.length} pattern(s) from Git LFS`,
1091 |                   patterns: patternsArray,
1092 |                   results: untrackResults,
1093 |                 },
1094 |                 null,
1095 |                 2
1096 |               ),
1097 |             },
1098 |           ],
1099 |         };
1100 | 
1101 |       case "list":
1102 |         // List tracked patterns
1103 |         const { stdout: listOutput } = await execPromise(
1104 |           `cd "${repo_path}" && git lfs track`
1105 |         );
1106 | 
1107 |         // Parse the output to extract patterns
1108 |         const trackedPatterns = listOutput
1109 |           .split("\n")
1110 |           .filter((line) => line.includes("("))
1111 |           .map((line) => {
1112 |             const match = line.match(/Tracking "([^"]+)"/);
1113 |             return match ? match[1] : null;
1114 |           })
1115 |           .filter(Boolean);
1116 | 
1117 |         return {
1118 |           content: [
1119 |             {
1120 |               type: "text",
1121 |               text: JSON.stringify(
1122 |                 {
1123 |                   success: true,
1124 |                   tracked_patterns: trackedPatterns,
1125 |                 },
1126 |                 null,
1127 |                 2
1128 |               ),
1129 |             },
1130 |           ],
1131 |         };
1132 | 
1133 |       default:
1134 |         return {
1135 |           content: [
1136 |             {
1137 |               type: "text",
1138 |               text: JSON.stringify(
1139 |                 { error: `Unknown LFS action: ${action}` },
1140 |                 null,
1141 |                 2
1142 |               ),
1143 |             },
1144 |           ],
1145 |           isError: true,
1146 |         };
1147 |     }
1148 |   } catch (error) {
1149 |     // Special handling for "git lfs not installed" error
1150 |     if (error.message.includes("git: lfs is not a git command")) {
1151 |       return {
1152 |         content: [
1153 |           {
1154 |             type: "text",
1155 |             text: JSON.stringify(
1156 |               { error: "Git LFS is not installed on the system" },
1157 |               null,
1158 |               2
1159 |             ),
1160 |           },
1161 |         ],
1162 |         isError: true,
1163 |       };
1164 |     }
1165 | 
1166 |     return {
1167 |       content: [
1168 |         {
1169 |           type: "text",
1170 |           text: JSON.stringify(
1171 |             { error: `Failed to perform LFS operation: ${error.message}` },
1172 |             null,
1173 |             2
1174 |           ),
1175 |         },
1176 |       ],
1177 |       isError: true,
1178 |     };
1179 |   }
1180 | }
1181 | 
```

--------------------------------------------------------------------------------
/src/server.js:
--------------------------------------------------------------------------------

```javascript
   1 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
   2 | import {
   3 |   CallToolRequestSchema,
   4 |   ErrorCode,
   5 |   ListToolsRequestSchema,
   6 |   McpError,
   7 | } from "@modelcontextprotocol/sdk/types.js";
   8 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
   9 | 
  10 | import {
  11 |   handleGitDirectoryStructure,
  12 |   handleGitReadFiles,
  13 |   handleGitBranchDiff,
  14 |   handleGitCommitHistory,
  15 |   handleGitCommitsDetails,
  16 |   handleGitLocalChanges,
  17 |   handleGitSearchCode,
  18 |   handleGitCommit,
  19 |   handleGitTrack,
  20 |   handleGitCheckoutBranch,
  21 |   handleGitDeleteBranch,
  22 |   handleGitMergeBranch,
  23 |   handleGitPush,
  24 |   handleGitPull,
  25 |   handleGitStash,
  26 |   handleGitCreateTag,
  27 |   handleGitRebase,
  28 |   handleGitConfig,
  29 |   handleGitReset,
  30 |   handleGitArchive,
  31 |   handleGitAttributes,
  32 |   handleGitBlame,
  33 |   handleGitClean,
  34 |   handleGitHooks,
  35 |   handleGitLFS,
  36 |   handleGitLFSFetch,
  37 |   handleGitRevert,
  38 | } from "./handlers/index.js";
  39 | 
  40 | /**
  41 |  * Main server class for the Git Repository Browser MCP server
  42 |  */
  43 | export class GitRepoBrowserServer {
  44 |   /**
  45 |    * Initialize the server
  46 |    */
  47 |   constructor() {
  48 |     this.server = new Server(
  49 |       {
  50 |         name: "mcp-git-repo-browser",
  51 |         version: "0.1.0",
  52 |       },
  53 |       {
  54 |         capabilities: {
  55 |           tools: {},
  56 |         },
  57 |       }
  58 |     );
  59 | 
  60 |     this.setupToolHandlers();
  61 | 
  62 |     // Error handling
  63 |     this.server.onerror = (error) => console.error("[MCP Error]", error);
  64 |     process.on("SIGINT", async () => {
  65 |       await this.server.close();
  66 |       process.exit(0);
  67 |     });
  68 |   }
  69 | 
  70 |   /**
  71 |    * Get all registered handler names
  72 |    * @returns {string[]} Array of handler names
  73 |    */
  74 |   getHandlerNames() {
  75 |     return Object.keys(this.handlersMap || {});
  76 |   }
  77 | 
  78 |   /**
  79 |    * Check if a handler exists
  80 |    * @param {string} name - Handler name to check
  81 |    * @returns {boolean} True if handler exists
  82 |    */
  83 |   hasHandler(name) {
  84 |     return Boolean(this.handlersMap && this.handlersMap[name]);
  85 |   }
  86 | 
  87 |   /**
  88 |    * Set up tool handlers for the server
  89 |    */
  90 |   setupToolHandlers() {
  91 |     // Store tools list for dynamic updates
  92 |     this.toolsList = [
  93 |       // Basic Repository Operations
  94 |       {
  95 |         name: "git_directory_structure",
  96 |         description:
  97 |           "Clone a Git repository and return its directory structure in a tree format.",
  98 |         inputSchema: {
  99 |           type: "object",
 100 |           properties: {
 101 |             repo_url: {
 102 |               type: "string",
 103 |               description: "The URL of the Git repository",
 104 |             },
 105 |           },
 106 |           required: ["repo_url"],
 107 |         },
 108 |       },
 109 |       {
 110 |         name: "git_read_files",
 111 |         description:
 112 |           "Read the contents of specified files in a given git repository.",
 113 |         inputSchema: {
 114 |           type: "object",
 115 |           properties: {
 116 |             repo_url: {
 117 |               type: "string",
 118 |               description: "The URL of the Git repository",
 119 |             },
 120 |             file_paths: {
 121 |               type: "array",
 122 |               items: { type: "string" },
 123 |               description:
 124 |                 "List of file paths to read (relative to repository root)",
 125 |             },
 126 |           },
 127 |           required: ["repo_url", "file_paths"],
 128 |         },
 129 |       },
 130 | 
 131 |       // Branch Operations
 132 |       {
 133 |         name: "git_branch_diff",
 134 |         description:
 135 |           "Compare two branches and show files changed between them.",
 136 |         inputSchema: {
 137 |           type: "object",
 138 |           properties: {
 139 |             repo_url: {
 140 |               type: "string",
 141 |               description: "The URL of the Git repository",
 142 |             },
 143 |             source_branch: {
 144 |               type: "string",
 145 |               description: "The source branch name",
 146 |             },
 147 |             target_branch: {
 148 |               type: "string",
 149 |               description: "The target branch name",
 150 |             },
 151 |             show_patch: {
 152 |               type: "boolean",
 153 |               description: "Whether to include the actual diff patches",
 154 |               default: false,
 155 |             },
 156 |           },
 157 |           required: ["repo_url", "source_branch", "target_branch"],
 158 |         },
 159 |       },
 160 |       {
 161 |         name: "git_checkout_branch",
 162 |         description: "Create and/or checkout a branch.",
 163 |         inputSchema: {
 164 |           type: "object",
 165 |           properties: {
 166 |             repo_path: {
 167 |               type: "string",
 168 |               description: "The path to the local Git repository",
 169 |             },
 170 |             branch_name: {
 171 |               type: "string",
 172 |               description: "The name of the branch to checkout",
 173 |             },
 174 |             start_point: {
 175 |               type: "string",
 176 |               description: "Starting point for the branch (optional)",
 177 |             },
 178 |             create: {
 179 |               type: "boolean",
 180 |               description: "Whether to create a new branch",
 181 |               default: false,
 182 |             },
 183 |           },
 184 |           required: ["repo_path", "branch_name"],
 185 |         },
 186 |       },
 187 |       {
 188 |         name: "git_delete_branch",
 189 |         description: "Delete a branch from the repository.",
 190 |         inputSchema: {
 191 |           type: "object",
 192 |           properties: {
 193 |             repo_path: {
 194 |               type: "string",
 195 |               description: "The path to the local Git repository",
 196 |             },
 197 |             branch_name: {
 198 |               type: "string",
 199 |               description: "The name of the branch to delete",
 200 |             },
 201 |             force: {
 202 |               type: "boolean",
 203 |               description: "Whether to force deletion",
 204 |               default: false,
 205 |             },
 206 |           },
 207 |           required: ["repo_path", "branch_name"],
 208 |         },
 209 |       },
 210 |       {
 211 |         name: "git_merge_branch",
 212 |         description: "Merge a source branch into the current or target branch.",
 213 |         inputSchema: {
 214 |           type: "object",
 215 |           properties: {
 216 |             repo_path: {
 217 |               type: "string",
 218 |               description: "The path to the local Git repository",
 219 |             },
 220 |             source_branch: {
 221 |               type: "string",
 222 |               description: "Branch to merge from",
 223 |             },
 224 |             target_branch: {
 225 |               type: "string",
 226 |               description:
 227 |                 "Branch to merge into (optional, uses current branch if not provided)",
 228 |             },
 229 |             no_fast_forward: {
 230 |               type: "boolean",
 231 |               description:
 232 |                 "Whether to create a merge commit even if fast-forward is possible",
 233 |               default: false,
 234 |             },
 235 |           },
 236 |           required: ["repo_path", "source_branch"],
 237 |         },
 238 |       },
 239 | 
 240 |       // Commit Operations
 241 |       {
 242 |         name: "git_commit_history",
 243 |         description: "Get commit history for a branch with optional filtering.",
 244 |         inputSchema: {
 245 |           type: "object",
 246 |           properties: {
 247 |             repo_url: {
 248 |               type: "string",
 249 |               description: "The URL of the Git repository",
 250 |             },
 251 |             branch: {
 252 |               type: "string",
 253 |               description: "The branch to get history from",
 254 |               default: "main",
 255 |             },
 256 |             max_count: {
 257 |               type: "integer",
 258 |               description: "Maximum number of commits to retrieve",
 259 |               default: 10,
 260 |             },
 261 |             author: {
 262 |               type: "string",
 263 |               description: "Filter by author (optional)",
 264 |             },
 265 |             since: {
 266 |               type: "string",
 267 |               description:
 268 |                 'Get commits after this date (e.g., "1 week ago", "2023-01-01")',
 269 |             },
 270 |             until: {
 271 |               type: "string",
 272 |               description:
 273 |                 'Get commits before this date (e.g., "yesterday", "2023-12-31")',
 274 |             },
 275 |             grep: {
 276 |               type: "string",
 277 |               description: "Filter commits by message content (optional)",
 278 |             },
 279 |           },
 280 |           required: ["repo_url"],
 281 |         },
 282 |       },
 283 |       {
 284 |         name: "git_commits_details",
 285 |         description:
 286 |           "Get detailed information about commits including full messages and diffs.",
 287 |         inputSchema: {
 288 |           type: "object",
 289 |           properties: {
 290 |             repo_url: {
 291 |               type: "string",
 292 |               description: "The URL of the Git repository",
 293 |             },
 294 |             branch: {
 295 |               type: "string",
 296 |               description: "The branch to get commits from",
 297 |               default: "main",
 298 |             },
 299 |             max_count: {
 300 |               type: "integer",
 301 |               description: "Maximum number of commits to retrieve",
 302 |               default: 10,
 303 |             },
 304 |             include_diff: {
 305 |               type: "boolean",
 306 |               description: "Whether to include the commit diffs",
 307 |               default: false,
 308 |             },
 309 |             since: {
 310 |               type: "string",
 311 |               description:
 312 |                 'Get commits after this date (e.g., "1 week ago", "2023-01-01")',
 313 |             },
 314 |             until: {
 315 |               type: "string",
 316 |               description:
 317 |                 'Get commits before this date (e.g., "yesterday", "2023-12-31")',
 318 |             },
 319 |             author: {
 320 |               type: "string",
 321 |               description: "Filter by author (optional)",
 322 |             },
 323 |             grep: {
 324 |               type: "string",
 325 |               description: "Filter commits by message content (optional)",
 326 |             },
 327 |           },
 328 |           required: ["repo_url"],
 329 |         },
 330 |       },
 331 |       {
 332 |         name: "git_commit",
 333 |         description: "Create a commit with the specified message.",
 334 |         inputSchema: {
 335 |           type: "object",
 336 |           properties: {
 337 |             repo_path: {
 338 |               type: "string",
 339 |               description: "The path to the local Git repository",
 340 |             },
 341 |             message: {
 342 |               type: "string",
 343 |               description: "The commit message",
 344 |             },
 345 |           },
 346 |           required: ["repo_path", "message"],
 347 |         },
 348 |       },
 349 |       {
 350 |         name: "git_track",
 351 |         description: "Track (stage) specific files or all files.",
 352 |         inputSchema: {
 353 |           type: "object",
 354 |           properties: {
 355 |             repo_path: {
 356 |               type: "string",
 357 |               description: "The path to the local Git repository",
 358 |             },
 359 |             files: {
 360 |               type: "array",
 361 |               items: { type: "string" },
 362 |               description:
 363 |                 'Array of file paths to track/stage (use ["."] for all files)',
 364 |               default: ["."],
 365 |             },
 366 |           },
 367 |           required: ["repo_path"],
 368 |         },
 369 |       },
 370 |       {
 371 |         name: "git_local_changes",
 372 |         description: "Get uncommitted changes in the working directory.",
 373 |         inputSchema: {
 374 |           type: "object",
 375 |           properties: {
 376 |             repo_path: {
 377 |               type: "string",
 378 |               description: "The path to the local Git repository",
 379 |             },
 380 |           },
 381 |           required: ["repo_path"],
 382 |         },
 383 |       },
 384 |       {
 385 |         name: "git_search_code",
 386 |         description: "Search for patterns in repository code.",
 387 |         inputSchema: {
 388 |           type: "object",
 389 |           properties: {
 390 |             repo_url: {
 391 |               type: "string",
 392 |               description: "The URL of the Git repository",
 393 |             },
 394 |             pattern: {
 395 |               type: "string",
 396 |               description: "Search pattern (regex or string)",
 397 |             },
 398 |             file_patterns: {
 399 |               type: "array",
 400 |               items: { type: "string" },
 401 |               description: 'Optional file patterns to filter (e.g., "*.js")',
 402 |             },
 403 |             case_sensitive: {
 404 |               type: "boolean",
 405 |               description: "Whether the search is case sensitive",
 406 |               default: false,
 407 |             },
 408 |             context_lines: {
 409 |               type: "integer",
 410 |               description: "Number of context lines to include",
 411 |               default: 2,
 412 |             },
 413 |           },
 414 |           required: ["repo_url", "pattern"],
 415 |         },
 416 |       },
 417 | 
 418 |       // Remote Operations
 419 |       {
 420 |         name: "git_push",
 421 |         description: "Push changes to a remote repository.",
 422 |         inputSchema: {
 423 |           type: "object",
 424 |           properties: {
 425 |             repo_path: {
 426 |               type: "string",
 427 |               description: "The path to the local Git repository",
 428 |             },
 429 |             remote: {
 430 |               type: "string",
 431 |               description: "Remote name",
 432 |               default: "origin",
 433 |             },
 434 |             branch: {
 435 |               type: "string",
 436 |               description: "Branch to push (default: current branch)",
 437 |             },
 438 |             force: {
 439 |               type: "boolean",
 440 |               description: "Whether to force push",
 441 |               default: false,
 442 |             },
 443 |           },
 444 |           required: ["repo_path"],
 445 |         },
 446 |       },
 447 |       {
 448 |         name: "git_pull",
 449 |         description: "Pull changes from a remote repository.",
 450 |         inputSchema: {
 451 |           type: "object",
 452 |           properties: {
 453 |             repo_path: {
 454 |               type: "string",
 455 |               description: "The path to the local Git repository",
 456 |             },
 457 |             remote: {
 458 |               type: "string",
 459 |               description: "Remote name",
 460 |               default: "origin",
 461 |             },
 462 |             branch: {
 463 |               type: "string",
 464 |               description: "Branch to pull (default: current branch)",
 465 |             },
 466 |             rebase: {
 467 |               type: "boolean",
 468 |               description: "Whether to rebase instead of merge",
 469 |               default: false,
 470 |             },
 471 |           },
 472 |           required: ["repo_path"],
 473 |         },
 474 |       },
 475 | 
 476 |       // Stash Operations
 477 |       {
 478 |         name: "git_stash",
 479 |         description: "Create or apply a stash.",
 480 |         inputSchema: {
 481 |           type: "object",
 482 |           properties: {
 483 |             repo_path: {
 484 |               type: "string",
 485 |               description: "The path to the local Git repository",
 486 |             },
 487 |             action: {
 488 |               type: "string",
 489 |               description: "Stash action (save, pop, apply, list, drop)",
 490 |               default: "save",
 491 |               enum: ["save", "pop", "apply", "list", "drop"],
 492 |             },
 493 |             message: {
 494 |               type: "string",
 495 |               description: "Stash message (for save action)",
 496 |               default: "",
 497 |             },
 498 |             index: {
 499 |               type: "integer",
 500 |               description: "Stash index (for pop, apply, drop actions)",
 501 |               default: 0,
 502 |             },
 503 |           },
 504 |           required: ["repo_path"],
 505 |         },
 506 |       },
 507 | 
 508 |       // Tag Operations
 509 |       {
 510 |         name: "git_create_tag",
 511 |         description: "Create a tag.",
 512 |         inputSchema: {
 513 |           type: "object",
 514 |           properties: {
 515 |             repo_path: {
 516 |               type: "string",
 517 |               description: "The path to the local Git repository",
 518 |             },
 519 |             tag_name: {
 520 |               type: "string",
 521 |               description: "Name of the tag",
 522 |             },
 523 |             message: {
 524 |               type: "string",
 525 |               description: "Tag message (for annotated tags)",
 526 |               default: "",
 527 |             },
 528 |             annotated: {
 529 |               type: "boolean",
 530 |               description: "Whether to create an annotated tag",
 531 |               default: true,
 532 |             },
 533 |           },
 534 |           required: ["repo_path", "tag_name"],
 535 |         },
 536 |       },
 537 | 
 538 |       // Advanced Operations
 539 |       {
 540 |         name: "git_rebase",
 541 |         description: "Rebase the current branch onto another branch or commit.",
 542 |         inputSchema: {
 543 |           type: "object",
 544 |           properties: {
 545 |             repo_path: {
 546 |               type: "string",
 547 |               description: "The path to the local Git repository",
 548 |             },
 549 |             onto: {
 550 |               type: "string",
 551 |               description: "Branch or commit to rebase onto",
 552 |             },
 553 |             interactive: {
 554 |               type: "boolean",
 555 |               description: "Whether to perform an interactive rebase",
 556 |               default: false,
 557 |             },
 558 |           },
 559 |           required: ["repo_path", "onto"],
 560 |         },
 561 |       },
 562 | 
 563 |       // Configuration
 564 |       {
 565 |         name: "git_config",
 566 |         description: "Configure git settings for the repository.",
 567 |         inputSchema: {
 568 |           type: "object",
 569 |           properties: {
 570 |             repo_path: {
 571 |               type: "string",
 572 |               description: "The path to the local Git repository",
 573 |             },
 574 |             scope: {
 575 |               type: "string",
 576 |               description: "Configuration scope (local, global, system)",
 577 |               default: "local",
 578 |               enum: ["local", "global", "system"],
 579 |             },
 580 |             key: {
 581 |               type: "string",
 582 |               description: "Configuration key",
 583 |             },
 584 |             value: {
 585 |               type: "string",
 586 |               description: "Configuration value",
 587 |             },
 588 |           },
 589 |           required: ["repo_path", "key", "value"],
 590 |         },
 591 |       },
 592 | 
 593 |       // Repo Management
 594 |       {
 595 |         name: "git_reset",
 596 |         description: "Reset repository to specified commit or state.",
 597 |         inputSchema: {
 598 |           type: "object",
 599 |           properties: {
 600 |             repo_path: {
 601 |               type: "string",
 602 |               description: "The path to the local Git repository",
 603 |             },
 604 |             mode: {
 605 |               type: "string",
 606 |               description: "Reset mode (soft, mixed, hard)",
 607 |               default: "mixed",
 608 |               enum: ["soft", "mixed", "hard"],
 609 |             },
 610 |             to: {
 611 |               type: "string",
 612 |               description: "Commit or reference to reset to",
 613 |               default: "HEAD",
 614 |             },
 615 |           },
 616 |           required: ["repo_path"],
 617 |         },
 618 |       },
 619 | 
 620 |       // Archive Operations
 621 |       {
 622 |         name: "git_archive",
 623 |         description: "Create a git archive (zip or tar).",
 624 |         inputSchema: {
 625 |           type: "object",
 626 |           properties: {
 627 |             repo_path: {
 628 |               type: "string",
 629 |               description: "The path to the local Git repository",
 630 |             },
 631 |             output_path: {
 632 |               type: "string",
 633 |               description: "Output path for the archive",
 634 |             },
 635 |             format: {
 636 |               type: "string",
 637 |               description: "Archive format (zip or tar)",
 638 |               default: "zip",
 639 |               enum: ["zip", "tar"],
 640 |             },
 641 |             prefix: {
 642 |               type: "string",
 643 |               description: "Prefix for files in the archive",
 644 |             },
 645 |             treeish: {
 646 |               type: "string",
 647 |               description: "Tree-ish to archive (default: HEAD)",
 648 |               default: "HEAD",
 649 |             },
 650 |           },
 651 |           required: ["repo_path", "output_path"],
 652 |         },
 653 |       },
 654 | 
 655 |       // Attributes Operations
 656 |       {
 657 |         name: "git_attributes",
 658 |         description: "Manage git attributes for files.",
 659 |         inputSchema: {
 660 |           type: "object",
 661 |           properties: {
 662 |             repo_path: {
 663 |               type: "string",
 664 |               description: "The path to the local Git repository",
 665 |             },
 666 |             action: {
 667 |               type: "string",
 668 |               description: "Action (get, set, list)",
 669 |               default: "list",
 670 |               enum: ["get", "set", "list"],
 671 |             },
 672 |             pattern: {
 673 |               type: "string",
 674 |               description: "File pattern",
 675 |             },
 676 |             attribute: {
 677 |               type: "string",
 678 |               description: "Attribute to set",
 679 |             },
 680 |           },
 681 |           required: ["repo_path", "action"],
 682 |         },
 683 |       },
 684 | 
 685 |       // Blame Operations
 686 |       {
 687 |         name: "git_blame",
 688 |         description: "Get blame information for a file.",
 689 |         inputSchema: {
 690 |           type: "object",
 691 |           properties: {
 692 |             repo_path: {
 693 |               type: "string",
 694 |               description: "The path to the local Git repository",
 695 |             },
 696 |             file_path: {
 697 |               type: "string",
 698 |               description: "Path to the file",
 699 |             },
 700 |             rev: {
 701 |               type: "string",
 702 |               description: "Revision to blame (default: HEAD)",
 703 |               default: "HEAD",
 704 |             },
 705 |           },
 706 |           required: ["repo_path", "file_path"],
 707 |         },
 708 |       },
 709 | 
 710 |       // Clean Operations
 711 |       {
 712 |         name: "git_clean",
 713 |         description: "Perform git clean operations.",
 714 |         inputSchema: {
 715 |           type: "object",
 716 |           properties: {
 717 |             repo_path: {
 718 |               type: "string",
 719 |               description: "The path to the local Git repository",
 720 |             },
 721 |             directories: {
 722 |               type: "boolean",
 723 |               description: "Whether to remove directories as well",
 724 |               default: false,
 725 |             },
 726 |             force: {
 727 |               type: "boolean",
 728 |               description: "Whether to force clean",
 729 |               default: false,
 730 |             },
 731 |             dry_run: {
 732 |               type: "boolean",
 733 |               description: "Whether to perform a dry run",
 734 |               default: true,
 735 |             },
 736 |           },
 737 |           required: ["repo_path"],
 738 |         },
 739 |       },
 740 | 
 741 |       // Hooks Operations
 742 |       {
 743 |         name: "git_hooks",
 744 |         description: "Manage git hooks in the repository.",
 745 |         inputSchema: {
 746 |           type: "object",
 747 |           properties: {
 748 |             repo_path: {
 749 |               type: "string",
 750 |               description: "The path to the local Git repository",
 751 |             },
 752 |             action: {
 753 |               type: "string",
 754 |               description: "Hook action (list, get, create, enable, disable)",
 755 |               default: "list",
 756 |               enum: ["list", "get", "create", "enable", "disable"],
 757 |             },
 758 |             hook_name: {
 759 |               type: "string",
 760 |               description:
 761 |                 "Name of the hook (e.g., 'pre-commit', 'post-merge')",
 762 |             },
 763 |             script: {
 764 |               type: "string",
 765 |               description: "Script content for the hook (for create action)",
 766 |             },
 767 |           },
 768 |           required: ["repo_path", "action"],
 769 |         },
 770 |       },
 771 | 
 772 |       // LFS Operations
 773 |       {
 774 |         name: "git_lfs",
 775 |         description: "Manage Git LFS (Large File Storage).",
 776 |         inputSchema: {
 777 |           type: "object",
 778 |           properties: {
 779 |             repo_path: {
 780 |               type: "string",
 781 |               description: "The path to the local Git repository",
 782 |             },
 783 |             action: {
 784 |               type: "string",
 785 |               description: "LFS action (install, track, untrack, list)",
 786 |               default: "list",
 787 |               enum: ["install", "track", "untrack", "list"],
 788 |             },
 789 |             patterns: {
 790 |               type: "array",
 791 |               description: "File patterns for track/untrack",
 792 |               items: { type: "string" },
 793 |             },
 794 |           },
 795 |           required: ["repo_path", "action"],
 796 |         },
 797 |       },
 798 | 
 799 |       // LFS Fetch Operations
 800 |       {
 801 |         name: "git_lfs_fetch",
 802 |         description: "Fetch LFS objects from the remote repository.",
 803 |         inputSchema: {
 804 |           type: "object",
 805 |           properties: {
 806 |             repo_path: {
 807 |               type: "string",
 808 |               description: "The path to the local Git repository",
 809 |             },
 810 |             dry_run: {
 811 |               type: "boolean",
 812 |               description: "Whether to perform a dry run",
 813 |               default: false,
 814 |             },
 815 |             pointers: {
 816 |               type: "boolean",
 817 |               description: "Whether to convert pointers to objects",
 818 |               default: false,
 819 |             },
 820 |           },
 821 |           required: ["repo_path"],
 822 |         },
 823 |       },
 824 | 
 825 |       // Revert Operations
 826 |       {
 827 |         name: "git_revert",
 828 |         description: "Revert the current branch to a commit or state.",
 829 |         inputSchema: {
 830 |           type: "object",
 831 |           properties: {
 832 |             repo_path: {
 833 |               type: "string",
 834 |               description: "The path to the local Git repository",
 835 |             },
 836 |             commit: {
 837 |               type: "string",
 838 |               description: "Commit hash or reference to revert",
 839 |             },
 840 |             no_commit: {
 841 |               type: "boolean",
 842 |               description: "Whether to stage changes without committing",
 843 |               default: false,
 844 |             },
 845 |           },
 846 |           required: ["repo_path"],
 847 |         },
 848 |       },
 849 |     ];
 850 | 
 851 |     // Set up dynamic tool listing handler
 852 |     this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
 853 |       tools: this.toolsList,
 854 |     }));
 855 | 
 856 |     // Handler categories for organization and improved discoverability
 857 |     this.handlerCategories = {
 858 |       read: [
 859 |         "git_directory_structure",
 860 |         "git_read_files",
 861 |         "git_branch_diff",
 862 |         "git_commit_history",
 863 |         "git_commits_details",
 864 |         "git_local_changes",
 865 |         "git_search_code",
 866 |       ],
 867 |       write: ["git_commit", "git_track", "git_reset"],
 868 |       branch: [
 869 |         "git_checkout_branch",
 870 |         "git_delete_branch",
 871 |         "git_merge_branch",
 872 |         "git_branch_diff",
 873 |       ],
 874 |       remote: ["git_push", "git_pull"],
 875 |       stash: ["git_stash"],
 876 |       config: ["git_config"],
 877 |       tag: ["git_create_tag"],
 878 |       advanced: ["git_rebase"],
 879 |     };
 880 | 
 881 |     // Create handler aliases for improved usability
 882 |     this.handlerAliases = {
 883 |       git_ls: "git_directory_structure",
 884 |       git_show: "git_read_files",
 885 |       git_diff: "git_branch_diff",
 886 |       git_log: "git_commit_history",
 887 |       git_status: "git_local_changes",
 888 |       git_grep: "git_search_code",
 889 |       git_add: "git_track",
 890 |       git_checkout: "git_checkout_branch",
 891 |       git_fetch: "git_pull",
 892 |     };
 893 | 
 894 |     // Initialize statistics tracking
 895 |     this.handlerStats = new Map();
 896 | 
 897 |     // Create a handlers mapping for O(1) lookup time
 898 |     this.handlersMap = {
 899 |       // Primary handlers
 900 |       git_directory_structure: handleGitDirectoryStructure,
 901 |       git_read_files: handleGitReadFiles,
 902 |       git_branch_diff: handleGitBranchDiff,
 903 |       git_commit_history: handleGitCommitHistory,
 904 |       git_commits_details: handleGitCommitsDetails,
 905 |       git_local_changes: handleGitLocalChanges,
 906 |       git_search_code: handleGitSearchCode,
 907 |       git_commit: handleGitCommit,
 908 |       git_track: handleGitTrack,
 909 |       git_checkout_branch: handleGitCheckoutBranch,
 910 |       git_delete_branch: handleGitDeleteBranch,
 911 |       git_merge_branch: handleGitMergeBranch,
 912 |       git_push: handleGitPush,
 913 |       git_pull: handleGitPull,
 914 |       git_stash: handleGitStash,
 915 |       git_create_tag: handleGitCreateTag,
 916 |       git_rebase: handleGitRebase,
 917 |       git_config: handleGitConfig,
 918 |       git_reset: handleGitReset,
 919 |       git_archive: handleGitArchive,
 920 |       git_attributes: handleGitAttributes,
 921 |       git_blame: handleGitBlame,
 922 |       git_clean: handleGitClean,
 923 |       git_hooks: handleGitHooks,
 924 |       git_lfs: handleGitLFS,
 925 |       git_lfs_fetch: handleGitLFSFetch,
 926 |       git_revert: handleGitRevert,
 927 |     };
 928 | 
 929 |     // Register aliases for O(1) lookup
 930 |     Object.entries(this.handlerAliases).forEach(([alias, target]) => {
 931 |       if (this.handlersMap[target]) {
 932 |         this.handlersMap[alias] = this.handlersMap[target];
 933 |       }
 934 |     });
 935 | 
 936 |     // Log registered handlers
 937 |     console.error(
 938 |       `[INFO] Registered ${
 939 |         Object.keys(this.handlersMap).length
 940 |       } Git tool handlers`
 941 |     );
 942 | 
 943 |     // Add method to get handlers by category
 944 |     this.getHandlersByCategory = (category) => {
 945 |       return this.handlerCategories[category] || [];
 946 |     };
 947 | 
 948 |     // Add method to execute multiple Git operations in sequence
 949 |     this.executeSequence = async (operations) => {
 950 |       const results = [];
 951 |       for (const op of operations) {
 952 |         const { name, arguments: args } = op;
 953 |         const handler = this.handlersMap[name];
 954 |         if (!handler) {
 955 |           throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
 956 |         }
 957 |         results.push(await handler(args));
 958 |       }
 959 |       return results;
 960 |     };
 961 | 
 962 |     // Add method to check if a repository is valid
 963 |     this.validateRepository = async (repoPath) => {
 964 |       try {
 965 |         // Implementation would verify if the path is a valid git repository
 966 |         return true;
 967 |       } catch (error) {
 968 |         return false;
 969 |       }
 970 |     };
 971 | 
 972 |     this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
 973 |       const { name, arguments: args } = request.params;
 974 |       const startTime = Date.now();
 975 | 
 976 |       // Handle batch operations as a special case
 977 |       if (name === "git_batch") {
 978 |         if (!Array.isArray(args.operations)) {
 979 |           throw new McpError(
 980 |             ErrorCode.InvalidParams,
 981 |             "Operations must be an array"
 982 |           );
 983 |         }
 984 |         return await this.executeSequence(args.operations);
 985 |       }
 986 | 
 987 |       try {
 988 |         // Resolve handler via direct match or alias
 989 |         const handler = this.handlersMap[name];
 990 |         if (handler) {
 991 |           // Track usage statistics
 992 |           const stats = this.handlerStats.get(name) || {
 993 |             count: 0,
 994 |             totalTime: 0,
 995 |           };
 996 |           stats.count++;
 997 |           this.handlerStats.set(name, stats);
 998 | 
 999 |           console.error(`[INFO] Executing Git tool: ${name}`);
1000 |           const result = await handler(args);
1001 | 
1002 |           const executionTime = Date.now() - startTime;
1003 |           stats.totalTime += executionTime;
1004 |           console.error(`[INFO] Completed ${name} in ${executionTime}ms`);
1005 | 
1006 |           return result;
1007 |         }
1008 | 
1009 |         // Suggest similar commands if not found
1010 |         const similarCommands = Object.keys(this.handlersMap)
1011 |           .filter((cmd) => cmd.includes(name.replace(/^git_/, "")))
1012 |           .slice(0, 3);
1013 | 
1014 |         const suggestion =
1015 |           similarCommands.length > 0
1016 |             ? `. Did you mean: ${similarCommands.join(", ")}?`
1017 |             : "";
1018 | 
1019 |         throw new McpError(
1020 |           ErrorCode.MethodNotFound,
1021 |           `Unknown tool: ${name}${suggestion}`
1022 |         );
1023 |       } catch (error) {
1024 |         // Enhanced error handling
1025 |         if (error instanceof McpError) {
1026 |           throw error;
1027 |         }
1028 |         console.error(`[ERROR] Failed to execute ${name}: ${error.message}`);
1029 |         throw new McpError(
1030 |           ErrorCode.InternalError,
1031 |           `Failed to execute ${name}: ${error.message}`
1032 |         );
1033 |       }
1034 |     });
1035 | 
1036 |     /**
1037 |      * Register a new handler at runtime
1038 |      * @param {string} name - The name of the handler
1039 |      * @param {Function} handler - The handler function
1040 |      * @param {Object} [toolInfo] - Optional tool information for ListToolsRequestSchema
1041 |      * @returns {boolean} True if registration was successful
1042 |      */
1043 |     this.registerHandler = (name, handler, toolInfo) => {
1044 |       if (typeof handler !== "function") {
1045 |         throw new Error(`Handler for ${name} must be a function`);
1046 |       }
1047 | 
1048 |       // Add to handlers map
1049 |       this.handlersMap[name] = handler;
1050 | 
1051 |       // Update tools list if toolInfo is provided
1052 |       if (toolInfo && typeof toolInfo === "object") {
1053 |         // Get current tools
1054 |         const currentTools = this.toolsList || [];
1055 | 
1056 |         // Add new tool info if not already present
1057 |         const exists = currentTools.some((tool) => tool.name === name);
1058 |         if (!exists) {
1059 |           this.toolsList = [...currentTools, { name, ...toolInfo }];
1060 |         }
1061 |       }
1062 | 
1063 |       console.error(`[INFO] Dynamically registered new handler: ${name}`);
1064 |       return true;
1065 |     };
1066 | 
1067 |     /**
1068 |      * Remove a handler
1069 |      * @param {string} name - The name of the handler to remove
1070 |      * @returns {boolean} True if removal was successful
1071 |      */
1072 |     this.unregisterHandler = (name) => {
1073 |       if (!this.handlersMap[name]) {
1074 |         return false;
1075 |       }
1076 | 
1077 |       delete this.handlersMap[name];
1078 |       console.error(`[INFO] Unregistered handler: ${name}`);
1079 |       return true;
1080 |     };
1081 |   }
1082 | 
1083 |   /**
1084 |    * Start the server
1085 |    */
1086 |   async run() {
1087 |     const transport = new StdioServerTransport();
1088 |     await this.server.connect(transport);
1089 |     console.error("Git Repo Browser MCP server running on stdio");
1090 |   }
1091 | }
1092 | 
```