# Directory Structure ``` ├── .dockerignore ├── .github │ ├── actions │ │ └── test-python │ │ └── action.yml │ ├── copilot-instructions.md │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.md │ │ └── feature_request.md │ └── workflows │ ├── docker-publish.yml │ └── pypi-publish.yml ├── .gitignore ├── .markdownlint.jsonc ├── .python-version ├── .vscode │ ├── launch.json │ ├── mcp.json │ ├── settings.json │ └── tasks.json ├── Dockerfile ├── inspector │ ├── package-lock.json │ └── package.json ├── LICENSE ├── pyproject.toml ├── README.md ├── requirements-dev.txt ├── src │ └── mcp_git_commit_generator │ ├── __init__.py │ ├── __main__.py │ └── server.py ├── tests │ └── test_server.py └── uv.lock ``` # Files -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- ``` 1 | 3.13.5 2 | ``` -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- ``` 1 | __pycache__/ 2 | *.pyc 3 | *.pyo 4 | *.pyd 5 | *.egg-info/ 6 | build/ 7 | dist/ 8 | .git/ 9 | .vscode/ 10 | .env 11 | .venv 12 | *.log 13 | uv.lock.bak 14 | ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Python-generated files 2 | __pycache__/ 3 | *.py[oc] 4 | build/ 5 | dist/ 6 | wheels/ 7 | *.egg-info 8 | 9 | # Virtual environments 10 | .venv 11 | 12 | # others 13 | node_modules/ 14 | .DS_Store 15 | ``` -------------------------------------------------------------------------------- /.markdownlint.jsonc: -------------------------------------------------------------------------------- ``` 1 | { 2 | "MD033": { 3 | "allowed_elements": ["br", "details", "summary"] 4 | }, 5 | "MD013": { 6 | "line_length": 120, 7 | "code_blocks": false, 8 | "tables": false 9 | } 10 | } 11 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # MCP Git Commit Generator 2 | 3 | [](https://pypi.org/project/mcp-git-commit-generator/) 4 | [](https://github.com/theoklitosBam7/mcp-git-commit-generator/releases) 5 | [](https://github.com/theoklitosBam7/mcp-git-commit-generator/actions/workflows/pypi-publish.yml) 6 | [](https://github.com/theoklitosBam7/mcp-git-commit-generator/actions/workflows/docker-publish.yml) 7 | [](https://github.com/theoklitosBam7/mcp-git-commit-generator/blob/main/LICENSE) 8 | 9 | Generate conventional commit messages from your staged git changes using Model Context Protocol (MCP). 10 | 11 | ## ✨ Features 12 | 13 | - **🤖 Automatic commit message generation** based on staged git diffs 14 | - **📝 Conventional Commits** support with auto-detection of type and scope 15 | - **🔄 Multiple transport options** - stdio (default) and SSE for different use cases 16 | - **🔍 Inspector UI** for interactive testing and debugging 17 | - **🐳 Docker support** with pre-built images for easy deployment 18 | - **⚡ Cross-platform** - works on macOS, Linux, and Windows 19 | 20 | ## 📦 Requirements 21 | 22 | - **For Docker usage**: [Docker](https://www.docker.com/) (for running the server in a container) 23 | - **For PyPI/uvx usage**: [Python](https://www.python.org/) >= 3.13.5 and [uv](https://github.com/astral-sh/uv) 24 | (recommended) or pip 25 | - [Git](https://git-scm.com/) (for version control) 26 | - An MCP-compatible client (VS Code with MCP extension, Claude Desktop, Cursor, Windsurf, etc.) 27 | 28 | ## 🚀 Installation 29 | 30 | You can install and use the MCP Git Commit Generator in multiple ways: 31 | 32 | ### Option 1: Using uvx (Recommended) 33 | 34 | The easiest way to use the package is with `uvx`, which automatically manages the virtual environment: 35 | 36 | ```sh 37 | uvx mcp-git-commit-generator 38 | ``` 39 | 40 | ### Option 2: Install from PyPI 41 | 42 | ```sh 43 | pip install mcp-git-commit-generator 44 | ``` 45 | 46 | Or with uv: 47 | 48 | ```sh 49 | uv pip install mcp-git-commit-generator 50 | ``` 51 | 52 | ### Option 3: Using Docker 53 | 54 | Use the pre-built Docker image from GitHub Container Registry (no installation required): 55 | 56 | ```sh 57 | docker run -i --rm --mount type=bind,src=${HOME},dst=${HOME} ghcr.io/theoklitosbam7/mcp-git-commit-generator:latest 58 | ``` 59 | 60 | ## 🛠️ Available Tools 61 | 62 | This MCP server provides the following tools to help you generate conventional commit messages: 63 | 64 | ### `generate_commit_message` 65 | 66 | Generates a conventional commit message based on your staged git changes. 67 | 68 | **Parameters:** 69 | 70 | - `repo_path` (string, optional): Path to the git repository. If omitted, uses the current directory. 71 | - `commit_type` (string, optional): Conventional commit type (e.g., `feat`, `fix`, `docs`, `style`, `refactor`, 72 | `perf`, `build`, `ci`, `test`, `chore`, `revert`). If omitted, the type will be auto-detected. 73 | - `scope` (string, optional): Scope of the change (e.g., file or module name). If omitted, the scope will be 74 | auto-detected based on changed files. 75 | 76 | **Usage:** 77 | 78 | 1. Stage your changes: `git add <files>` 79 | 2. Use the tool through your MCP client to generate a commit message 80 | 3. The tool will analyze your staged changes and generate an appropriate conventional commit message 81 | 82 | ### `check_git_status` 83 | 84 | Checks the current git repository status, including staged, unstaged, and untracked files. 85 | 86 | **Parameters:** 87 | 88 | - `repo_path` (string, optional): Path to the git repository. If omitted, uses the current directory. 89 | 90 | **Usage:** 91 | 92 | Use this tool to get an overview of your current git repository state before generating commit messages. 93 | 94 | ## 🧩 MCP Client Configuration 95 | 96 | Configure the MCP Git Commit Generator in your favorite MCP client. You have multiple options: 97 | 98 | 1. **Using uvx** (recommended - automatically manages dependencies) 99 | 2. **Using Docker** (no local Python installation required) 100 | 3. **Using local Python installation** (for development) 101 | 102 | ### VS Code 103 | 104 | Add one of the following configurations to your VS Code `mcp.json` file (usually located at `.vscode/mcp.json` in your workspace): 105 | 106 | #### Using uvx (Recommended) 107 | 108 | ```jsonc 109 | { 110 | "servers": { 111 | "mcp-git-commit-generator": { 112 | "command": "uvx", 113 | "args": ["mcp-git-commit-generator"] 114 | } 115 | } 116 | } 117 | ``` 118 | 119 | #### Using Docker 120 | 121 | ```jsonc 122 | { 123 | "servers": { 124 | "mcp-git-commit-generator": { 125 | "command": "docker", 126 | "args": [ 127 | "run", 128 | "-i", 129 | "--rm", 130 | "--mount", 131 | "type=bind,src=${userHome},dst=${userHome}", 132 | "ghcr.io/theoklitosbam7/mcp-git-commit-generator:latest" 133 | ] 134 | } 135 | } 136 | } 137 | ``` 138 | 139 | If you want to put the configuration in your user `settings.json` file, you can do so by adding: 140 | 141 | ```jsonc 142 | { 143 | "mcp": { 144 | "servers": { 145 | "mcp-git-commit-generator": { 146 | "command": "uvx", 147 | "args": ["mcp-git-commit-generator"] 148 | } 149 | } 150 | } 151 | } 152 | ``` 153 | 154 | ### Cursor 155 | 156 | Add one of the following to your Cursor MCP configuration file (usually located at `~/.cursor/mcp.json`): 157 | 158 | #### Cursor with uvx (Recommended) 159 | 160 | ```jsonc 161 | { 162 | "mcpServers": { 163 | "mcp-git-commit-generator": { 164 | "command": "uvx", 165 | "args": ["mcp-git-commit-generator"] 166 | } 167 | } 168 | } 169 | ``` 170 | 171 | #### Cursor with Docker 172 | 173 | ```jsonc 174 | { 175 | "mcpServers": { 176 | "mcp-git-commit-generator": { 177 | "command": "docker", 178 | "args": [ 179 | "run", 180 | "-i", 181 | "--rm", 182 | "--mount", 183 | "type=bind,src=${userHome},dst=${userHome}", 184 | "ghcr.io/theoklitosbam7/mcp-git-commit-generator:latest" 185 | ] 186 | } 187 | } 188 | } 189 | ``` 190 | 191 | ### Windsurf 192 | 193 | Configure Windsurf with one of the following MCP server settings (usually located at `~/.codeium/windsurf/mcp_config.json`): 194 | 195 | #### Windsurf with uvx (Recommended) 196 | 197 | ```jsonc 198 | { 199 | "mcpServers": { 200 | "mcp-git-commit-generator": { 201 | "command": "uvx", 202 | "args": ["mcp-git-commit-generator"] 203 | } 204 | } 205 | } 206 | ``` 207 | 208 | #### Windsurf with Docker 209 | 210 | ```jsonc 211 | { 212 | "mcpServers": { 213 | "mcp-git-commit-generator": { 214 | "command": "docker", 215 | "args": [ 216 | "run", 217 | "-i", 218 | "--rm", 219 | "--mount", 220 | "type=bind,src=${userHome},dst=${userHome}", 221 | "ghcr.io/theoklitosbam7/mcp-git-commit-generator:latest" 222 | ] 223 | } 224 | } 225 | } 226 | ``` 227 | 228 | ### Claude Desktop 229 | 230 | Add one of the following to your Claude Desktop configuration file (usually located at 231 | `~/Library/Application Support/Claude/claude_desktop_config.json` on macOS): 232 | 233 | #### Claude Desktop with uvx (Recommended) 234 | 235 | ```jsonc 236 | { 237 | "mcpServers": { 238 | "mcp-git-commit-generator": { 239 | "command": "uvx", 240 | "args": ["mcp-git-commit-generator"] 241 | } 242 | } 243 | } 244 | ``` 245 | 246 | #### Claude Desktop with Docker 247 | 248 | ```jsonc 249 | { 250 | "mcpServers": { 251 | "mcp-git-commit-generator": { 252 | "command": "docker", 253 | "args": [ 254 | "run", 255 | "-i", 256 | "--rm", 257 | "--mount", 258 | "type=bind,src=${userHome},dst=${userHome}", 259 | "ghcr.io/theoklitosbam7/mcp-git-commit-generator:latest" 260 | ] 261 | } 262 | } 263 | } 264 | ``` 265 | 266 | > **Note**: The `--mount` option in Docker configurations allows the Docker container to access your home 267 | > directory, enabling it to work with git repositories located anywhere in your file system. When using uvx or pip 268 | > installations, this mounting is not needed as the tool runs directly on your system. Adjust the mount path if your 269 | > repositories are located elsewhere when using Docker. 270 | 271 | ## 🚀 Quick Start Guide 272 | 273 | 1. **Install the package** using one of the methods above: 274 | - **Recommended**: `uvx mcp-git-commit-generator` (or configure in your MCP client) 275 | - **Alternative**: `pip install mcp-git-commit-generator` 276 | - **Docker**: Use the configurations above with Docker 277 | 2. **Configure your MCP client** using one of the configurations above 278 | 3. **Stage some changes** in a git repository: 279 | 280 | ```sh 281 | git add <files> 282 | ``` 283 | 284 | 4. **Use the tools** through your MCP client: 285 | - Use `check_git_status` to see your current repository state 286 | - Use `generate_commit_message` to create a conventional commit message 287 | 5. **Commit your changes** with the generated message 288 | 289 | --- 290 | 291 | ## 👨💻 Developer Guidelines 292 | 293 | The following sections are intended for developers who want to contribute to or modify the MCP Git Commit Generator. 294 | 295 | ### Local Development Setup 🛠️ 296 | 297 | If you prefer not to use Docker for development, you can run the server locally: 298 | 299 | **Requirements:** 300 | 301 | - [Python](https://www.python.org/) >= 3.13.5 302 | - [MCP CLI](https://pypi.org/project/mcp/) >= 1.10.1 303 | - [uv](https://github.com/astral-sh/uv) (for dependency management, optional but recommended) 304 | - [Node.js](https://nodejs.org/en) (for Inspector UI, optional) 305 | - [Python Debugger Extension](https://marketplace.visualstudio.com/items?itemName=ms-python.debugpy) (for debugging, optional) 306 | 307 | **Installation:** 308 | 309 | 1. **Clone the repository:** 310 | 311 | ```sh 312 | git clone https://github.com/theoklitosBam7/mcp-git-commit-generator.git 313 | cd mcp-git-commit-generator 314 | ``` 315 | 316 | 2. **Prepare environment:** 317 | 318 | There are two approaches to set up the environment for this project. You can choose either one based on your preference. 319 | 320 | > Note: Reload VSCode or terminal to ensure the virtual environment python is used after creating the virtual environment. 321 | 322 | | Approach | Steps | 323 | | -------- | ----- | 324 | | Using `uv` | 1. Create virtual environment: `uv venv` <br>2. Run VSCode Command "***Python: Select Interpreter***" and select the python from created virtual environment <br>3. Install dependencies (include dev dependencies): `uv pip install -r pyproject.toml --group dev` <br>4. Install `mcp-git-commit-generator` using the command: `uv pip install -e .`. | 325 | | Using `pip` | 1. Create virtual environment: `python -m venv .venv` <br>2. Run VSCode Command "***Python: Select Interpreter***" and select the python from created virtual environment <br>3. Install dependencies: `pip install -e .`. <br>4. Install pip dev dependencies: `pip install -r requirements-dev.txt`. | 326 | 327 | 3. **(Optional) Install Inspector dependencies:** 328 | 329 | ```sh 330 | cd inspector 331 | npm install 332 | ``` 333 | 334 | ### 📦 Publishing to PyPI 335 | 336 | The project includes an automated PyPI publishing workflow (`.github/workflows/pypi-publish.yml`) that: 337 | 338 | - **Triggers on**: Tag pushes matching `v*.*.*` pattern, manual workflow dispatch, or pull requests to main 339 | - **Builds**: Python package distributions using the `build` package 340 | - **Publishes**: Automatically publishes to PyPI using trusted publishing (OIDC) when tags are pushed 341 | 342 | To publish a new version: 343 | 344 | 1. Update the version in `pyproject.toml` 345 | 2. Create and push a git tag: `git tag vX.Y.Z && git push origin vX.Y.Z` 346 | 3. The workflow will automatically build and publish to PyPI 347 | 348 | ### 🐳 Building and Running with Docker 349 | 350 | You can build and run the MCP Git Commit Generator using Docker. The provided Dockerfile uses a multi-stage build 351 | with [`uv`](https://github.com/astral-sh/uv) for dependency management and runs the server as a non-root user for security. 352 | 353 | #### Build the Docker image 354 | 355 | ```sh 356 | docker build -t mcp-git-commit-generator . 357 | ``` 358 | 359 | #### Run the server in a container (default: stdio transport) 360 | 361 | You can run the published image directly from GitHub Container Registry. 362 | 363 | ```sh 364 | docker run -d \ 365 | --name mcp-git-commit-generator \ 366 | ghcr.io/theoklitosbam7/mcp-git-commit-generator:latest 367 | ``` 368 | 369 | By default, the container runs: 370 | 371 | ```sh 372 | mcp-git-commit-generator --transport stdio 373 | ``` 374 | 375 | If you want to use SSE transport (for Inspector UI or remote access), override the entrypoint or run manually: 376 | 377 | ```sh 378 | docker run -d \ 379 | --name mcp-git-commit-generator \ 380 | -p 3001:3001 \ 381 | --entrypoint mcp-git-commit-generator \ 382 | ghcr.io/theoklitosbam7/mcp-git-commit-generator:latest --transport sse --host 0.0.0.0 --port 3001 383 | ``` 384 | 385 | The server will be available at `http://localhost:3001` when using SSE. 386 | 387 | ### 🖥️ Running the Server Locally 388 | 389 | **To run locally (without Docker):** 390 | 391 | 1. Set up your uv or Python environment as described in the Local Development Setup section. 392 | 2. From the project root, run: 393 | 394 | <details> 395 | <summary>mcp-git-commit-generator</summary> 396 | 397 | ```sh 398 | # If you have mcp-git-commit-generator installed in your environment (default: stdio) 399 | mcp-git-commit-generator 400 | ``` 401 | 402 | </details> 403 | 404 | <details> 405 | <summary>mcp-git-commit-generator with SSE transport</summary> 406 | 407 | ```sh 408 | mcp-git-commit-generator --transport sse 409 | ``` 410 | 411 | </details> 412 | 413 | <details> 414 | <summary>Using uv</summary> 415 | 416 | ```sh 417 | uv run -m mcp_git_commit_generator --transport sse 418 | ``` 419 | 420 | </details> 421 | 422 | <details> 423 | <summary>Using Python directly</summary> 424 | 425 | ```sh 426 | python -m mcp_git_commit_generator --transport sse 427 | ``` 428 | 429 | </details> 430 | 431 | <br/> 432 | 433 | You can specify other options, for example: 434 | 435 | ```sh 436 | python -m mcp_git_commit_generator --transport sse --host 0.0.0.0 --port 3001 -v 437 | ``` 438 | 439 | > The server listens on `0.0.0.0:3001` by default when using SSE, or as specified by the options above. 440 | 441 | **Note:** 442 | 443 | - If you want to use the CLI entrypoint, ensure the package is installed and your environment is activated. 444 | - Do not use positional arguments (e.g., `python -m mcp_git_commit_generator sse`); 445 | always use options like `--transport sse`. 446 | - Available arguments with their values are: 447 | - `--transport`: Transport type (e.g., `stdio` (default), `sse`). 448 | - `--host`: Host to bind the server (default: `0.0.0.0`). 449 | - `--port`: Port to bind the server (default: `3001`). 450 | - `-v`, `--verbose`: Verbosity level (e.g., `-v`, `-vv`). 451 | 452 | ### 🔎 Start the Inspector UI 453 | 454 | From the `inspector` directory: 455 | 456 | ```sh 457 | npm run dev:inspector 458 | ``` 459 | 460 | > The Inspector UI will be available at `http://localhost:5173`. 461 | 462 | ### 🧪 Running Tests 463 | 464 | The project includes comprehensive unit tests to ensure reliability: 465 | 466 | ```sh 467 | # Run all tests 468 | pytest 469 | 470 | # Run tests with verbose output 471 | pytest -v 472 | 473 | # Run tests with coverage 474 | pytest --cov=src/mcp_git_commit_generator 475 | 476 | # Run specific test file 477 | pytest tests/test_server.py 478 | ``` 479 | 480 | **Test Coverage:** 481 | 482 | - ✅ Tool validation with invalid repository paths 483 | - ✅ Staged and unstaged change detection 484 | - ✅ Git status reporting 485 | - ✅ Commit message generation workflows 486 | - ✅ Error handling for git command failures 487 | 488 | ### 🗂️ Project Structure 489 | 490 | ```sh 491 | . 492 | ├── .github/ # GitHub workflows and issue templates 493 | ├── .gitignore 494 | ├── .markdownlint.jsonc 495 | ├── .python-version 496 | ├── .vscode/ # VSCode configuration 497 | ├── LICENSE 498 | ├── README.md 499 | ├── pyproject.toml # Python project configuration 500 | ├── requirements-dev.txt # Development dependencies 501 | ├── uv.lock # Python dependencies lock file 502 | ├── Dockerfile # Docker build file 503 | ├── build/ # Build artifacts 504 | ├── src/ # Python source code 505 | │ └── mcp_git_commit_generator/ 506 | │ ├── __init__.py # Main entry point 507 | │ ├── __main__.py # CLI entry point 508 | │ └── server.py # Main server implementation 509 | └── inspector/ # Inspector related files 510 | ├── package.json # Node.js dependencies 511 | └── package-lock.json 512 | ``` 513 | 514 | ### ⚙️ Advanced MCP Server Configuration for Development 515 | 516 | The `.vscode/mcp.json` file configures how VS Code and related tools connect to your MCP Git Commit Generator server. 517 | This file defines available server transports and their connection details, making it easy to switch between 518 | different modes (stdio is default, SSE is optional) for development and debugging. 519 | 520 | #### Example Development `mcp.json` 521 | 522 | ```jsonc 523 | { 524 | "servers": { 525 | "mcp-git-commit-generator": { 526 | "command": "docker", 527 | "args": [ 528 | "run", 529 | "-i", 530 | "--rm", 531 | "--mount", 532 | "type=bind,src=${userHome},dst=${userHome}", 533 | "ghcr.io/theoklitosbam7/mcp-git-commit-generator:latest" 534 | ] 535 | }, 536 | "sse-mcp-git-commit-generator": { 537 | "type": "sse", 538 | "url": "http://localhost:3001/sse" 539 | }, 540 | "stdio-mcp-git-commit-generator": { 541 | "type": "stdio", 542 | "command": "${command:python.interpreterPath}", 543 | "args": ["-m", "mcp_git_commit_generator", "--transport", "stdio"] 544 | }, 545 | "uvx-mcp-git-commit-generator": { 546 | "command": "uvx", 547 | "args": ["mcp-git-commit-generator"] 548 | } 549 | } 550 | } 551 | ``` 552 | 553 | - **mcp-git-commit-generator**: Runs the server in a Docker container (default: stdio transport), using the published image. 554 | - **sse-mcp-git-commit-generator**: Connects to the MCP server using Server-Sent Events (SSE) at `http://localhost:3001/sse`. 555 | Only useful if you run the server with `--transport sse`. 556 | - **stdio-mcp-git-commit-generator**: Connects using standard input/output (stdio), running the server as a subprocess. 557 | This is the default and recommended for local development and debugging. 558 | - **uvx-mcp-git-commit-generator**: Uses uvx to automatically install and run the package from PyPI. 559 | 560 | ### 🐞 Debugging the MCP Server 561 | 562 | > Notes: 563 | > 564 | > - [MCP Inspector](https://github.com/modelcontextprotocol/inspector) is a visual developer tool for testing 565 | and debugging MCP servers. 566 | > - All debugging modes support breakpoints, so you can add breakpoints to the tool implementation code. 567 | > - **You can test tool arguments directly in the Inspector UI**: When using the Inspector, select a tool and provide 568 | arguments in the input fields to simulate real usage and debug argument handling. 569 | 570 | | Debug Mode | Description | Steps to debug | 571 | | ---------- | ----------- | --------------- | 572 | | MCP Inspector | Debug the MCP server using the MCP Inspector. | 1. Install [Node.js](https://nodejs.org/)<br> 2. Set up Inspector: `cd inspector` && `npm install` <br> 3. Open VS Code Debug panel. Select `Debug in Inspector (Edge)` or `Debug in Inspector (Chrome)`. Press F5 to start debugging.<br> 4. When MCP Inspector launches in the browser, click the `Connect` button to connect this MCP server.<br> 5. Then you can `List Tools`, select a tool, input parameters (see arguments above), and `Run Tool` to debug your server code.<br> | 573 | 574 | ### ⚙️ Default Ports and Customizations 575 | 576 | | Debug Mode | Ports | Definitions | Customizations | Note | 577 | | ---------- | ----- | ------------ | -------------- |-------------- | 578 | | MCP Inspector | 3001 (Server, SSE only); 5173 and 3000 (Inspector) | [tasks.json](.vscode/tasks.json) | Edit [launch.json](.vscode/launch.json), [tasks.json](.vscode/tasks.json), [\_\_init\_\_.py](src/__init__.py), [mcp.json](.vscode/mcp.json) to change above ports.| N/A | 579 | 580 | ## 💬 Feedback 581 | 582 | If you have any feedback or suggestions, please open an issue on the [MCP Git Commit Generator GitHub repository](https://github.com/theoklitosBam7/mcp-git-commit-generator/issues) 583 | 584 | ## 📖 Troubleshooting 585 | 586 | ### Common Issues 587 | 588 | - **"Path is not a valid git repository"**: Ensure you're in a directory with a `.git` folder 589 | - **"No staged changes found"**: Run `git add <files>` to stage your changes first 590 | - **"Git is not installed"**: Install Git from [git-scm.com](https://git-scm.com/) 591 | - **Docker permission issues**: Ensure Docker can access your home directory 592 | - **MCP connection fails**: Verify your client configuration matches the examples above 593 | 594 | ### Getting Help 595 | 596 | - Check the [Issues page](https://github.com/theoklitosBam7/mcp-git-commit-generator/issues) for solutions 597 | - Use the Inspector UI for interactive debugging 598 | - Run `pytest -v` to verify your installation 599 | 600 | ## 📄 License 601 | 602 | [MIT](./LICENSE) License © 2025 [Theoklitos Bampouris](https://github.com/theoklitosBam7) 603 | ``` -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- ``` 1 | # requirements-dev.txt only for pip compatibility 2 | debugpy>=1.8.14 3 | pre-commit>=4.2.0 4 | pytest>=8.4.1 5 | ``` -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "python.testing.pytestArgs": ["tests"], 3 | "python.testing.unittestEnabled": false, 4 | "python.testing.pytestEnabled": true 5 | } 6 | ``` -------------------------------------------------------------------------------- /src/mcp_git_commit_generator/__main__.py: -------------------------------------------------------------------------------- ```python 1 | """Generate conventional commit messages from your staged git changes using Model Context Protocol (MCP).""" 2 | 3 | from mcp_git_commit_generator import main 4 | 5 | main() 6 | ``` -------------------------------------------------------------------------------- /inspector/package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "inspector-placeholder", 3 | "version": "0.0.1", 4 | "description": "This is just a placeholder to launch the inspector.", 5 | "private": true, 6 | "scripts": { 7 | "dev:inspector": "CLIENT_PORT=5173 SERVER_PORT=3000 mcp-inspector" 8 | }, 9 | "devDependencies": { 10 | "@modelcontextprotocol/inspector": "^0.15.0" 11 | } 12 | } 13 | ``` -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | ``` -------------------------------------------------------------------------------- /.vscode/mcp.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "servers": { 3 | "mcp-git-commit-generator": { 4 | "command": "docker", 5 | "args": [ 6 | "run", 7 | "-i", 8 | "--rm", 9 | "--mount", 10 | "type=bind,src=${userHome},dst=${userHome}", 11 | "ghcr.io/theoklitosbam7/mcp-git-commit-generator:latest" 12 | ] 13 | }, 14 | "sse-mcp-git-commit-generator": { 15 | "type": "sse", 16 | "url": "http://localhost:3001/sse" 17 | }, 18 | "stdio-mcp-git-commit-generator": { 19 | "type": "stdio", 20 | "command": "${command:python.interpreterPath}", 21 | "args": ["-m", "mcp_git_commit_generator", "--transport", "stdio"] 22 | }, 23 | "uvx-mcp-git-commit-generator": { 24 | "command": "uvx", 25 | "args": ["mcp-git-commit-generator"] 26 | } 27 | } 28 | } 29 | ``` -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | ``` -------------------------------------------------------------------------------- /.github/actions/test-python/action.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: test-python 2 | description: "Runs Python tests for the MCP Git Commit Generator project" 3 | 4 | runs: 5 | using: "composite" 6 | steps: 7 | - name: Set up Python for tests 8 | uses: actions/setup-python@v5 9 | with: 10 | python-version: "3.13.5" 11 | 12 | - name: Cache pip 13 | uses: actions/cache@v4 14 | with: 15 | path: | 16 | ~/.cache/pip 17 | key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements-dev.txt') }} 18 | restore-keys: | 19 | ${{ runner.os }}-pip- 20 | 21 | - name: Configure git user for CI 22 | run: | 23 | git config --global user.email "[email protected]" 24 | git config --global user.name "CI Runner" 25 | shell: bash 26 | 27 | - name: Install test dependencies 28 | run: | 29 | python -m pip install --upgrade pip 30 | python -m pip install -e . 31 | if [ -f requirements-dev.txt ]; then python -m pip install -r requirements-dev.txt; fi 32 | shell: bash 33 | 34 | - name: Run tests 35 | run: | 36 | pytest -q 37 | shell: bash 38 | ``` -------------------------------------------------------------------------------- /.github/workflows/pypi-publish.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: Publish Python 🐍 package to PyPI 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | tags: 7 | - "v*.*.*" 8 | pull_request: 9 | branches: 10 | - "main" 11 | 12 | jobs: 13 | release-build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Run Python tests 20 | uses: ./.github/actions/test-python 21 | 22 | - name: Build release distributions 23 | run: | 24 | python -m pip install build 25 | python -m build 26 | 27 | - name: Upload distributions 28 | uses: actions/upload-artifact@v4 29 | with: 30 | name: release-dists 31 | path: dist/ 32 | 33 | pypi-publish: 34 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') 35 | runs-on: ubuntu-latest 36 | needs: 37 | - release-build 38 | permissions: 39 | contents: read 40 | id-token: write 41 | 42 | environment: 43 | name: pypi 44 | 45 | steps: 46 | - name: Retrieve release distributions 47 | uses: actions/download-artifact@v4 48 | with: 49 | name: release-dists 50 | path: dist/ 51 | 52 | - name: Publish release distributions to PyPI 53 | uses: pypa/gh-action-pypi-publish@release/v1 54 | with: 55 | packages-dir: dist/ 56 | ``` -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- ```toml 1 | [project] 2 | name = "mcp-git-commit-generator" 3 | version = "2.1.0" 4 | description = "Generate conventional commit messages from your staged git changes using Model Context Protocol (MCP)." 5 | authors = [{ name = "Theoklitos Bampouris", email = "[email protected]" }] 6 | license = { text = "MIT" } 7 | maintainers = [ 8 | { name = "Theoklitos Bampouris", email = "[email protected]" }, 9 | ] 10 | keywords = [ 11 | "mcp", 12 | "model context protocol", 13 | "git", 14 | "commit", 15 | "conventional commits", 16 | "conventional commit messages", 17 | ] 18 | readme = "README.md" 19 | requires-python = ">=3.13.5" 20 | classifiers = [ 21 | "Programming Language :: Python :: 3", 22 | "Operating System :: OS Independent", 23 | ] 24 | dependencies = ["click>=8.2.1", "mcp[cli]>=1.10.1"] 25 | 26 | [dependency-groups] 27 | dev = ["debugpy>=1.8.14", "pre-commit>=4.2.0", "pytest>=8.4.1"] 28 | 29 | [project.scripts] 30 | mcp-git-commit-generator = "mcp_git_commit_generator:main" 31 | 32 | [project.urls] 33 | Homepage = "https://github.com/theoklitosBam7/mcp-git-commit-generator" 34 | Issues = "https://github.com/theoklitosBam7/mcp-git-commit-generator/issues" 35 | 36 | [build-system] 37 | requires = ["uv_build >= 0.7.19, <0.9.0"] 38 | build-backend = "uv_build" 39 | 40 | [tool.pytest.ini_options] 41 | pythonpath = ["src"] 42 | testpaths = ["tests"] 43 | ``` -------------------------------------------------------------------------------- /.github/workflows/docker-publish.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: Create and Publish Docker 🐳 image 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | tags: 7 | - "v*.*.*" 8 | pull_request: 9 | branches: 10 | - "main" 11 | 12 | env: 13 | REGISTRY: ghcr.io 14 | IMAGE_NAME: ${{ github.repository }} 15 | 16 | jobs: 17 | build-and-push: 18 | runs-on: ubuntu-latest 19 | 20 | permissions: 21 | contents: read 22 | packages: write 23 | 24 | steps: 25 | - name: Checkout repository 26 | uses: actions/checkout@v4 27 | 28 | - name: Run Python tests 29 | uses: ./.github/actions/test-python 30 | 31 | - name: Set up QEMU 32 | uses: docker/setup-qemu-action@v3 33 | 34 | - name: Set up Docker Buildx 35 | uses: docker/setup-buildx-action@v3 36 | 37 | - name: Extract metadata (tags, labels) for Docker 38 | id: meta 39 | uses: docker/metadata-action@v5 40 | with: 41 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 42 | 43 | - name: Log in to GitHub Container Registry 44 | uses: docker/login-action@v3 45 | with: 46 | registry: ${{ env.REGISTRY }} 47 | username: ${{ github.actor }} 48 | password: ${{ secrets.GITHUB_TOKEN }} 49 | 50 | - name: Build and push Docker image 51 | uses: docker/build-push-action@v6 52 | with: 53 | context: . 54 | push: ${{ github.event_name != 'pull_request' }} 55 | tags: ${{ steps.meta.outputs.tags }} 56 | labels: ${{ steps.meta.outputs.labels }} 57 | platforms: linux/amd64,linux/arm64 58 | ``` -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Attach to Local MCP", 6 | "type": "debugpy", 7 | "request": "attach", 8 | "connect": { 9 | "host": "localhost", 10 | "port": 5678 11 | }, 12 | "presentation": { 13 | "hidden": true 14 | }, 15 | "internalConsoleOptions": "neverOpen", 16 | "postDebugTask": "Terminate All Tasks" 17 | }, 18 | { 19 | "name": "Launch Inspector (Edge)", 20 | "type": "msedge", 21 | "request": "launch", 22 | "url": "http://localhost:5173?timeout=60000&serverUrl=http://localhost:3001/sse#tools", 23 | "cascadeTerminateToConfigurations": ["Attach to Local MCP"], 24 | "presentation": { 25 | "hidden": true 26 | }, 27 | "internalConsoleOptions": "neverOpen" 28 | }, 29 | { 30 | "name": "Launch Inspector (Chrome)", 31 | "type": "chrome", 32 | "request": "launch", 33 | "url": "http://localhost:5173?timeout=60000&serverUrl=http://localhost:3001/sse#tools", 34 | "cascadeTerminateToConfigurations": ["Attach to Local MCP"], 35 | "presentation": { 36 | "hidden": true 37 | }, 38 | "internalConsoleOptions": "neverOpen" 39 | } 40 | ], 41 | "compounds": [ 42 | { 43 | "name": "Debug in Inspector (Edge)", 44 | "configurations": ["Launch Inspector (Edge)", "Attach to Local MCP"], 45 | "preLaunchTask": "Start MCP Inspector", 46 | "stopAll": true 47 | }, 48 | { 49 | "name": "Debug in Inspector (Chrome)", 50 | "configurations": ["Launch Inspector (Chrome)", "Attach to Local MCP"], 51 | "preLaunchTask": "Start MCP Inspector", 52 | "stopAll": true 53 | } 54 | ] 55 | } 56 | ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- ```dockerfile 1 | # Use a Python image with uv pre-installed 2 | FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim AS uv 3 | 4 | LABEL org.opencontainers.image.source="https://github.com/theoklitosBam7/mcp-git-commit-generator" 5 | LABEL org.opencontainers.image.description="Generate conventional commit messages from your staged git changes using Model Context Protocol (MCP)." 6 | LABEL org.opencontainers.image.licenses=MIT 7 | 8 | # Install the project into `/app` 9 | WORKDIR /app 10 | 11 | # First copy only the required files for dependency installation 12 | COPY pyproject.toml uv.lock ./ 13 | 14 | # Enable bytecode compilation 15 | ENV UV_COMPILE_BYTECODE=1 16 | 17 | # Copy from the cache instead of linking since it's a mounted volume 18 | ENV UV_LINK_MODE=copy 19 | 20 | # Install the project's dependencies using the lockfile and settings 21 | RUN --mount=type=cache,target=/root/.cache/uv \ 22 | uv sync --frozen --no-install-project --no-dev --no-editable 23 | 24 | # Then, add the rest of the project source code and install it 25 | # Installing separately from its dependencies allows optimal layer caching 26 | COPY . /app 27 | RUN --mount=type=cache,target=/root/.cache/uv \ 28 | uv sync --frozen --no-dev --no-editable 29 | 30 | RUN uv pip install . 31 | 32 | FROM python:3.13-slim-bookworm 33 | 34 | LABEL org.opencontainers.image.source="https://github.com/theoklitosBam7/mcp-git-commit-generator" 35 | LABEL org.opencontainers.image.description="Generate conventional commit messages from your staged git changes using Model Context Protocol (MCP)." 36 | LABEL org.opencontainers.image.licenses=MIT 37 | 38 | WORKDIR /app 39 | 40 | RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/* \ 41 | && adduser --disabled-password --gecos '' --uid 1000 appuser 42 | COPY --from=uv --chown=appuser:appuser /app/.venv /app/.venv 43 | 44 | ENV PATH="/app/.venv/bin:$PATH" 45 | ENV PYTHONUNBUFFERED=1 46 | 47 | RUN chown -R appuser:appuser /app 48 | USER appuser 49 | 50 | ENTRYPOINT ["mcp-git-commit-generator", "--transport", "stdio"] 51 | ``` -------------------------------------------------------------------------------- /src/mcp_git_commit_generator/__init__.py: -------------------------------------------------------------------------------- ```python 1 | """Main entry point for the MCP server.""" 2 | 3 | import logging 4 | import os 5 | import sys 6 | from typing import Literal, cast 7 | 8 | import click 9 | 10 | from .server import mcp 11 | 12 | 13 | @click.command() 14 | @click.option( 15 | "--transport", 16 | default="stdio", 17 | type=click.Choice(["stdio", "sse"]), 18 | help="Transport type (stdio or sse)", 19 | ) 20 | @click.option("--host", default="0.0.0.0", help="Host to listen on (for sse)") 21 | @click.option("--port", default=3001, type=int, help="Port to listen on (for sse)") 22 | @click.option( 23 | "-v", 24 | "--verbose", 25 | count=True, 26 | help="Verbosity level, use -v or -vv", 27 | ) 28 | def main( 29 | transport, 30 | host, 31 | port, 32 | verbose, 33 | ): 34 | """ 35 | Generate conventional commit messages from your staged git changes using 36 | Model Context Protocol (MCP). 37 | """ 38 | logging_level = logging.WARN 39 | if verbose == 1: 40 | logging_level = logging.INFO 41 | elif verbose >= 2: 42 | logging_level = logging.DEBUG 43 | 44 | logging.basicConfig(level=logging_level, stream=sys.stderr) 45 | logger = logging.getLogger(__name__) 46 | logger.info( 47 | "Starting MCP server with transport type: %s, verbosity level: %d", 48 | transport, 49 | verbose, 50 | ) 51 | 52 | allowed_levels = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"} 53 | env_log_level = cast( 54 | Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], 55 | os.environ.get("LOG_LEVEL", "DEBUG"), 56 | ) 57 | mcp.settings.log_level = ( 58 | env_log_level if env_log_level in allowed_levels else "DEBUG" 59 | ) 60 | if transport == "sse": 61 | mcp.settings.port = port 62 | mcp.settings.host = host 63 | mcp.run(transport="sse") 64 | elif transport == "stdio": 65 | mcp.run(transport="stdio") 66 | else: 67 | logger.error("Invalid transport type. Use 'sse' or 'stdio'.") 68 | sys.exit(1) 69 | 70 | 71 | if __name__ == "__main__": 72 | main(transport="stdio", host=None, port=None, verbose=False) 73 | ``` -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "Start MCP Server", 6 | "type": "shell", 7 | "command": "${command:python.interpreterPath}", 8 | "args": [ 9 | "-m", 10 | "debugpy", 11 | "--listen", 12 | "localhost:5678", 13 | "-m", 14 | "mcp_git_commit_generator", 15 | "--transport", 16 | "sse" 17 | ], 18 | "isBackground": true, 19 | "options": { 20 | "cwd": "${workspaceFolder}", 21 | "env": { 22 | "PORT": "3001" 23 | } 24 | }, 25 | "problemMatcher": { 26 | "pattern": [ 27 | { 28 | "regexp": "^.*$", 29 | "file": 0, 30 | "location": 1, 31 | "message": 2 32 | } 33 | ], 34 | "background": { 35 | "activeOnStart": true, 36 | "beginsPattern": ".*", 37 | "endsPattern": "Application startup complete|running" 38 | } 39 | } 40 | }, 41 | { 42 | "label": "Start MCP Inspector", 43 | "type": "shell", 44 | "command": "npm run dev:inspector", 45 | "isBackground": true, 46 | "options": { 47 | "cwd": "${workspaceFolder}/inspector", 48 | "env": { 49 | "CLIENT_PORT": "5173", 50 | "SERVER_PORT": "3000" 51 | } 52 | }, 53 | "problemMatcher": { 54 | "pattern": [ 55 | { 56 | "regexp": "^.*$", 57 | "file": 0, 58 | "location": 1, 59 | "message": 2 60 | } 61 | ], 62 | "background": { 63 | "activeOnStart": true, 64 | "beginsPattern": "Starting MCP inspector", 65 | "endsPattern": "⚙️ Proxy server listening on" 66 | } 67 | }, 68 | "dependsOn": ["Start MCP Server"] 69 | }, 70 | { 71 | "label": "Terminate All Tasks", 72 | "command": "echo 'Terminating all tasks...'", 73 | "type": "shell", 74 | "problemMatcher": [] 75 | } 76 | ], 77 | "inputs": [ 78 | { 79 | "id": "terminate", 80 | "type": "command", 81 | "command": "workbench.action.tasks.terminate", 82 | "args": "terminateAll" 83 | } 84 | ] 85 | } 86 | ``` -------------------------------------------------------------------------------- /.github/copilot-instructions.md: -------------------------------------------------------------------------------- ```markdown 1 | # MCP Git Commit Generator: AI Agent Guide 2 | 3 | ## Project Overview 4 | 5 | This MCP server generates conventional commit messages from staged git changes. It exposes two main MCP tools: 6 | 7 | - `generate_commit_message`: Analyzes staged changes and generates a conventional commit message. Commit type and scope can be auto-detected or provided. 8 | - `check_git_status`: Reports staged, unstaged, and untracked files with clear validation of the git repository path. 9 | 10 | ## Architecture & Key Files 11 | 12 | - **MCP Tool Logic**: All tool logic is in `src/mcp_git_commit_generator/server.py` using the `@mcp.tool()` decorator. Helper functions like `_get_valid_repo_path` and robust error handling with `subprocess.run` are standard. 13 | - **CLI Entrypoint**: `src/mcp_git_commit_generator/__init__.py` enforces strict `--option value` usage for all CLI arguments (never use positional args). 14 | - **Module Launcher**: `src/mcp_git_commit_generator/__main__.py` bootstraps the server for `python -m` and CLI use. 15 | - **Inspector UI**: The `inspector/` directory contains a Node.js-based tool for interactive MCP tool testing (`npm run dev:inspector`). 16 | 17 | ## Developer Workflow & Commands 18 | 19 | ### Environment Setup 20 | 21 | - **Recommended**: `uv venv` → `uv pip install -r pyproject.toml --group dev` → `uv pip install -e .` 22 | - **Alternative**: `python -m venv .venv` → `pip install -e . && pip install -r requirements-dev.txt` 23 | 24 | ### Running & Debugging 25 | 26 | - **VS Code Tasks**: Use "Start MCP Server" (SSE, port 5678) and "Start MCP Inspector" for local development. Inspector UI runs at `http://localhost:5173`. 27 | - **Local CLI**: Run with `mcp-git-commit-generator --transport sse` or `python -m mcp_git_commit_generator --transport sse` (always use `--option value`). 28 | - **Docker**: Build with `docker build -t mcp-git-commit-generator .`. Run with: 29 | - Default: `docker run -i --rm --mount type=bind,src=${HOME},dst=${HOME} mcp-git-commit-generator` 30 | - SSE: `docker run -d -p 3001:3001 --entrypoint mcp-git-commit-generator ... --transport sse --host 0.0.0.0 --port 3001` 31 | 32 | ### Inspector UI Workflow 33 | 34 | 1. Start the server (see above) 35 | 2. From `inspector/`, run `npm run dev:inspector` 36 | 3. Open `http://localhost:5173` and connect to the running MCP server 37 | 4. Use the UI to list, invoke, and debug tools interactively 38 | 39 | ## Project-Specific Conventions & Patterns 40 | 41 | - **CLI Usage**: Always use `--option value` for all arguments. Positional arguments are not supported and will error. 42 | - **MCP Tool Definitions**: Define new tools in `server.py` with detailed docstrings and robust error handling. Follow the pattern of `generate_commit_message` and `check_git_status`. 43 | - **Git Operations**: Always validate the repo path with `_get_valid_repo_path`. Use `subprocess.run` for all git commands, with structured error handling. 44 | - **Testing**: Run tests with `pytest`, e.g. `pytest -v` or `pytest --cov=src/mcp_git_commit_generator`. 45 | - **Versioning**: Update `pyproject.toml` for releases. Docker images are published on tag pushes starting with "v". 46 | 47 | ## Integration Points & Communication 48 | 49 | - **MCP Clients**: Integrates with VS Code, Cursor, Windsurf, and Claude Desktop. See README for config examples. 50 | - **Transports**: Supports both `stdio` and `sse` (for Inspector UI and remote access). Match your client config to the server mode. 51 | 52 | ## Example Usage 53 | 54 | 1. Stage changes: `git add <files>` 55 | 2. Run `check_git_status` to verify the current git state 56 | 3. Generate a commit message with `generate_commit_message` 57 | 4. Commit using the generated message 58 | 59 | ## Key Patterns for AI Agents 60 | 61 | - **Focus on Key Files**: Use `server.py` for tool logic, `__init__.py` for CLI conventions 62 | - **Agent Guidance**: When building new features, follow the code and docstring patterns in this guide. Use inline comments like `// ...existing code...` to contextualize changes 63 | - **Integration**: Ensure new code fits the established patterns for git ops, Docker, and Inspector UI 64 | 65 | ## Feedback & Iteration 66 | 67 | Please review these instructions and provide feedback on any unclear or incomplete sections so they can be improved. 68 | ``` -------------------------------------------------------------------------------- /tests/test_server.py: -------------------------------------------------------------------------------- ```python 1 | """ 2 | Unit tests for the MCP Git Commit Generator server tools. 3 | """ 4 | 5 | import subprocess 6 | 7 | from mcp_git_commit_generator import server 8 | 9 | 10 | def test_generate_commit_message_invalid_repo(): 11 | """Test that an invalid repo path returns an error message.""" 12 | result = server.generate_commit_message(repo_path="/not/a/repo") 13 | assert "not a valid git repository" in result 14 | 15 | 16 | def test_check_git_status_invalid_repo(): 17 | """Test that an invalid repo path returns an error message for git status.""" 18 | result = server.check_git_status(repo_path="/not/a/repo") 19 | assert "not a valid git repository" in result 20 | 21 | 22 | def test_generate_commit_message_no_staged_changes(tmp_path): 23 | """Test that no staged changes returns the appropriate message.""" 24 | repo_dir = tmp_path / "repo" 25 | repo_dir.mkdir() 26 | subprocess.run(["git", "init"], cwd=repo_dir, check=True) 27 | result = server.generate_commit_message(repo_path=str(repo_dir)) 28 | assert "No staged changes found" in result 29 | 30 | 31 | def test_check_git_status_clean_repo(tmp_path): 32 | """Test that a clean repo returns the correct status message.""" 33 | repo_dir = tmp_path / "repo" 34 | repo_dir.mkdir() 35 | subprocess.run(["git", "init"], cwd=repo_dir, check=True) 36 | result = server.check_git_status(repo_path=str(repo_dir)) 37 | assert "No changes to commit" in result 38 | 39 | 40 | def test_generate_commit_message_with_staged_change(tmp_path): 41 | """Test that a staged file produces a commit message analysis.""" 42 | repo_dir = tmp_path / "repo" 43 | repo_dir.mkdir() 44 | subprocess.run(["git", "init"], cwd=repo_dir, check=True) 45 | file_path = repo_dir / "foo.txt" 46 | file_path.write_text("hello world\n") 47 | subprocess.run(["git", "add", "foo.txt"], cwd=repo_dir, check=True) 48 | result = server.generate_commit_message(repo_path=str(repo_dir)) 49 | assert "Git Change Analysis for Conventional Commit Message" in result 50 | assert "foo.txt" in result 51 | 52 | 53 | def test_check_git_status_with_staged_and_unstaged(tmp_path): 54 | """Test that both staged and unstaged changes are reported correctly.""" 55 | repo_dir = tmp_path / "repo" 56 | repo_dir.mkdir() 57 | subprocess.run(["git", "init"], cwd=repo_dir, check=True) 58 | file_path = repo_dir / "bar.txt" 59 | file_path.write_text("first\n") 60 | subprocess.run(["git", "add", "bar.txt"], cwd=repo_dir, check=True) 61 | file_path.write_text("second\n") 62 | result = server.check_git_status(repo_path=str(repo_dir)) 63 | assert "Staged files" in result 64 | assert "Unstaged files" in result 65 | assert "bar.txt" in result 66 | 67 | 68 | def test_check_git_status_with_untracked_files(tmp_path): 69 | """Test that untracked files are reported correctly.""" 70 | repo_dir = tmp_path / "repo" 71 | repo_dir.mkdir() 72 | subprocess.run(["git", "init"], cwd=repo_dir, check=True) 73 | file_path = repo_dir / "untracked.txt" 74 | file_path.write_text("untracked\n") 75 | result = server.check_git_status(repo_path=str(repo_dir)) 76 | assert "Untracked files" in result 77 | assert "untracked.txt" in result 78 | 79 | 80 | def test_generate_commit_message_multiple_files_staged(tmp_path): 81 | """Test commit message analysis with multiple files staged.""" 82 | repo_dir = tmp_path / "repo" 83 | repo_dir.mkdir() 84 | subprocess.run(["git", "init"], cwd=repo_dir, check=True) 85 | file1 = repo_dir / "a.txt" 86 | file2 = repo_dir / "b.txt" 87 | file1.write_text("A\n") 88 | file2.write_text("B\n") 89 | subprocess.run(["git", "add", "a.txt", "b.txt"], cwd=repo_dir, check=True) 90 | result = server.generate_commit_message(repo_path=str(repo_dir)) 91 | assert "a.txt" in result and "b.txt" in result 92 | 93 | 94 | def test_check_git_status_with_staged_deletion(tmp_path): 95 | """Test that staged file deletions are reported correctly.""" 96 | repo_dir = tmp_path / "repo" 97 | repo_dir.mkdir() 98 | subprocess.run(["git", "init"], cwd=repo_dir, check=True) 99 | file_path = repo_dir / "delete_me.txt" 100 | file_path.write_text("bye\n") 101 | subprocess.run(["git", "add", "delete_me.txt"], cwd=repo_dir, check=True) 102 | subprocess.run(["git", "commit", "-m", "add file"], cwd=repo_dir, check=True) 103 | subprocess.run(["git", "rm", "delete_me.txt"], cwd=repo_dir, check=True) 104 | result = server.check_git_status(repo_path=str(repo_dir)) 105 | print(result) 106 | assert "delete_me.txt" in result 107 | assert "Staged files" in result 108 | 109 | 110 | def test_generate_commit_message_with_type_and_scope(tmp_path): 111 | """Test generate_commit_message with explicit commit_type and scope.""" 112 | repo_dir = tmp_path / "repo" 113 | repo_dir.mkdir() 114 | subprocess.run(["git", "init"], cwd=repo_dir, check=True) 115 | file_path = repo_dir / "scoped.txt" 116 | file_path.write_text("scoped\n") 117 | subprocess.run(["git", "add", "scoped.txt"], cwd=repo_dir, check=True) 118 | result = server.generate_commit_message( 119 | repo_path=str(repo_dir), commit_type="feat", scope="core" 120 | ) 121 | assert "Requested commit type: feat" in result 122 | assert "Requested scope: core" in result 123 | 124 | 125 | def test_generate_commit_message_breaking_change_prompt(tmp_path): 126 | """Test that the breaking change footer instructions are present in the prompt.""" 127 | repo_dir = tmp_path / "repo" 128 | repo_dir.mkdir() 129 | subprocess.run(["git", "init"], cwd=repo_dir, check=True) 130 | file_path = repo_dir / "breaking.txt" 131 | file_path.write_text("breaking change\n") 132 | subprocess.run(["git", "add", "breaking.txt"], cwd=repo_dir, check=True) 133 | result = server.generate_commit_message(repo_path=str(repo_dir)) 134 | assert "BREAKING CHANGE" in result 135 | ``` -------------------------------------------------------------------------------- /src/mcp_git_commit_generator/server.py: -------------------------------------------------------------------------------- ```python 1 | """ 2 | FastMCP Server for generating conventional commit messages from git diff 3 | """ 4 | 5 | import logging 6 | import os 7 | import subprocess 8 | import textwrap 9 | from typing import Optional 10 | 11 | from mcp.server.fastmcp import FastMCP 12 | 13 | # Create the FastMCP server 14 | mcp = FastMCP("Git Commit Generator") 15 | 16 | 17 | def _get_valid_repo_path(repo_path: Optional[str]) -> Optional[str]: 18 | """ 19 | Resolve and validate the git repository path. 20 | Returns the valid repo path if valid, otherwise None. 21 | """ 22 | logger = logging.getLogger(__name__) 23 | # Resolve user tilde and symlinks to a canonical path 24 | resolved = ( 25 | os.path.realpath(os.path.expanduser(repo_path)) if repo_path else os.getcwd() 26 | ) 27 | logger.info("[get_valid_repo_path] Resolved repository path: %s", resolved) 28 | if not os.path.isdir(resolved) or not os.path.exists( 29 | os.path.join(resolved, ".git") 30 | ): 31 | return None 32 | return resolved 33 | 34 | 35 | @mcp.tool() 36 | def generate_commit_message( 37 | repo_path: Optional[str] = None, 38 | commit_type: Optional[str] = None, 39 | scope: Optional[str] = None, 40 | ) -> str: 41 | """ 42 | Prepare a structured analysis and instruction block for generating a 43 | Conventional Commit message from staged git changes only. 44 | 45 | Behavior: 46 | - Validates the repository path and operates on the provided repo or CWD. 47 | - Collects staged diff, porcelain status, and a name-status summary. 48 | - Incorporates optional user preferences for commit_type and scope. 49 | - Returns a single formatted string that includes context plus strict 50 | output instructions for an LLM to produce a Conventional Commit. 51 | 52 | Args: 53 | repo_path: Optional path to the target git repository. If not provided, uses the current working directory. 54 | commit_type: Optional commit type (feat, fix, docs, style, refactor, perf, build, ci, test, chore, revert) 55 | scope: Optional scope of the change 56 | 57 | Returns: 58 | A formatted prompt containing git change context and clear output rules 59 | for generating a Conventional Commit message 60 | """ 61 | try: 62 | valid_repo_path = _get_valid_repo_path(repo_path) 63 | if not valid_repo_path: 64 | return f"Path '{repo_path or os.getcwd()}' is not a valid git repository." 65 | cwd = valid_repo_path 66 | # Get staged changes 67 | diff_result = subprocess.run( 68 | ["git", "diff", "--cached"], 69 | capture_output=True, 70 | text=True, 71 | check=True, 72 | cwd=cwd, 73 | ) 74 | 75 | if not diff_result.stdout.strip(): 76 | return "No staged changes found. Please stage your changes with 'git add' first." 77 | 78 | # Get git status for context 79 | status_result = subprocess.run( 80 | ["git", "status", "--porcelain"], 81 | capture_output=True, 82 | text=True, 83 | check=True, 84 | cwd=cwd, 85 | ) 86 | 87 | # Get list of changed files for better analysis 88 | files_result = subprocess.run( 89 | ["git", "diff", "--cached", "--name-status"], 90 | capture_output=True, 91 | text=True, 92 | check=True, 93 | cwd=cwd, 94 | ) 95 | 96 | diff_preview = diff_result.stdout[:1500] 97 | analysis = textwrap.dedent(f""" 98 | ## Git Change Analysis for Conventional Commit Message 99 | 100 | ### Changed Files: 101 | {files_result.stdout} 102 | 103 | ### File Status Summary: 104 | {status_result.stdout} 105 | 106 | ### Diff Preview (first 1500 chars): 107 | {diff_preview} 108 | 109 | ### User Preferences: 110 | - Requested commit type: {commit_type or "auto-detect based on changes"} 111 | - Requested scope: {scope or "auto-detect based on files changed"} 112 | 113 | ### Task 114 | Write a Conventional Commit message for the STAGED changes only. 115 | 116 | ### Output format (return ONLY this) 117 | First line: type(scope): subject 118 | Add blank line before body 119 | Body paragraphs, each line <= 72 chars; bullets in body starting with "- " 120 | Optional footers (each on its own line), e.g.: 121 | BREAKING CHANGE: description 122 | 123 | ### Example generated commit message 124 | feat(core): add new feature 125 | 126 | - Implement new feature in core module 127 | - Update documentation 128 | 129 | BREAKING CHANGE: this change removes the old API method 130 | 131 | ### Rules 132 | - If commit_type or scope is provided above, USE THEM as-is. 133 | - If not provided, infer an appropriate type and a concise scope (or omit scope if unclear). 134 | - Subject: use imperative mood, start lowercase, no trailing period, <= 50 chars. 135 | - Body: use imperative mood (e.g. Update, Add etc.); explain WHAT and WHY, wrap at 72 chars; omit if subject suffices. 136 | - Use domain-specific terms; avoid generic phrases. 137 | - Do NOT mention "staged", "diff", or counts of files/lines. 138 | - Do NOT include markdown headers, code fences, or extra commentary. 139 | - Prefer a broad scope if many files; derive scope from top-level dirs when clear. 140 | - If there is a breaking change (e.g., API removal/rename), add a BREAKING CHANGE footer. 141 | - Keep the response to ONLY the commit message in the format above. 142 | 143 | ### Common types 144 | feat, fix, docs, style, refactor, perf, build, ci, test, chore, revert 145 | """) 146 | 147 | return analysis.strip() 148 | 149 | except subprocess.CalledProcessError as e: 150 | error_msg = e.stderr or e.stdout or str(e) 151 | return f"Git command failed: {error_msg}" 152 | except FileNotFoundError: 153 | return "Git is not installed or not found in PATH" 154 | except OSError as e: 155 | return f"OS error occurred: {str(e)}" 156 | 157 | 158 | def _parse_git_status_line(line): 159 | """ 160 | Helper to parse a single git status line. 161 | Returns (staged_file, unstaged_file, untracked_file) 162 | """ 163 | if len(line) < 3: 164 | return None, None, None 165 | staged_status = line[0] 166 | unstaged_status = line[1] 167 | filename = line[3:] 168 | if staged_status == "?" and unstaged_status == "?": 169 | return None, None, filename 170 | staged_file = filename if staged_status != " " else None 171 | unstaged_file = filename if unstaged_status != " " else None 172 | return staged_file, unstaged_file, None 173 | 174 | 175 | def _parse_git_status_lines(status_lines): 176 | staged_files = [] 177 | unstaged_files = [] 178 | untracked_files = [] 179 | for line in status_lines: 180 | staged_file, unstaged_file, untracked_file = _parse_git_status_line(line) 181 | if staged_file: 182 | staged_files.append(staged_file) 183 | if unstaged_file: 184 | unstaged_files.append(unstaged_file) 185 | if untracked_file: 186 | untracked_files.append(untracked_file) 187 | return staged_files, unstaged_files, untracked_files 188 | 189 | 190 | @mcp.tool() 191 | def check_git_status(repo_path: Optional[str] = None) -> str: 192 | """ 193 | Check the current git repository status. 194 | 195 | Args: 196 | repo_path: Optional path to the target git repository. If not provided, uses the current working directory. 197 | 198 | Returns: 199 | Current git status including staged, unstaged, and untracked files 200 | """ 201 | try: 202 | valid_repo_path = _get_valid_repo_path(repo_path) 203 | if not valid_repo_path: 204 | return f"Path '{repo_path or os.getcwd()}' is not a valid git repository." 205 | cwd = valid_repo_path 206 | # Get full git status 207 | status_result = subprocess.run( 208 | ["git", "status", "--porcelain"], 209 | capture_output=True, 210 | text=True, 211 | check=True, 212 | cwd=cwd, 213 | ) 214 | 215 | # Get branch info 216 | branch_result = subprocess.run( 217 | ["git", "branch", "--show-current"], 218 | capture_output=True, 219 | text=True, 220 | check=True, 221 | cwd=cwd, 222 | ) 223 | 224 | current_branch = branch_result.stdout.strip() 225 | 226 | if not status_result.stdout.strip(): 227 | return f"Repository is clean on branch '{current_branch}'. No changes to commit." 228 | 229 | # Parse status using helper 230 | status_lines = [line for line in status_result.stdout.split("\n") if line] 231 | staged_files, unstaged_files, untracked_files = _parse_git_status_lines( 232 | status_lines 233 | ) 234 | 235 | status_summary = f"Current branch: {current_branch}\n\n" 236 | 237 | if staged_files: 238 | status_summary += "Staged files (ready to commit):\n" 239 | status_summary += "\n".join(f" {file}" for file in staged_files) 240 | status_summary += "\n\n" 241 | 242 | if unstaged_files: 243 | status_summary += "Unstaged files (need to be added):\n" 244 | status_summary += "\n".join(f" {file}" for file in unstaged_files) 245 | status_summary += "\n\n" 246 | 247 | if untracked_files: 248 | status_summary += "Untracked files:\n" 249 | status_summary += "\n".join(f" {file}" for file in untracked_files) 250 | status_summary += "\n\n" 251 | 252 | if staged_files: 253 | status_summary += "✓ Ready to generate commit message!" 254 | else: 255 | status_summary += ( 256 | "ℹ Stage some files with 'git add' to generate commit messages." 257 | ) 258 | 259 | return status_summary 260 | 261 | except subprocess.CalledProcessError as e: 262 | error_msg = e.stderr or e.stdout or str(e) 263 | return f"Git command failed: {error_msg}" 264 | except FileNotFoundError: 265 | return "Git is not installed or not found in PATH" 266 | except OSError as e: 267 | return f"OS error occurred: {str(e)}" 268 | 269 | 270 | if __name__ == "__main__": 271 | mcp.run() 272 | ```