#
tokens: 6981/50000 17/17 files
lines: off (toggle) GitHub
raw markdown copy
# Directory Structure

```
├── .env.example
├── .flake8
├── .github
│   └── ISSUE_TEMPLATE
│       ├── bug_report.md
│       └── feature_request.md
├── .gitignore
├── .pre-commit-config.yaml
├── CONTRIBUTING.md
├── LICENSE
├── mcp-config-example.json
├── pyproject.toml
├── pytest.ini
├── README.md
├── requirements-dev.txt
├── requirements.txt
├── server.py
├── setup.py
└── tests
    ├── __init__.py
    └── test_server.py
```

# Files

--------------------------------------------------------------------------------
/.flake8:
--------------------------------------------------------------------------------

```
[flake8]
max-line-length = 88
extend-ignore = E203
exclude = .git,__pycache__,.venv,venv,dist,build,*.egg-info 
```

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

```
# Python-specific
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg

# Virtual environments
.venv/
venv/
ENV/

# Environment variables
.env

# Editor/IDE specific
.idea/
.vscode/
*.swp
*.swo
.DS_Store

# Log files
*.log

# Local development
uv.lock 
```

--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------

```yaml
repos:
-   repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.4.0
    hooks:
    -   id: trailing-whitespace
    -   id: end-of-file-fixer
    -   id: check-yaml
    -   id: check-toml
    -   id: check-added-large-files

-   repo: https://github.com/pycqa/isort
    rev: 5.12.0
    hooks:
    -   id: isort
        name: isort (python)

-   repo: https://github.com/psf/black
    rev: 23.3.0
    hooks:
    -   id: black

-   repo: https://github.com/pycqa/flake8
    rev: 6.0.0
    hooks:
    -   id: flake8
        additional_dependencies: [flake8-docstrings]

-   repo: https://github.com/pre-commit/mirrors-mypy
    rev: v1.3.0
    hooks:
    -   id: mypy
        files: ^server\.py$
        additional_dependencies: [types-requests, types-PyYAML] 
```

--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------

```
# GitLab API Configuration
# =====================

# Required: Your GitLab personal access token with appropriate scopes
# You can generate one at: https://gitlab.com/-/profile/personal_access_tokens
# Required scopes: api, read_api
GITLAB_TOKEN=your_personal_access_token_here

# Optional: Your GitLab host URL (defaults to gitlab.com if not specified)
# For self-hosted GitLab instances, use your domain, e.g., gitlab.example.com
GITLAB_HOST=gitlab.com

# Optional: API version (defaults to v4 if not specified)
# Only change this if you need to use a different API version
GITLAB_API_VERSION=v4

# Logging Configuration
# ====================

# Optional: Log level - one of: DEBUG, INFO, WARNING, ERROR, CRITICAL
# Defaults to INFO if not specified
LOG_LEVEL=INFO

# Optional: Enable debugging (true/false)
# Set to true only during development
DEBUG=false

# Application Settings
# ===================

# Optional: Request timeout in seconds
# Maximum time to wait for GitLab API responses
REQUEST_TIMEOUT=30

# Optional: Maximum retries for failed requests
MAX_RETRIES=3

# Optional: User-Agent header for API requests
# Helps GitLab identify your application
USER_AGENT=GitLabMCPCodeReview/1.0

```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
# GitLab MCP for Code Review

[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

> This project is forked from [cayirtepeomer/gerrit-code-review-mcp](https://github.com/cayirtepeomer/gerrit-code-review-mcp) and adapted for GitLab integration.

An MCP (Model Context Protocol) server for integrating AI assistants like Claude with GitLab's merge requests. This allows AI assistants to review code changes directly through the GitLab API.

## Features

- **Complete Merge Request Analysis**: Fetch full details about merge requests including diffs, commits, and comments
- **File-Specific Diffs**: Analyze changes to specific files within merge requests
- **Version Comparison**: Compare different branches, tags, or commits
- **Review Management**: Add comments, approve, or unapprove merge requests
- **Project Overview**: Get lists of all merge requests in a project

## Installation

### Prerequisites

- Python 3.10+ 
- GitLab personal access token with API scope (read_api, api)
- [Cursor IDE](https://cursor.sh/) or [Claude Desktop App](https://claude.ai/desktop) for MCP integration

### Quick Start

1. Clone this repository:

```bash
git clone https://github.com/mehmetakinn/gitlab-mcp-code-review.git
cd gitlab-mcp-code-review
```

2. Create and activate a virtual environment:

```bash
python -m venv .venv
source .venv/bin/activate  # On Windows: .venv\Scripts\activate
```

3. Install dependencies:

```bash
pip install -r requirements.txt
```

4. Create a `.env` file with your GitLab configuration (see `.env.example` for all options):

```
# Required
GITLAB_TOKEN=your_personal_access_token_here

# Optional settings
GITLAB_HOST=gitlab.com
GITLAB_API_VERSION=v4
LOG_LEVEL=INFO
```

## Configuration Options

The following environment variables can be configured in your `.env` file:

| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| GITLAB_TOKEN | Yes | - | Your GitLab personal access token |
| GITLAB_HOST | No | gitlab.com | GitLab instance hostname |
| GITLAB_API_VERSION | No | v4 | GitLab API version to use |
| LOG_LEVEL | No | INFO | Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL) |
| DEBUG | No | false | Enable debug mode |
| REQUEST_TIMEOUT | No | 30 | API request timeout in seconds |
| MAX_RETRIES | No | 3 | Maximum retry attempts for failed requests |

## Cursor IDE Integration

To use this MCP with Cursor IDE, add this configuration to your `~/.cursor/mcp.json` file:

```json
{
  "mcpServers": {
    "gitlab-mcp-code-review": {
      "command": "/path/to/your/gitlab-mcp-code-review/.venv/bin/python",
      "args": [
        "/path/to/your/gitlab-mcp-code-review/server.py",
        "--transport",
        "stdio"
      ],
      "cwd": "/path/to/your/gitlab-mcp-code-review",
      "env": {
        "PYTHONPATH": "/path/to/your/gitlab-mcp-code-review",
        "VIRTUAL_ENV": "/path/to/your/gitlab-mcp-code-review/.venv",
        "PATH": "/path/to/your/gitlab-mcp-code-review/.venv/bin:/usr/local/bin:/usr/bin:/bin"
      },
      "stdio": true
    }
  }
}
```

Replace `/path/to/your/gitlab-mcp-code-review` with the actual path to your cloned repository.

## Claude Desktop App Integration

To use this MCP with the Claude Desktop App:

1. Open the Claude Desktop App
2. Go to Settings → Advanced → MCP Configuration
3. Add the following configuration:

```json
{
  "mcpServers": {
    "gitlab-mcp-code-review": {
      "command": "/path/to/your/gitlab-mcp-code-review/.venv/bin/python",
      "args": [
        "/path/to/your/gitlab-mcp-code-review/server.py",
        "--transport",
        "stdio"
      ],
      "cwd": "/path/to/your/gitlab-mcp-code-review",
      "env": {
        "PYTHONPATH": "/path/to/your/gitlab-mcp-code-review",
        "VIRTUAL_ENV": "/path/to/your/gitlab-mcp-code-review/.venv",
        "PATH": "/path/to/your/gitlab-mcp-code-review/.venv/bin:/usr/local/bin:/usr/bin:/bin"
      },
      "stdio": true
    }
  }
}
```

Replace `/path/to/your/gitlab-mcp-code-review` with the actual path to your cloned repository.

## Available Tools

The MCP server provides the following tools for interacting with GitLab:

| Tool | Description |
|------|-------------|
| `fetch_merge_request` | Get complete information about a merge request |
| `fetch_merge_request_diff` | Get diffs for a specific merge request |
| `fetch_commit_diff` | Get diff information for a specific commit |
| `compare_versions` | Compare different branches, tags, or commits |
| `add_merge_request_comment` | Add a comment to a merge request |
| `approve_merge_request` | Approve a merge request |
| `unapprove_merge_request` | Unapprove a merge request |
| `get_project_merge_requests` | Get a list of merge requests for a project |

## Usage Examples

### Fetch a Merge Request

```python
# Get details of merge request #5 in project with ID 123
mr = fetch_merge_request("123", "5")
```

### View Specific File Changes

```python
# Get diff for a specific file in a merge request
file_diff = fetch_merge_request_diff("123", "5", "path/to/file.js")
```

### Compare Branches

```python
# Compare develop branch with master branch
diff = compare_versions("123", "develop", "master")
```

### Add a Comment to a Merge Request

```python
# Add a comment to a merge request
comment = add_merge_request_comment("123", "5", "This code looks good!")
```

### Approve a Merge Request

```python
# Approve a merge request and set required approvals to 2
approval = approve_merge_request("123", "5", approvals_required=2)
```

## Troubleshooting

If you encounter issues:

1. Verify your GitLab token has the appropriate permissions (api, read_api)
2. Check your `.env` file settings
3. Ensure your MCP configuration paths are correct
4. Test connection with: `curl -H "Private-Token: your-token" https://gitlab.com/api/v4/projects`
5. Set LOG_LEVEL=DEBUG in your .env file for more detailed logging

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

1. Fork the repository
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request

See the [CONTRIBUTING.md](CONTRIBUTING.md) file for more details on the development process.

## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

```

--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------

```markdown
# Contributing to GitLab Code Review MCP

Thank you for considering contributing to GitLab Code Review MCP! Here's how you can help:

## Development Process

1. Fork the repository
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
3. Set up the development environment:
   ```bash
   python -m venv .venv
   source .venv/bin/activate  # On Windows: .venv\Scripts\activate
   pip install -e ".[dev]"
   ```
4. Make your changes
5. Run linting and tests:
   ```bash
   black .
   isort .
   mypy server.py
   pytest
   ```
6. Commit your changes with meaningful commit messages:
   ```bash
   git commit -m "Add some amazing feature"
   ```
7. Push to your branch:
   ```bash
   git push origin feature/amazing-feature
   ```
8. Open a Pull Request

## Pull Request Guidelines

- Update the README.md if needed
- Keep pull requests focused on a single change
- Write tests for your changes when possible
- Document new code based using docstrings
- End all files with a newline

## Code Style

This project uses:
- Black for code formatting
- isort for import sorting
- mypy for type checking

## License

By contributing, you agree that your contributions will be licensed under the project's MIT License. 
```

--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------

```python
# Tests package 
```

--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------

```python
from setuptools import setup

if __name__ == "__main__":
    setup() 
```

--------------------------------------------------------------------------------
/pytest.ini:
--------------------------------------------------------------------------------

```
[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts = --cov=. --cov-report=term-missing 
```

--------------------------------------------------------------------------------
/requirements-dev.txt:
--------------------------------------------------------------------------------

```
# Include main requirements
-r requirements.txt

# Testing
pytest>=7.0.0
pytest-cov>=4.0.0

# Code quality
black>=23.0.0
isort>=5.0.0
mypy>=1.0.0
pre-commit>=3.0.0 
```

--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------

```
# Core dependencies
mcp[cli]>=1.6.0
python-dotenv>=1.0.0
requests>=2.31.0

# Development dependencies (optional)
# Install with: pip install -r requirements-dev.txt 
```

--------------------------------------------------------------------------------
/mcp-config-example.json:
--------------------------------------------------------------------------------

```json
{
  "mcpServers": {
    "gitlab-review-mcp": {
      "command": "${WORKSPACE_PATH}/.venv/bin/python",
      "args": [
        "${WORKSPACE_PATH}/server.py",
        "--transport",
        "stdio"
      ],
      "cwd": "${WORKSPACE_PATH}",
      "env": {
        "PYTHONPATH": "${WORKSPACE_PATH}",
        "VIRTUAL_ENV": "${WORKSPACE_PATH}/.venv",
        "PATH": "${WORKSPACE_PATH}/.venv/bin:/usr/local/bin:/usr/bin:/bin"
      },
      "stdio": true
    }
  }
} 
```

--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------

```markdown
---
name: Feature request
about: Suggest an idea for this project
title: '[FEATURE] '
labels: enhancement
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. 
```

--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------

```markdown
---
name: Bug report
about: Create a report to help us improve
title: '[BUG] '
labels: bug
assignees: ''

---

**Describe the bug**
A clear and concise description of what the bug is.

**To Reproduce**
Steps to reproduce the behavior:
1. Import '...'
2. Call function '....'
3. Pass arguments '....'
4. See error

**Expected behavior**
A clear and concise description of what you expected to happen.

**Error messages**
If applicable, add error messages or exception tracebacks.

**Environment:**
 - OS: [e.g. Ubuntu 22.04, macOS 13.0]
 - Python version: [e.g. 3.10.5]
 - GitLab API version: [e.g. v4]
 - Package version: [e.g. 0.1.0]

**Additional context**
Add any other context about the problem here. 
```

--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------

```toml
[project]
name = "gitlab-mcp-code-review"
version = "0.1.0"
description = "MCP server for GitLab Code Review"
authors = [{name = "GitLab MCP Code Review Contributors"}]
license = "MIT"
readme = "README.md"
requires-python = ">=3.10"
classifiers = [
    "Development Status :: 4 - Beta",
    "Intended Audience :: Developers",
    "License :: OSI Approved :: MIT License",
    "Programming Language :: Python :: 3.10",
    "Programming Language :: Python :: 3.11",
    "Programming Language :: Python :: 3.12",
]
dependencies = [
    "mcp[cli]>=1.6.0",
    "python-dotenv>=1.0.0",
    "requests>=2.31.0",
]

[project.optional-dependencies]
dev = [
    "pytest>=7.0.0",
    "pytest-cov>=4.0.0",
    "black>=23.0.0",
    "isort>=5.0.0",
    "mypy>=1.0.0",
    "pre-commit>=3.0.0",
]

[build-system]
requires = ["setuptools>=42", "wheel"]
build-backend = "setuptools.build_meta"

[tool.black]
line-length = 88

[tool.isort]
profile = "black"

[tool.mypy]
strict = true 

```

--------------------------------------------------------------------------------
/tests/test_server.py:
--------------------------------------------------------------------------------

```python
import unittest
from unittest.mock import patch, MagicMock

# This is a basic test skeleton for the server
# You would need to add more comprehensive tests


class TestGitLabMCP(unittest.TestCase):
    """Test cases for GitLab MCP server"""

    def setUp(self):
        """Set up test fixtures"""
        self.mock_ctx = MagicMock()
        self.mock_lifespan_context = MagicMock()
        self.mock_ctx.request_context.lifespan_context = self.mock_lifespan_context
        self.mock_lifespan_context.token = "fake_token"
        self.mock_lifespan_context.host = "gitlab.com"

    @patch('requests.get')
    def test_make_gitlab_api_request(self, mock_get):
        """Test the GitLab API request function"""
        # Import here to avoid module-level imports before patching
        from server import make_gitlab_api_request
        
        # Setup mock response
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = {"id": 123, "name": "test_project"}
        mock_get.return_value = mock_response
        
        # Test the function
        result = make_gitlab_api_request(self.mock_ctx, "projects/123")
        
        # Assertions
        mock_get.assert_called_once()
        self.assertEqual(result, {"id": 123, "name": "test_project"})


if __name__ == '__main__':
    unittest.main() 
```

--------------------------------------------------------------------------------
/server.py:
--------------------------------------------------------------------------------

```python
import os
import json
import logging
from typing import Optional, Dict, Any, Union, List
from dataclasses import dataclass
from contextlib import asynccontextmanager
from collections.abc import AsyncIterator
from urllib.parse import quote
import requests

from dotenv import load_dotenv
from mcp.server.fastmcp import FastMCP, Context

# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# Load environment variables
load_dotenv()

@dataclass
class GitLabContext:
    host: str
    token: str
    api_version: str = "v4"

def make_gitlab_api_request(ctx: Context, endpoint: str, method: str = "GET", data: Optional[Dict[str, Any]] = None) -> Any:
    """Make a REST API request to GitLab and handle the response"""
    gitlab_ctx = ctx.request_context.lifespan_context
    
    if not gitlab_ctx.token:
        logger.error("GitLab token not set in context")
        raise ValueError("GitLab token not set. Please set GITLAB_TOKEN in your environment.")
    
    url = f"https://{gitlab_ctx.host}/api/{gitlab_ctx.api_version}/{endpoint}"
    headers = {
        'Accept': 'application/json',
        'User-Agent': 'GitLabMCPCodeReview/1.0',
        'Private-Token': gitlab_ctx.token
    }
    
    try:
        if method.upper() == "GET":
            response = requests.get(url, headers=headers, verify=True)
        elif method.upper() == "POST":
            response = requests.post(url, headers=headers, json=data, verify=True)
        else:
            raise ValueError(f"Unsupported HTTP method: {method}")
        
        if response.status_code == 401:
            logger.error("Authentication failed. Check your GitLab token.")
            raise Exception("Authentication failed. Please check your GitLab token.")
            
        response.raise_for_status()
        
        if not response.content:
            return {}
            
        try:
            return response.json()
        except json.JSONDecodeError as e:
            logger.error(f"Failed to parse JSON response: {str(e)}")
            raise Exception(f"Failed to parse GitLab response as JSON: {str(e)}")
            
    except requests.exceptions.RequestException as e:
        logger.error(f"REST request failed: {str(e)}")
        if hasattr(e, 'response'):
            logger.error(f"Response status: {e.response.status_code}")
        raise Exception(f"Failed to make GitLab API request: {str(e)}")

@asynccontextmanager
async def gitlab_lifespan(server: FastMCP) -> AsyncIterator[GitLabContext]:
    """Manage GitLab connection details"""
    host = os.getenv("GITLAB_HOST", "gitlab.com")
    token = os.getenv("GITLAB_TOKEN", "")
    
    if not token:
        logger.error("Missing required environment variable: GITLAB_TOKEN")
        raise ValueError(
            "Missing required environment variable: GITLAB_TOKEN. "
            "Please set this in your environment or .env file."
        )
    
    ctx = GitLabContext(host=host, token=token)
    try:
        yield ctx
    finally:
        pass

# Create MCP server
mcp = FastMCP(
    "GitLab MCP for Code Review",
    description="MCP server for reviewing GitLab code changes",
    lifespan=gitlab_lifespan,
    dependencies=["python-dotenv", "requests"]
)

@mcp.tool()
def fetch_merge_request(ctx: Context, project_id: str, merge_request_iid: str) -> Dict[str, Any]:
    """
    Fetch a GitLab merge request and its contents.
    
    Args:
        project_id: The GitLab project ID or URL-encoded path
        merge_request_iid: The merge request IID (project-specific ID)
    Returns:
        Dict containing the merge request information
    """
    # Get merge request details
    mr_endpoint = f"projects/{quote(project_id, safe='')}/merge_requests/{merge_request_iid}"
    mr_info = make_gitlab_api_request(ctx, mr_endpoint)
    
    if not mr_info:
        raise ValueError(f"Merge request {merge_request_iid} not found in project {project_id}")
    
    # Get the changes (diffs) for this merge request
    changes_endpoint = f"{mr_endpoint}/changes"
    changes_info = make_gitlab_api_request(ctx, changes_endpoint)
    
    # Get the commit information
    commits_endpoint = f"{mr_endpoint}/commits"
    commits_info = make_gitlab_api_request(ctx, commits_endpoint)
    
    # Get the notes (comments) for this merge request
    notes_endpoint = f"{mr_endpoint}/notes"
    notes_info = make_gitlab_api_request(ctx, notes_endpoint)
    
    return {
        "merge_request": mr_info,
        "changes": changes_info,
        "commits": commits_info,
        "notes": notes_info
    }

@mcp.tool()
def fetch_merge_request_diff(ctx: Context, project_id: str, merge_request_iid: str, file_path: Optional[str] = None) -> Dict[str, Any]:
    """
    Fetch the diff for a specific file in a merge request, or all files if none specified.
    
    Args:
        project_id: The GitLab project ID or URL-encoded path
        merge_request_iid: The merge request IID (project-specific ID)
        file_path: Optional specific file path to get diff for
    Returns:
        Dict containing the diff information
    """
    # Get the changes for this merge request
    changes_endpoint = f"projects/{quote(project_id, safe='')}/merge_requests/{merge_request_iid}/changes"
    changes_info = make_gitlab_api_request(ctx, changes_endpoint)
    
    if not changes_info:
        raise ValueError(f"Changes not found for merge request {merge_request_iid}")
    
    # Extract all changes
    files = changes_info.get("changes", [])
    
    # Filter by file path if specified
    if file_path:
        files = [f for f in files if f.get("new_path") == file_path or f.get("old_path") == file_path]
        if not files:
            raise ValueError(f"File '{file_path}' not found in the merge request changes")
    
    return {
        "merge_request_iid": merge_request_iid,
        "files": files
    }

@mcp.tool()
def fetch_commit_diff(ctx: Context, project_id: str, commit_sha: str, file_path: Optional[str] = None) -> Dict[str, Any]:
    """
    Fetch the diff for a specific commit, or for a specific file in that commit.
    
    Args:
        project_id: The GitLab project ID or URL-encoded path
        commit_sha: The commit SHA
        file_path: Optional specific file path to get diff for
    Returns:
        Dict containing the diff information
    """
    # Get the diff for this commit
    diff_endpoint = f"projects/{quote(project_id, safe='')}/repository/commits/{commit_sha}/diff"
    diff_info = make_gitlab_api_request(ctx, diff_endpoint)
    
    if not diff_info:
        raise ValueError(f"Diff not found for commit {commit_sha}")
    
    # Filter by file path if specified
    if file_path:
        diff_info = [d for d in diff_info if d.get("new_path") == file_path or d.get("old_path") == file_path]
        if not diff_info:
            raise ValueError(f"File '{file_path}' not found in the commit diff")
    
    # Get the commit details
    commit_endpoint = f"projects/{quote(project_id, safe='')}/repository/commits/{commit_sha}"
    commit_info = make_gitlab_api_request(ctx, commit_endpoint)
    
    return {
        "commit": commit_info,
        "diffs": diff_info
    }

@mcp.tool()
def compare_versions(ctx: Context, project_id: str, from_sha: str, to_sha: str) -> Dict[str, Any]:
    """
    Compare two commits/branches/tags to see the differences between them.
    
    Args:
        project_id: The GitLab project ID or URL-encoded path
        from_sha: The source commit/branch/tag
        to_sha: The target commit/branch/tag
    Returns:
        Dict containing the comparison information
    """
    # Compare the versions
    compare_endpoint = f"projects/{quote(project_id, safe='')}/repository/compare?from={quote(from_sha, safe='')}&to={quote(to_sha, safe='')}"
    compare_info = make_gitlab_api_request(ctx, compare_endpoint)
    
    if not compare_info:
        raise ValueError(f"Comparison failed between {from_sha} and {to_sha}")
    
    return compare_info

@mcp.tool()
def add_merge_request_comment(ctx: Context, project_id: str, merge_request_iid: str, body: str, position: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
    """
    Add a comment to a merge request, optionally at a specific position in a file.
    
    Args:
        project_id: The GitLab project ID or URL-encoded path
        merge_request_iid: The merge request IID (project-specific ID)
        body: The comment text
        position: Optional position data for line comments
    Returns:
        Dict containing the created comment information
    """
    # Create the comment data
    data = {
        "body": body
    }
    
    # Add position data if provided
    if position:
        data["position"] = position
    
    # Add the comment
    comment_endpoint = f"projects/{quote(project_id, safe='')}/merge_requests/{merge_request_iid}/notes"
    comment_info = make_gitlab_api_request(ctx, comment_endpoint, method="POST", data=data)
    
    if not comment_info:
        raise ValueError("Failed to add comment to merge request")
    
    return comment_info

@mcp.tool()
def approve_merge_request(ctx: Context, project_id: str, merge_request_iid: str, approvals_required: Optional[int] = None) -> Dict[str, Any]:
    """
    Approve a merge request.
    
    Args:
        project_id: The GitLab project ID or URL-encoded path
        merge_request_iid: The merge request IID (project-specific ID)
        approvals_required: Optional number of required approvals to set
    Returns:
        Dict containing the approval information
    """
    # Approve the merge request
    approve_endpoint = f"projects/{quote(project_id, safe='')}/merge_requests/{merge_request_iid}/approve"
    approve_info = make_gitlab_api_request(ctx, approve_endpoint, method="POST")
    
    # Set required approvals if specified
    if approvals_required is not None:
        approvals_endpoint = f"projects/{quote(project_id, safe='')}/merge_requests/{merge_request_iid}/approvals"
        data = {
            "approvals_required": approvals_required
        }
        make_gitlab_api_request(ctx, approvals_endpoint, method="POST", data=data)
    
    return approve_info

@mcp.tool()
def unapprove_merge_request(ctx: Context, project_id: str, merge_request_iid: str) -> Dict[str, Any]:
    """
    Unapprove a merge request.
    
    Args:
        project_id: The GitLab project ID or URL-encoded path
        merge_request_iid: The merge request IID (project-specific ID)
    Returns:
        Dict containing the unapproval information
    """
    # Unapprove the merge request
    unapprove_endpoint = f"projects/{quote(project_id, safe='')}/merge_requests/{merge_request_iid}/unapprove"
    unapprove_info = make_gitlab_api_request(ctx, unapprove_endpoint, method="POST")
    
    return unapprove_info

@mcp.tool()
def get_project_merge_requests(ctx: Context, project_id: str, state: str = "all", limit: int = 20) -> List[Dict[str, Any]]:
    """
    Get all merge requests for a project.
    
    Args:
        project_id: The GitLab project ID or URL-encoded path
        state: Filter merge requests by state (all, opened, closed, merged, or locked)
        limit: Maximum number of merge requests to return
    Returns:
        List of merge request objects
    """
    # Get the merge requests
    mrs_endpoint = f"projects/{quote(project_id, safe='')}/merge_requests?state={state}&per_page={limit}"
    mrs_info = make_gitlab_api_request(ctx, mrs_endpoint)
    
    return mrs_info

if __name__ == "__main__":
    try:
        logger.info("Starting GitLab Review MCP server")
        # Initialize and run the server
        mcp.run(transport='stdio')
    except Exception as e:
        logger.error(f"Failed to start MCP server: {str(e)}")
        raise 
```