# 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: -------------------------------------------------------------------------------- ``` 3.13.5 ``` -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- ``` __pycache__/ *.pyc *.pyo *.pyd *.egg-info/ build/ dist/ .git/ .vscode/ .env .venv *.log uv.lock.bak ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` # Python-generated files __pycache__/ *.py[oc] build/ dist/ wheels/ *.egg-info # Virtual environments .venv # others node_modules/ .DS_Store ``` -------------------------------------------------------------------------------- /.markdownlint.jsonc: -------------------------------------------------------------------------------- ``` { "MD033": { "allowed_elements": ["br", "details", "summary"] }, "MD013": { "line_length": 120, "code_blocks": false, "tables": false } } ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # MCP Git Commit Generator [](https://pypi.org/project/mcp-git-commit-generator/) [](https://github.com/theoklitosBam7/mcp-git-commit-generator/releases) [](https://github.com/theoklitosBam7/mcp-git-commit-generator/actions/workflows/pypi-publish.yml) [](https://github.com/theoklitosBam7/mcp-git-commit-generator/actions/workflows/docker-publish.yml) [](https://github.com/theoklitosBam7/mcp-git-commit-generator/blob/main/LICENSE) Generate conventional commit messages from your staged git changes using Model Context Protocol (MCP). ## ✨ Features - **🤖 Automatic commit message generation** based on staged git diffs - **📝 Conventional Commits** support with auto-detection of type and scope - **🔄 Multiple transport options** - stdio (default) and SSE for different use cases - **🔍 Inspector UI** for interactive testing and debugging - **🐳 Docker support** with pre-built images for easy deployment - **⚡ Cross-platform** - works on macOS, Linux, and Windows ## 📦 Requirements - **For Docker usage**: [Docker](https://www.docker.com/) (for running the server in a container) - **For PyPI/uvx usage**: [Python](https://www.python.org/) >= 3.13.5 and [uv](https://github.com/astral-sh/uv) (recommended) or pip - [Git](https://git-scm.com/) (for version control) - An MCP-compatible client (VS Code with MCP extension, Claude Desktop, Cursor, Windsurf, etc.) ## 🚀 Installation You can install and use the MCP Git Commit Generator in multiple ways: ### Option 1: Using uvx (Recommended) The easiest way to use the package is with `uvx`, which automatically manages the virtual environment: ```sh uvx mcp-git-commit-generator ``` ### Option 2: Install from PyPI ```sh pip install mcp-git-commit-generator ``` Or with uv: ```sh uv pip install mcp-git-commit-generator ``` ### Option 3: Using Docker Use the pre-built Docker image from GitHub Container Registry (no installation required): ```sh docker run -i --rm --mount type=bind,src=${HOME},dst=${HOME} ghcr.io/theoklitosbam7/mcp-git-commit-generator:latest ``` ## 🛠️ Available Tools This MCP server provides the following tools to help you generate conventional commit messages: ### `generate_commit_message` Generates a conventional commit message based on your staged git changes. **Parameters:** - `repo_path` (string, optional): Path to the git repository. If omitted, uses the current directory. - `commit_type` (string, optional): Conventional commit type (e.g., `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `build`, `ci`, `test`, `chore`, `revert`). If omitted, the type will be auto-detected. - `scope` (string, optional): Scope of the change (e.g., file or module name). If omitted, the scope will be auto-detected based on changed files. **Usage:** 1. Stage your changes: `git add <files>` 2. Use the tool through your MCP client to generate a commit message 3. The tool will analyze your staged changes and generate an appropriate conventional commit message ### `check_git_status` Checks the current git repository status, including staged, unstaged, and untracked files. **Parameters:** - `repo_path` (string, optional): Path to the git repository. If omitted, uses the current directory. **Usage:** Use this tool to get an overview of your current git repository state before generating commit messages. ## 🧩 MCP Client Configuration Configure the MCP Git Commit Generator in your favorite MCP client. You have multiple options: 1. **Using uvx** (recommended - automatically manages dependencies) 2. **Using Docker** (no local Python installation required) 3. **Using local Python installation** (for development) ### VS Code Add one of the following configurations to your VS Code `mcp.json` file (usually located at `.vscode/mcp.json` in your workspace): #### Using uvx (Recommended) ```jsonc { "servers": { "mcp-git-commit-generator": { "command": "uvx", "args": ["mcp-git-commit-generator"] } } } ``` #### Using Docker ```jsonc { "servers": { "mcp-git-commit-generator": { "command": "docker", "args": [ "run", "-i", "--rm", "--mount", "type=bind,src=${userHome},dst=${userHome}", "ghcr.io/theoklitosbam7/mcp-git-commit-generator:latest" ] } } } ``` If you want to put the configuration in your user `settings.json` file, you can do so by adding: ```jsonc { "mcp": { "servers": { "mcp-git-commit-generator": { "command": "uvx", "args": ["mcp-git-commit-generator"] } } } } ``` ### Cursor Add one of the following to your Cursor MCP configuration file (usually located at `~/.cursor/mcp.json`): #### Cursor with uvx (Recommended) ```jsonc { "mcpServers": { "mcp-git-commit-generator": { "command": "uvx", "args": ["mcp-git-commit-generator"] } } } ``` #### Cursor with Docker ```jsonc { "mcpServers": { "mcp-git-commit-generator": { "command": "docker", "args": [ "run", "-i", "--rm", "--mount", "type=bind,src=${userHome},dst=${userHome}", "ghcr.io/theoklitosbam7/mcp-git-commit-generator:latest" ] } } } ``` ### Windsurf Configure Windsurf with one of the following MCP server settings (usually located at `~/.codeium/windsurf/mcp_config.json`): #### Windsurf with uvx (Recommended) ```jsonc { "mcpServers": { "mcp-git-commit-generator": { "command": "uvx", "args": ["mcp-git-commit-generator"] } } } ``` #### Windsurf with Docker ```jsonc { "mcpServers": { "mcp-git-commit-generator": { "command": "docker", "args": [ "run", "-i", "--rm", "--mount", "type=bind,src=${userHome},dst=${userHome}", "ghcr.io/theoklitosbam7/mcp-git-commit-generator:latest" ] } } } ``` ### Claude Desktop Add one of the following to your Claude Desktop configuration file (usually located at `~/Library/Application Support/Claude/claude_desktop_config.json` on macOS): #### Claude Desktop with uvx (Recommended) ```jsonc { "mcpServers": { "mcp-git-commit-generator": { "command": "uvx", "args": ["mcp-git-commit-generator"] } } } ``` #### Claude Desktop with Docker ```jsonc { "mcpServers": { "mcp-git-commit-generator": { "command": "docker", "args": [ "run", "-i", "--rm", "--mount", "type=bind,src=${userHome},dst=${userHome}", "ghcr.io/theoklitosbam7/mcp-git-commit-generator:latest" ] } } } ``` > **Note**: The `--mount` option in Docker configurations allows the Docker container to access your home > directory, enabling it to work with git repositories located anywhere in your file system. When using uvx or pip > installations, this mounting is not needed as the tool runs directly on your system. Adjust the mount path if your > repositories are located elsewhere when using Docker. ## 🚀 Quick Start Guide 1. **Install the package** using one of the methods above: - **Recommended**: `uvx mcp-git-commit-generator` (or configure in your MCP client) - **Alternative**: `pip install mcp-git-commit-generator` - **Docker**: Use the configurations above with Docker 2. **Configure your MCP client** using one of the configurations above 3. **Stage some changes** in a git repository: ```sh git add <files> ``` 4. **Use the tools** through your MCP client: - Use `check_git_status` to see your current repository state - Use `generate_commit_message` to create a conventional commit message 5. **Commit your changes** with the generated message --- ## 👨💻 Developer Guidelines The following sections are intended for developers who want to contribute to or modify the MCP Git Commit Generator. ### Local Development Setup 🛠️ If you prefer not to use Docker for development, you can run the server locally: **Requirements:** - [Python](https://www.python.org/) >= 3.13.5 - [MCP CLI](https://pypi.org/project/mcp/) >= 1.10.1 - [uv](https://github.com/astral-sh/uv) (for dependency management, optional but recommended) - [Node.js](https://nodejs.org/en) (for Inspector UI, optional) - [Python Debugger Extension](https://marketplace.visualstudio.com/items?itemName=ms-python.debugpy) (for debugging, optional) **Installation:** 1. **Clone the repository:** ```sh git clone https://github.com/theoklitosBam7/mcp-git-commit-generator.git cd mcp-git-commit-generator ``` 2. **Prepare environment:** There are two approaches to set up the environment for this project. You can choose either one based on your preference. > Note: Reload VSCode or terminal to ensure the virtual environment python is used after creating the virtual environment. | Approach | Steps | | -------- | ----- | | 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 .`. | | 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`. | 3. **(Optional) Install Inspector dependencies:** ```sh cd inspector npm install ``` ### 📦 Publishing to PyPI The project includes an automated PyPI publishing workflow (`.github/workflows/pypi-publish.yml`) that: - **Triggers on**: Tag pushes matching `v*.*.*` pattern, manual workflow dispatch, or pull requests to main - **Builds**: Python package distributions using the `build` package - **Publishes**: Automatically publishes to PyPI using trusted publishing (OIDC) when tags are pushed To publish a new version: 1. Update the version in `pyproject.toml` 2. Create and push a git tag: `git tag vX.Y.Z && git push origin vX.Y.Z` 3. The workflow will automatically build and publish to PyPI ### 🐳 Building and Running with Docker You can build and run the MCP Git Commit Generator using Docker. The provided Dockerfile uses a multi-stage build with [`uv`](https://github.com/astral-sh/uv) for dependency management and runs the server as a non-root user for security. #### Build the Docker image ```sh docker build -t mcp-git-commit-generator . ``` #### Run the server in a container (default: stdio transport) You can run the published image directly from GitHub Container Registry. ```sh docker run -d \ --name mcp-git-commit-generator \ ghcr.io/theoklitosbam7/mcp-git-commit-generator:latest ``` By default, the container runs: ```sh mcp-git-commit-generator --transport stdio ``` If you want to use SSE transport (for Inspector UI or remote access), override the entrypoint or run manually: ```sh docker run -d \ --name mcp-git-commit-generator \ -p 3001:3001 \ --entrypoint mcp-git-commit-generator \ ghcr.io/theoklitosbam7/mcp-git-commit-generator:latest --transport sse --host 0.0.0.0 --port 3001 ``` The server will be available at `http://localhost:3001` when using SSE. ### 🖥️ Running the Server Locally **To run locally (without Docker):** 1. Set up your uv or Python environment as described in the Local Development Setup section. 2. From the project root, run: <details> <summary>mcp-git-commit-generator</summary> ```sh # If you have mcp-git-commit-generator installed in your environment (default: stdio) mcp-git-commit-generator ``` </details> <details> <summary>mcp-git-commit-generator with SSE transport</summary> ```sh mcp-git-commit-generator --transport sse ``` </details> <details> <summary>Using uv</summary> ```sh uv run -m mcp_git_commit_generator --transport sse ``` </details> <details> <summary>Using Python directly</summary> ```sh python -m mcp_git_commit_generator --transport sse ``` </details> <br/> You can specify other options, for example: ```sh python -m mcp_git_commit_generator --transport sse --host 0.0.0.0 --port 3001 -v ``` > The server listens on `0.0.0.0:3001` by default when using SSE, or as specified by the options above. **Note:** - If you want to use the CLI entrypoint, ensure the package is installed and your environment is activated. - Do not use positional arguments (e.g., `python -m mcp_git_commit_generator sse`); always use options like `--transport sse`. - Available arguments with their values are: - `--transport`: Transport type (e.g., `stdio` (default), `sse`). - `--host`: Host to bind the server (default: `0.0.0.0`). - `--port`: Port to bind the server (default: `3001`). - `-v`, `--verbose`: Verbosity level (e.g., `-v`, `-vv`). ### 🔎 Start the Inspector UI From the `inspector` directory: ```sh npm run dev:inspector ``` > The Inspector UI will be available at `http://localhost:5173`. ### 🧪 Running Tests The project includes comprehensive unit tests to ensure reliability: ```sh # Run all tests pytest # Run tests with verbose output pytest -v # Run tests with coverage pytest --cov=src/mcp_git_commit_generator # Run specific test file pytest tests/test_server.py ``` **Test Coverage:** - ✅ Tool validation with invalid repository paths - ✅ Staged and unstaged change detection - ✅ Git status reporting - ✅ Commit message generation workflows - ✅ Error handling for git command failures ### 🗂️ Project Structure ```sh . ├── .github/ # GitHub workflows and issue templates ├── .gitignore ├── .markdownlint.jsonc ├── .python-version ├── .vscode/ # VSCode configuration ├── LICENSE ├── README.md ├── pyproject.toml # Python project configuration ├── requirements-dev.txt # Development dependencies ├── uv.lock # Python dependencies lock file ├── Dockerfile # Docker build file ├── build/ # Build artifacts ├── src/ # Python source code │ └── mcp_git_commit_generator/ │ ├── __init__.py # Main entry point │ ├── __main__.py # CLI entry point │ └── server.py # Main server implementation └── inspector/ # Inspector related files ├── package.json # Node.js dependencies └── package-lock.json ``` ### ⚙️ Advanced MCP Server Configuration for Development The `.vscode/mcp.json` file configures how VS Code and related tools connect to your MCP Git Commit Generator server. This file defines available server transports and their connection details, making it easy to switch between different modes (stdio is default, SSE is optional) for development and debugging. #### Example Development `mcp.json` ```jsonc { "servers": { "mcp-git-commit-generator": { "command": "docker", "args": [ "run", "-i", "--rm", "--mount", "type=bind,src=${userHome},dst=${userHome}", "ghcr.io/theoklitosbam7/mcp-git-commit-generator:latest" ] }, "sse-mcp-git-commit-generator": { "type": "sse", "url": "http://localhost:3001/sse" }, "stdio-mcp-git-commit-generator": { "type": "stdio", "command": "${command:python.interpreterPath}", "args": ["-m", "mcp_git_commit_generator", "--transport", "stdio"] }, "uvx-mcp-git-commit-generator": { "command": "uvx", "args": ["mcp-git-commit-generator"] } } } ``` - **mcp-git-commit-generator**: Runs the server in a Docker container (default: stdio transport), using the published image. - **sse-mcp-git-commit-generator**: Connects to the MCP server using Server-Sent Events (SSE) at `http://localhost:3001/sse`. Only useful if you run the server with `--transport sse`. - **stdio-mcp-git-commit-generator**: Connects using standard input/output (stdio), running the server as a subprocess. This is the default and recommended for local development and debugging. - **uvx-mcp-git-commit-generator**: Uses uvx to automatically install and run the package from PyPI. ### 🐞 Debugging the MCP Server > Notes: > > - [MCP Inspector](https://github.com/modelcontextprotocol/inspector) is a visual developer tool for testing and debugging MCP servers. > - All debugging modes support breakpoints, so you can add breakpoints to the tool implementation code. > - **You can test tool arguments directly in the Inspector UI**: When using the Inspector, select a tool and provide arguments in the input fields to simulate real usage and debug argument handling. | Debug Mode | Description | Steps to debug | | ---------- | ----------- | --------------- | | 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> | ### ⚙️ Default Ports and Customizations | Debug Mode | Ports | Definitions | Customizations | Note | | ---------- | ----- | ------------ | -------------- |-------------- | | 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 | ## 💬 Feedback 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) ## 📖 Troubleshooting ### Common Issues - **"Path is not a valid git repository"**: Ensure you're in a directory with a `.git` folder - **"No staged changes found"**: Run `git add <files>` to stage your changes first - **"Git is not installed"**: Install Git from [git-scm.com](https://git-scm.com/) - **Docker permission issues**: Ensure Docker can access your home directory - **MCP connection fails**: Verify your client configuration matches the examples above ### Getting Help - Check the [Issues page](https://github.com/theoklitosBam7/mcp-git-commit-generator/issues) for solutions - Use the Inspector UI for interactive debugging - Run `pytest -v` to verify your installation ## 📄 License [MIT](./LICENSE) License © 2025 [Theoklitos Bampouris](https://github.com/theoklitosBam7) ``` -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- ``` # requirements-dev.txt only for pip compatibility debugpy>=1.8.14 pre-commit>=4.2.0 pytest>=8.4.1 ``` -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- ```json { "python.testing.pytestArgs": ["tests"], "python.testing.unittestEnabled": false, "python.testing.pytestEnabled": true } ``` -------------------------------------------------------------------------------- /src/mcp_git_commit_generator/__main__.py: -------------------------------------------------------------------------------- ```python """Generate conventional commit messages from your staged git changes using Model Context Protocol (MCP).""" from mcp_git_commit_generator import main main() ``` -------------------------------------------------------------------------------- /inspector/package.json: -------------------------------------------------------------------------------- ```json { "name": "inspector-placeholder", "version": "0.0.1", "description": "This is just a placeholder to launch the inspector.", "private": true, "scripts": { "dev:inspector": "CLIENT_PORT=5173 SERVER_PORT=3000 mcp-inspector" }, "devDependencies": { "@modelcontextprotocol/inspector": "^0.15.0" } } ``` -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- ```markdown --- name: Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ``` -------------------------------------------------------------------------------- /.vscode/mcp.json: -------------------------------------------------------------------------------- ```json { "servers": { "mcp-git-commit-generator": { "command": "docker", "args": [ "run", "-i", "--rm", "--mount", "type=bind,src=${userHome},dst=${userHome}", "ghcr.io/theoklitosbam7/mcp-git-commit-generator:latest" ] }, "sse-mcp-git-commit-generator": { "type": "sse", "url": "http://localhost:3001/sse" }, "stdio-mcp-git-commit-generator": { "type": "stdio", "command": "${command:python.interpreterPath}", "args": ["-m", "mcp_git_commit_generator", "--transport", "stdio"] }, "uvx-mcp-git-commit-generator": { "command": "uvx", "args": ["mcp-git-commit-generator"] } } } ``` -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- ```markdown --- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - OS: [e.g. iOS] - Browser [e.g. chrome, safari] - Version [e.g. 22] **Smartphone (please complete the following information):** - Device: [e.g. iPhone6] - OS: [e.g. iOS8.1] - Browser [e.g. stock browser, safari] - Version [e.g. 22] **Additional context** Add any other context about the problem here. ``` -------------------------------------------------------------------------------- /.github/actions/test-python/action.yml: -------------------------------------------------------------------------------- ```yaml name: test-python description: "Runs Python tests for the MCP Git Commit Generator project" runs: using: "composite" steps: - name: Set up Python for tests uses: actions/setup-python@v5 with: python-version: "3.13.5" - name: Cache pip uses: actions/cache@v4 with: path: | ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements-dev.txt') }} restore-keys: | ${{ runner.os }}-pip- - name: Configure git user for CI run: | git config --global user.email "[email protected]" git config --global user.name "CI Runner" shell: bash - name: Install test dependencies run: | python -m pip install --upgrade pip python -m pip install -e . if [ -f requirements-dev.txt ]; then python -m pip install -r requirements-dev.txt; fi shell: bash - name: Run tests run: | pytest -q shell: bash ``` -------------------------------------------------------------------------------- /.github/workflows/pypi-publish.yml: -------------------------------------------------------------------------------- ```yaml name: Publish Python 🐍 package to PyPI on: workflow_dispatch: push: tags: - "v*.*.*" pull_request: branches: - "main" jobs: release-build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Run Python tests uses: ./.github/actions/test-python - name: Build release distributions run: | python -m pip install build python -m build - name: Upload distributions uses: actions/upload-artifact@v4 with: name: release-dists path: dist/ pypi-publish: if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') runs-on: ubuntu-latest needs: - release-build permissions: contents: read id-token: write environment: name: pypi steps: - name: Retrieve release distributions uses: actions/download-artifact@v4 with: name: release-dists path: dist/ - name: Publish release distributions to PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: packages-dir: dist/ ``` -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- ```toml [project] name = "mcp-git-commit-generator" version = "2.1.0" description = "Generate conventional commit messages from your staged git changes using Model Context Protocol (MCP)." authors = [{ name = "Theoklitos Bampouris", email = "[email protected]" }] license = { text = "MIT" } maintainers = [ { name = "Theoklitos Bampouris", email = "[email protected]" }, ] keywords = [ "mcp", "model context protocol", "git", "commit", "conventional commits", "conventional commit messages", ] readme = "README.md" requires-python = ">=3.13.5" classifiers = [ "Programming Language :: Python :: 3", "Operating System :: OS Independent", ] dependencies = ["click>=8.2.1", "mcp[cli]>=1.10.1"] [dependency-groups] dev = ["debugpy>=1.8.14", "pre-commit>=4.2.0", "pytest>=8.4.1"] [project.scripts] mcp-git-commit-generator = "mcp_git_commit_generator:main" [project.urls] Homepage = "https://github.com/theoklitosBam7/mcp-git-commit-generator" Issues = "https://github.com/theoklitosBam7/mcp-git-commit-generator/issues" [build-system] requires = ["uv_build >= 0.7.19, <0.9.0"] build-backend = "uv_build" [tool.pytest.ini_options] pythonpath = ["src"] testpaths = ["tests"] ``` -------------------------------------------------------------------------------- /.github/workflows/docker-publish.yml: -------------------------------------------------------------------------------- ```yaml name: Create and Publish Docker 🐳 image on: workflow_dispatch: push: tags: - "v*.*.*" pull_request: branches: - "main" env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} jobs: build-and-push: runs-on: ubuntu-latest permissions: contents: read packages: write steps: - name: Checkout repository uses: actions/checkout@v4 - name: Run Python tests uses: ./.github/actions/test-python - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Extract metadata (tags, labels) for Docker id: meta uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - name: Log in to GitHub Container Registry uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push Docker image uses: docker/build-push-action@v6 with: context: . push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} platforms: linux/amd64,linux/arm64 ``` -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- ```json { "version": "0.2.0", "configurations": [ { "name": "Attach to Local MCP", "type": "debugpy", "request": "attach", "connect": { "host": "localhost", "port": 5678 }, "presentation": { "hidden": true }, "internalConsoleOptions": "neverOpen", "postDebugTask": "Terminate All Tasks" }, { "name": "Launch Inspector (Edge)", "type": "msedge", "request": "launch", "url": "http://localhost:5173?timeout=60000&serverUrl=http://localhost:3001/sse#tools", "cascadeTerminateToConfigurations": ["Attach to Local MCP"], "presentation": { "hidden": true }, "internalConsoleOptions": "neverOpen" }, { "name": "Launch Inspector (Chrome)", "type": "chrome", "request": "launch", "url": "http://localhost:5173?timeout=60000&serverUrl=http://localhost:3001/sse#tools", "cascadeTerminateToConfigurations": ["Attach to Local MCP"], "presentation": { "hidden": true }, "internalConsoleOptions": "neverOpen" } ], "compounds": [ { "name": "Debug in Inspector (Edge)", "configurations": ["Launch Inspector (Edge)", "Attach to Local MCP"], "preLaunchTask": "Start MCP Inspector", "stopAll": true }, { "name": "Debug in Inspector (Chrome)", "configurations": ["Launch Inspector (Chrome)", "Attach to Local MCP"], "preLaunchTask": "Start MCP Inspector", "stopAll": true } ] } ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- ```dockerfile # Use a Python image with uv pre-installed FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim AS uv LABEL org.opencontainers.image.source="https://github.com/theoklitosBam7/mcp-git-commit-generator" LABEL org.opencontainers.image.description="Generate conventional commit messages from your staged git changes using Model Context Protocol (MCP)." LABEL org.opencontainers.image.licenses=MIT # Install the project into `/app` WORKDIR /app # First copy only the required files for dependency installation COPY pyproject.toml uv.lock ./ # Enable bytecode compilation ENV UV_COMPILE_BYTECODE=1 # Copy from the cache instead of linking since it's a mounted volume ENV UV_LINK_MODE=copy # Install the project's dependencies using the lockfile and settings RUN --mount=type=cache,target=/root/.cache/uv \ uv sync --frozen --no-install-project --no-dev --no-editable # Then, add the rest of the project source code and install it # Installing separately from its dependencies allows optimal layer caching COPY . /app RUN --mount=type=cache,target=/root/.cache/uv \ uv sync --frozen --no-dev --no-editable RUN uv pip install . FROM python:3.13-slim-bookworm LABEL org.opencontainers.image.source="https://github.com/theoklitosBam7/mcp-git-commit-generator" LABEL org.opencontainers.image.description="Generate conventional commit messages from your staged git changes using Model Context Protocol (MCP)." LABEL org.opencontainers.image.licenses=MIT WORKDIR /app RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/* \ && adduser --disabled-password --gecos '' --uid 1000 appuser COPY --from=uv --chown=appuser:appuser /app/.venv /app/.venv ENV PATH="/app/.venv/bin:$PATH" ENV PYTHONUNBUFFERED=1 RUN chown -R appuser:appuser /app USER appuser ENTRYPOINT ["mcp-git-commit-generator", "--transport", "stdio"] ``` -------------------------------------------------------------------------------- /src/mcp_git_commit_generator/__init__.py: -------------------------------------------------------------------------------- ```python """Main entry point for the MCP server.""" import logging import os import sys from typing import Literal, cast import click from .server import mcp @click.command() @click.option( "--transport", default="stdio", type=click.Choice(["stdio", "sse"]), help="Transport type (stdio or sse)", ) @click.option("--host", default="0.0.0.0", help="Host to listen on (for sse)") @click.option("--port", default=3001, type=int, help="Port to listen on (for sse)") @click.option( "-v", "--verbose", count=True, help="Verbosity level, use -v or -vv", ) def main( transport, host, port, verbose, ): """ Generate conventional commit messages from your staged git changes using Model Context Protocol (MCP). """ logging_level = logging.WARN if verbose == 1: logging_level = logging.INFO elif verbose >= 2: logging_level = logging.DEBUG logging.basicConfig(level=logging_level, stream=sys.stderr) logger = logging.getLogger(__name__) logger.info( "Starting MCP server with transport type: %s, verbosity level: %d", transport, verbose, ) allowed_levels = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"} env_log_level = cast( Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], os.environ.get("LOG_LEVEL", "DEBUG"), ) mcp.settings.log_level = ( env_log_level if env_log_level in allowed_levels else "DEBUG" ) if transport == "sse": mcp.settings.port = port mcp.settings.host = host mcp.run(transport="sse") elif transport == "stdio": mcp.run(transport="stdio") else: logger.error("Invalid transport type. Use 'sse' or 'stdio'.") sys.exit(1) if __name__ == "__main__": main(transport="stdio", host=None, port=None, verbose=False) ``` -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- ```json { "version": "2.0.0", "tasks": [ { "label": "Start MCP Server", "type": "shell", "command": "${command:python.interpreterPath}", "args": [ "-m", "debugpy", "--listen", "localhost:5678", "-m", "mcp_git_commit_generator", "--transport", "sse" ], "isBackground": true, "options": { "cwd": "${workspaceFolder}", "env": { "PORT": "3001" } }, "problemMatcher": { "pattern": [ { "regexp": "^.*$", "file": 0, "location": 1, "message": 2 } ], "background": { "activeOnStart": true, "beginsPattern": ".*", "endsPattern": "Application startup complete|running" } } }, { "label": "Start MCP Inspector", "type": "shell", "command": "npm run dev:inspector", "isBackground": true, "options": { "cwd": "${workspaceFolder}/inspector", "env": { "CLIENT_PORT": "5173", "SERVER_PORT": "3000" } }, "problemMatcher": { "pattern": [ { "regexp": "^.*$", "file": 0, "location": 1, "message": 2 } ], "background": { "activeOnStart": true, "beginsPattern": "Starting MCP inspector", "endsPattern": "⚙️ Proxy server listening on" } }, "dependsOn": ["Start MCP Server"] }, { "label": "Terminate All Tasks", "command": "echo 'Terminating all tasks...'", "type": "shell", "problemMatcher": [] } ], "inputs": [ { "id": "terminate", "type": "command", "command": "workbench.action.tasks.terminate", "args": "terminateAll" } ] } ``` -------------------------------------------------------------------------------- /.github/copilot-instructions.md: -------------------------------------------------------------------------------- ```markdown # MCP Git Commit Generator: AI Agent Guide ## Project Overview This MCP server generates conventional commit messages from staged git changes. It exposes two main MCP tools: - `generate_commit_message`: Analyzes staged changes and generates a conventional commit message. Commit type and scope can be auto-detected or provided. - `check_git_status`: Reports staged, unstaged, and untracked files with clear validation of the git repository path. ## Architecture & Key Files - **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. - **CLI Entrypoint**: `src/mcp_git_commit_generator/__init__.py` enforces strict `--option value` usage for all CLI arguments (never use positional args). - **Module Launcher**: `src/mcp_git_commit_generator/__main__.py` bootstraps the server for `python -m` and CLI use. - **Inspector UI**: The `inspector/` directory contains a Node.js-based tool for interactive MCP tool testing (`npm run dev:inspector`). ## Developer Workflow & Commands ### Environment Setup - **Recommended**: `uv venv` → `uv pip install -r pyproject.toml --group dev` → `uv pip install -e .` - **Alternative**: `python -m venv .venv` → `pip install -e . && pip install -r requirements-dev.txt` ### Running & Debugging - **VS Code Tasks**: Use "Start MCP Server" (SSE, port 5678) and "Start MCP Inspector" for local development. Inspector UI runs at `http://localhost:5173`. - **Local CLI**: Run with `mcp-git-commit-generator --transport sse` or `python -m mcp_git_commit_generator --transport sse` (always use `--option value`). - **Docker**: Build with `docker build -t mcp-git-commit-generator .`. Run with: - Default: `docker run -i --rm --mount type=bind,src=${HOME},dst=${HOME} mcp-git-commit-generator` - SSE: `docker run -d -p 3001:3001 --entrypoint mcp-git-commit-generator ... --transport sse --host 0.0.0.0 --port 3001` ### Inspector UI Workflow 1. Start the server (see above) 2. From `inspector/`, run `npm run dev:inspector` 3. Open `http://localhost:5173` and connect to the running MCP server 4. Use the UI to list, invoke, and debug tools interactively ## Project-Specific Conventions & Patterns - **CLI Usage**: Always use `--option value` for all arguments. Positional arguments are not supported and will error. - **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`. - **Git Operations**: Always validate the repo path with `_get_valid_repo_path`. Use `subprocess.run` for all git commands, with structured error handling. - **Testing**: Run tests with `pytest`, e.g. `pytest -v` or `pytest --cov=src/mcp_git_commit_generator`. - **Versioning**: Update `pyproject.toml` for releases. Docker images are published on tag pushes starting with "v". ## Integration Points & Communication - **MCP Clients**: Integrates with VS Code, Cursor, Windsurf, and Claude Desktop. See README for config examples. - **Transports**: Supports both `stdio` and `sse` (for Inspector UI and remote access). Match your client config to the server mode. ## Example Usage 1. Stage changes: `git add <files>` 2. Run `check_git_status` to verify the current git state 3. Generate a commit message with `generate_commit_message` 4. Commit using the generated message ## Key Patterns for AI Agents - **Focus on Key Files**: Use `server.py` for tool logic, `__init__.py` for CLI conventions - **Agent Guidance**: When building new features, follow the code and docstring patterns in this guide. Use inline comments like `// ...existing code...` to contextualize changes - **Integration**: Ensure new code fits the established patterns for git ops, Docker, and Inspector UI ## Feedback & Iteration Please review these instructions and provide feedback on any unclear or incomplete sections so they can be improved. ``` -------------------------------------------------------------------------------- /tests/test_server.py: -------------------------------------------------------------------------------- ```python """ Unit tests for the MCP Git Commit Generator server tools. """ import subprocess from mcp_git_commit_generator import server def test_generate_commit_message_invalid_repo(): """Test that an invalid repo path returns an error message.""" result = server.generate_commit_message(repo_path="/not/a/repo") assert "not a valid git repository" in result def test_check_git_status_invalid_repo(): """Test that an invalid repo path returns an error message for git status.""" result = server.check_git_status(repo_path="/not/a/repo") assert "not a valid git repository" in result def test_generate_commit_message_no_staged_changes(tmp_path): """Test that no staged changes returns the appropriate message.""" repo_dir = tmp_path / "repo" repo_dir.mkdir() subprocess.run(["git", "init"], cwd=repo_dir, check=True) result = server.generate_commit_message(repo_path=str(repo_dir)) assert "No staged changes found" in result def test_check_git_status_clean_repo(tmp_path): """Test that a clean repo returns the correct status message.""" repo_dir = tmp_path / "repo" repo_dir.mkdir() subprocess.run(["git", "init"], cwd=repo_dir, check=True) result = server.check_git_status(repo_path=str(repo_dir)) assert "No changes to commit" in result def test_generate_commit_message_with_staged_change(tmp_path): """Test that a staged file produces a commit message analysis.""" repo_dir = tmp_path / "repo" repo_dir.mkdir() subprocess.run(["git", "init"], cwd=repo_dir, check=True) file_path = repo_dir / "foo.txt" file_path.write_text("hello world\n") subprocess.run(["git", "add", "foo.txt"], cwd=repo_dir, check=True) result = server.generate_commit_message(repo_path=str(repo_dir)) assert "Git Change Analysis for Conventional Commit Message" in result assert "foo.txt" in result def test_check_git_status_with_staged_and_unstaged(tmp_path): """Test that both staged and unstaged changes are reported correctly.""" repo_dir = tmp_path / "repo" repo_dir.mkdir() subprocess.run(["git", "init"], cwd=repo_dir, check=True) file_path = repo_dir / "bar.txt" file_path.write_text("first\n") subprocess.run(["git", "add", "bar.txt"], cwd=repo_dir, check=True) file_path.write_text("second\n") result = server.check_git_status(repo_path=str(repo_dir)) assert "Staged files" in result assert "Unstaged files" in result assert "bar.txt" in result def test_check_git_status_with_untracked_files(tmp_path): """Test that untracked files are reported correctly.""" repo_dir = tmp_path / "repo" repo_dir.mkdir() subprocess.run(["git", "init"], cwd=repo_dir, check=True) file_path = repo_dir / "untracked.txt" file_path.write_text("untracked\n") result = server.check_git_status(repo_path=str(repo_dir)) assert "Untracked files" in result assert "untracked.txt" in result def test_generate_commit_message_multiple_files_staged(tmp_path): """Test commit message analysis with multiple files staged.""" repo_dir = tmp_path / "repo" repo_dir.mkdir() subprocess.run(["git", "init"], cwd=repo_dir, check=True) file1 = repo_dir / "a.txt" file2 = repo_dir / "b.txt" file1.write_text("A\n") file2.write_text("B\n") subprocess.run(["git", "add", "a.txt", "b.txt"], cwd=repo_dir, check=True) result = server.generate_commit_message(repo_path=str(repo_dir)) assert "a.txt" in result and "b.txt" in result def test_check_git_status_with_staged_deletion(tmp_path): """Test that staged file deletions are reported correctly.""" repo_dir = tmp_path / "repo" repo_dir.mkdir() subprocess.run(["git", "init"], cwd=repo_dir, check=True) file_path = repo_dir / "delete_me.txt" file_path.write_text("bye\n") subprocess.run(["git", "add", "delete_me.txt"], cwd=repo_dir, check=True) subprocess.run(["git", "commit", "-m", "add file"], cwd=repo_dir, check=True) subprocess.run(["git", "rm", "delete_me.txt"], cwd=repo_dir, check=True) result = server.check_git_status(repo_path=str(repo_dir)) print(result) assert "delete_me.txt" in result assert "Staged files" in result def test_generate_commit_message_with_type_and_scope(tmp_path): """Test generate_commit_message with explicit commit_type and scope.""" repo_dir = tmp_path / "repo" repo_dir.mkdir() subprocess.run(["git", "init"], cwd=repo_dir, check=True) file_path = repo_dir / "scoped.txt" file_path.write_text("scoped\n") subprocess.run(["git", "add", "scoped.txt"], cwd=repo_dir, check=True) result = server.generate_commit_message( repo_path=str(repo_dir), commit_type="feat", scope="core" ) assert "Requested commit type: feat" in result assert "Requested scope: core" in result def test_generate_commit_message_breaking_change_prompt(tmp_path): """Test that the breaking change footer instructions are present in the prompt.""" repo_dir = tmp_path / "repo" repo_dir.mkdir() subprocess.run(["git", "init"], cwd=repo_dir, check=True) file_path = repo_dir / "breaking.txt" file_path.write_text("breaking change\n") subprocess.run(["git", "add", "breaking.txt"], cwd=repo_dir, check=True) result = server.generate_commit_message(repo_path=str(repo_dir)) assert "BREAKING CHANGE" in result ``` -------------------------------------------------------------------------------- /src/mcp_git_commit_generator/server.py: -------------------------------------------------------------------------------- ```python """ FastMCP Server for generating conventional commit messages from git diff """ import logging import os import subprocess import textwrap from typing import Optional from mcp.server.fastmcp import FastMCP # Create the FastMCP server mcp = FastMCP("Git Commit Generator") def _get_valid_repo_path(repo_path: Optional[str]) -> Optional[str]: """ Resolve and validate the git repository path. Returns the valid repo path if valid, otherwise None. """ logger = logging.getLogger(__name__) # Resolve user tilde and symlinks to a canonical path resolved = ( os.path.realpath(os.path.expanduser(repo_path)) if repo_path else os.getcwd() ) logger.info("[get_valid_repo_path] Resolved repository path: %s", resolved) if not os.path.isdir(resolved) or not os.path.exists( os.path.join(resolved, ".git") ): return None return resolved @mcp.tool() def generate_commit_message( repo_path: Optional[str] = None, commit_type: Optional[str] = None, scope: Optional[str] = None, ) -> str: """ Prepare a structured analysis and instruction block for generating a Conventional Commit message from staged git changes only. Behavior: - Validates the repository path and operates on the provided repo or CWD. - Collects staged diff, porcelain status, and a name-status summary. - Incorporates optional user preferences for commit_type and scope. - Returns a single formatted string that includes context plus strict output instructions for an LLM to produce a Conventional Commit. Args: repo_path: Optional path to the target git repository. If not provided, uses the current working directory. commit_type: Optional commit type (feat, fix, docs, style, refactor, perf, build, ci, test, chore, revert) scope: Optional scope of the change Returns: A formatted prompt containing git change context and clear output rules for generating a Conventional Commit message """ try: valid_repo_path = _get_valid_repo_path(repo_path) if not valid_repo_path: return f"Path '{repo_path or os.getcwd()}' is not a valid git repository." cwd = valid_repo_path # Get staged changes diff_result = subprocess.run( ["git", "diff", "--cached"], capture_output=True, text=True, check=True, cwd=cwd, ) if not diff_result.stdout.strip(): return "No staged changes found. Please stage your changes with 'git add' first." # Get git status for context status_result = subprocess.run( ["git", "status", "--porcelain"], capture_output=True, text=True, check=True, cwd=cwd, ) # Get list of changed files for better analysis files_result = subprocess.run( ["git", "diff", "--cached", "--name-status"], capture_output=True, text=True, check=True, cwd=cwd, ) diff_preview = diff_result.stdout[:1500] analysis = textwrap.dedent(f""" ## Git Change Analysis for Conventional Commit Message ### Changed Files: {files_result.stdout} ### File Status Summary: {status_result.stdout} ### Diff Preview (first 1500 chars): {diff_preview} ### User Preferences: - Requested commit type: {commit_type or "auto-detect based on changes"} - Requested scope: {scope or "auto-detect based on files changed"} ### Task Write a Conventional Commit message for the STAGED changes only. ### Output format (return ONLY this) First line: type(scope): subject Add blank line before body Body paragraphs, each line <= 72 chars; bullets in body starting with "- " Optional footers (each on its own line), e.g.: BREAKING CHANGE: description ### Example generated commit message feat(core): add new feature - Implement new feature in core module - Update documentation BREAKING CHANGE: this change removes the old API method ### Rules - If commit_type or scope is provided above, USE THEM as-is. - If not provided, infer an appropriate type and a concise scope (or omit scope if unclear). - Subject: use imperative mood, start lowercase, no trailing period, <= 50 chars. - Body: use imperative mood (e.g. Update, Add etc.); explain WHAT and WHY, wrap at 72 chars; omit if subject suffices. - Use domain-specific terms; avoid generic phrases. - Do NOT mention "staged", "diff", or counts of files/lines. - Do NOT include markdown headers, code fences, or extra commentary. - Prefer a broad scope if many files; derive scope from top-level dirs when clear. - If there is a breaking change (e.g., API removal/rename), add a BREAKING CHANGE footer. - Keep the response to ONLY the commit message in the format above. ### Common types feat, fix, docs, style, refactor, perf, build, ci, test, chore, revert """) return analysis.strip() except subprocess.CalledProcessError as e: error_msg = e.stderr or e.stdout or str(e) return f"Git command failed: {error_msg}" except FileNotFoundError: return "Git is not installed or not found in PATH" except OSError as e: return f"OS error occurred: {str(e)}" def _parse_git_status_line(line): """ Helper to parse a single git status line. Returns (staged_file, unstaged_file, untracked_file) """ if len(line) < 3: return None, None, None staged_status = line[0] unstaged_status = line[1] filename = line[3:] if staged_status == "?" and unstaged_status == "?": return None, None, filename staged_file = filename if staged_status != " " else None unstaged_file = filename if unstaged_status != " " else None return staged_file, unstaged_file, None def _parse_git_status_lines(status_lines): staged_files = [] unstaged_files = [] untracked_files = [] for line in status_lines: staged_file, unstaged_file, untracked_file = _parse_git_status_line(line) if staged_file: staged_files.append(staged_file) if unstaged_file: unstaged_files.append(unstaged_file) if untracked_file: untracked_files.append(untracked_file) return staged_files, unstaged_files, untracked_files @mcp.tool() def check_git_status(repo_path: Optional[str] = None) -> str: """ Check the current git repository status. Args: repo_path: Optional path to the target git repository. If not provided, uses the current working directory. Returns: Current git status including staged, unstaged, and untracked files """ try: valid_repo_path = _get_valid_repo_path(repo_path) if not valid_repo_path: return f"Path '{repo_path or os.getcwd()}' is not a valid git repository." cwd = valid_repo_path # Get full git status status_result = subprocess.run( ["git", "status", "--porcelain"], capture_output=True, text=True, check=True, cwd=cwd, ) # Get branch info branch_result = subprocess.run( ["git", "branch", "--show-current"], capture_output=True, text=True, check=True, cwd=cwd, ) current_branch = branch_result.stdout.strip() if not status_result.stdout.strip(): return f"Repository is clean on branch '{current_branch}'. No changes to commit." # Parse status using helper status_lines = [line for line in status_result.stdout.split("\n") if line] staged_files, unstaged_files, untracked_files = _parse_git_status_lines( status_lines ) status_summary = f"Current branch: {current_branch}\n\n" if staged_files: status_summary += "Staged files (ready to commit):\n" status_summary += "\n".join(f" {file}" for file in staged_files) status_summary += "\n\n" if unstaged_files: status_summary += "Unstaged files (need to be added):\n" status_summary += "\n".join(f" {file}" for file in unstaged_files) status_summary += "\n\n" if untracked_files: status_summary += "Untracked files:\n" status_summary += "\n".join(f" {file}" for file in untracked_files) status_summary += "\n\n" if staged_files: status_summary += "✓ Ready to generate commit message!" else: status_summary += ( "ℹ Stage some files with 'git add' to generate commit messages." ) return status_summary except subprocess.CalledProcessError as e: error_msg = e.stderr or e.stdout or str(e) return f"Git command failed: {error_msg}" except FileNotFoundError: return "Git is not installed or not found in PATH" except OSError as e: return f"OS error occurred: {str(e)}" if __name__ == "__main__": mcp.run() ```