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

```
├── .env.example
├── .flake8
├── .github
│   └── ISSUE_TEMPLATE
│       ├── bug_report.md
│       └── feature_request.md
├── .gitignore
├── .gitlab
│   ├── issue_templates
│   │   ├── bug_report.md
│   │   └── feature_request.md
│   └── merge_request_templates
│       ├── bugfix.md
│       ├── default.md
│       └── feature.md
├── .gitlab-ci.yml
├── .gitlabci.yml
├── .pre-commit-config.yaml
├── CODE_REVIEW_GUIDELINES.md
├── CONTRIBUTING.md
├── fetch_mr_details.py
├── LICENSE
├── mcp-config-example.json
├── PULL_REQUEST_CHECKLIST.md
├── pyproject.toml
├── pytest.ini
├── README.md
├── README.zh-CN.md
├── requirements-dev.txt
├── requirements.txt
├── server.py
├── setup.py
├── test_gitlab_connection.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

```

--------------------------------------------------------------------------------
/.gitlab-ci.yml:
--------------------------------------------------------------------------------

```yaml
# GitLab CI/CD Pipeline for Code Review Quality Assurance

stages:
  - validate
  - test
  - quality
  - security
  - deploy

variables:
  PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
  PYTHON_VERSION: "3.12"

cache:
  paths:
    - .cache/pip
    - venv/

# 代码格式检查
code_format:
  stage: validate
  image: python:${PYTHON_VERSION}
  before_script:
    - python -m pip install --upgrade pip
    - pip install black isort flake8
  script:
    - black --check --diff .
    - isort --check-only --diff .
    - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
  allow_failure: false
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

# 类型检查
type_check:
  stage: validate
  image: python:${PYTHON_VERSION}
  before_script:
    - python -m pip install --upgrade pip
    - pip install mypy types-requests
    - pip install -r requirements.txt
  script:
    - mypy server.py --ignore-missing-imports
  allow_failure: true
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

# 单元测试
unit_tests:
  stage: test
  image: python:${PYTHON_VERSION}
  before_script:
    - python -m pip install --upgrade pip
    - pip install -r requirements.txt
    - pip install -r requirements-dev.txt
  script:
    - python -m pytest tests/ -v --cov=. --cov-report=xml --cov-report=term
  coverage: '/TOTAL.*\s+(\d+%)$/'
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: coverage.xml
    expire_in: 1 week
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

# 代码质量分析
code_quality:
  stage: quality
  image: python:${PYTHON_VERSION}
  before_script:
    - python -m pip install --upgrade pip
    - pip install pylint radon xenon
    - pip install -r requirements.txt
  script:
    - pylint server.py --output-format=text --reports=yes --exit-zero > pylint-report.txt
    - radon cc server.py --show-complexity --average
    - radon mi server.py --show
    - xenon --max-absolute B --max-modules A --max-average A server.py
  artifacts:
    paths:
      - pylint-report.txt
    expire_in: 1 week
  allow_failure: true
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

# 安全扫描
security_scan:
  stage: security
  image: python:${PYTHON_VERSION}
  before_script:
    - python -m pip install --upgrade pip
    - pip install bandit safety
    - pip install -r requirements.txt
  script:
    - bandit -r . -f json -o bandit-report.json || true
    - safety check --json --output safety-report.json || true
    - echo "Security scan completed"
  artifacts:
    paths:
      - bandit-report.json
      - safety-report.json
    expire_in: 1 week
  allow_failure: true
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

# 依赖扫描
dependency_scan:
  stage: security
  image: python:${PYTHON_VERSION}
  before_script:
    - python -m pip install --upgrade pip
    - pip install pip-audit
  script:
    - pip-audit --output=json --output-file=dependency-audit.json || true
  artifacts:
    paths:
      - dependency-audit.json
    expire_in: 1 week
  allow_failure: true
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

# MCP服务器功能测试
mcp_server_test:
  stage: test
  image: python:${PYTHON_VERSION}
  before_script:
    - python -m pip install --upgrade pip
    - pip install -r requirements.txt
    - pip install -r requirements-dev.txt
    # 创建测试用的环境变量
    - echo "GITLAB_TOKEN=test_token" > .env
    - echo "GITLAB_HOST=gitlab.example.com" >> .env
  script:
    - python -c "import server; print('Server imports successfully')"
    - python -m pytest tests/test_server.py -v
  allow_failure: false
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

# 文档检查
docs_check:
  stage: validate
  image: python:${PYTHON_VERSION}
  before_script:
    - python -m pip install --upgrade pip
    - pip install pydocstyle
  script:
    - pydocstyle server.py --convention=google
  allow_failure: true
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

# 性能测试
performance_test:
  stage: test
  image: python:${PYTHON_VERSION}
  before_script:
    - python -m pip install --upgrade pip
    - pip install -r requirements.txt
    - pip install memory-profiler psutil
  script:
    - python -c "
      import cProfile
      import pstats
      import server
      pr = cProfile.Profile()
      pr.enable()
      # 这里添加性能测试代码
      pr.disable()
      stats = pstats.Stats(pr)
      stats.sort_stats('cumulative')
      stats.print_stats(10)
      "
  allow_failure: true
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

# 代码审查提醒
review_reminder:
  stage: quality
  image: alpine:latest
  before_script:
    - apk add --no-cache curl jq
  script:
    - |
      if [ "$CI_PIPELINE_SOURCE" = "merge_request_event" ]; then
        echo "🔍 代码审查提醒:"
        echo "✅ 请确保代码符合项目规范"
        echo "✅ 请检查安全性和性能"
        echo "✅ 请验证测试覆盖率"
        echo "✅ 请确保文档更新"
        echo "📋 参考: CODE_REVIEW_GUIDELINES.md"
      fi
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

# 部署到开发环境
deploy_dev:
  stage: deploy
  image: alpine:latest
  script:
    - echo "部署到开发环境"
    - echo "验证MCP服务器配置"
    - echo "检查环境变量设置"
  environment:
    name: development
    url: https://dev.example.com
  rules:
    - if: $CI_COMMIT_BRANCH == "develop"
  when: manual

# 部署到生产环境
deploy_prod:
  stage: deploy
  image: alpine:latest
  script:
    - echo "部署到生产环境"
    - echo "验证生产环境配置"
    - echo "执行健康检查"
  environment:
    name: production
    url: https://prod.example.com
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
  when: manual
  only:
    - main
    - master

# 代码覆盖率检查
coverage_check:
  stage: quality
  image: python:${PYTHON_VERSION}
  before_script:
    - python -m pip install --upgrade pip
    - pip install -r requirements.txt
    - pip install -r requirements-dev.txt
  script:
    - python -m pytest tests/ --cov=. --cov-report=term --cov-fail-under=70
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
  allow_failure: true

# 生成代码质量报告
quality_report:
  stage: quality
  image: python:${PYTHON_VERSION}
  before_script:
    - python -m pip install --upgrade pip
    - pip install -r requirements.txt
  script:
    - |
      echo "📊 代码质量报告" > quality-report.md
      echo "==================" >> quality-report.md
      echo "" >> quality-report.md
      echo "## 检查项目:" >> quality-report.md
      echo "- ✅ 代码格式检查" >> quality-report.md
      echo "- ✅ 类型检查" >> quality-report.md
      echo "- ✅ 单元测试" >> quality-report.md
      echo "- ✅ 代码质量分析" >> quality-report.md
      echo "- ✅ 安全扫描" >> quality-report.md
      echo "- ✅ 依赖检查" >> quality-report.md
      echo "" >> quality-report.md
      echo "详细报告请查看各个阶段的输出。" >> quality-report.md
  artifacts:
    paths:
      - quality-report.md
    expire_in: 1 week
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

```

--------------------------------------------------------------------------------
/.gitlabci.yml:
--------------------------------------------------------------------------------

```yaml
# GitLab CI/CD Pipeline for Code Review Quality Assurance

stages:
  - validate
  - test
  - quality
  - security
  - deploy

variables:
  PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
  PYTHON_VERSION: "3.12"

cache:
  paths:
    - .cache/pip
    - venv/

# 代码格式检查
code_format:
  stage: validate
  image: python:${PYTHON_VERSION}
  before_script:
    - python -m pip install --upgrade pip
    - pip install black isort flake8
  script:
    - black --check --diff .
    - isort --check-only --diff .
    - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
  allow_failure: false
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

# 类型检查
type_check:
  stage: validate
  image: python:${PYTHON_VERSION}
  before_script:
    - python -m pip install --upgrade pip
    - pip install mypy types-requests
    - pip install -r requirements.txt
  script:
    - mypy server.py --ignore-missing-imports
  allow_failure: true
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

# 单元测试
unit_tests:
  stage: test
  image: python:${PYTHON_VERSION}
  before_script:
    - python -m pip install --upgrade pip
    - pip install -r requirements.txt
    - pip install -r requirements-dev.txt
  script:
    - python -m pytest tests/ -v --cov=. --cov-report=xml --cov-report=term
  coverage: '/TOTAL.*\s+(\d+%)$/'
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: coverage.xml
    expire_in: 1 week
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

# 代码质量分析
code_quality:
  stage: quality
  image: python:${PYTHON_VERSION}
  before_script:
    - python -m pip install --upgrade pip
    - pip install pylint radon xenon
    - pip install -r requirements.txt
  script:
    - pylint server.py --output-format=text --reports=yes --exit-zero > pylint-report.txt
    - radon cc server.py --show-complexity --average
    - radon mi server.py --show
    - xenon --max-absolute B --max-modules A --max-average A server.py
  artifacts:
    paths:
      - pylint-report.txt
    expire_in: 1 week
  allow_failure: true
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

# 安全扫描
security_scan:
  stage: security
  image: python:${PYTHON_VERSION}
  before_script:
    - python -m pip install --upgrade pip
    - pip install bandit safety
    - pip install -r requirements.txt
  script:
    - bandit -r . -f json -o bandit-report.json || true
    - safety check --json --output safety-report.json || true
    - echo "Security scan completed"
  artifacts:
    paths:
      - bandit-report.json
      - safety-report.json
    expire_in: 1 week
  allow_failure: true
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

# 依赖扫描
dependency_scan:
  stage: security
  image: python:${PYTHON_VERSION}
  before_script:
    - python -m pip install --upgrade pip
    - pip install pip-audit
  script:
    - pip-audit --output=json --output-file=dependency-audit.json || true
  artifacts:
    paths:
      - dependency-audit.json
    expire_in: 1 week
  allow_failure: true
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

# MCP服务器功能测试
mcp_server_test:
  stage: test
  image: python:${PYTHON_VERSION}
  before_script:
    - python -m pip install --upgrade pip
    - pip install -r requirements.txt
    - pip install -r requirements-dev.txt
    # 创建测试用的环境变量
    - echo "GITLAB_TOKEN=test_token" > .env
    - echo "GITLAB_HOST=gitlab.example.com" >> .env
  script:
    - python -c "import server; print('Server imports successfully')"
    - python -m pytest tests/test_server.py -v
  allow_failure: false
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

# 文档检查
docs_check:
  stage: validate
  image: python:${PYTHON_VERSION}
  before_script:
    - python -m pip install --upgrade pip
    - pip install pydocstyle
  script:
    - pydocstyle server.py --convention=google
  allow_failure: true
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

# 性能测试
performance_test:
  stage: test
  image: python:${PYTHON_VERSION}
  before_script:
    - python -m pip install --upgrade pip
    - pip install -r requirements.txt
    - pip install memory-profiler psutil
  script:
    - python -c "
      import cProfile
      import pstats
      import server
      pr = cProfile.Profile()
      pr.enable()
      # 这里添加性能测试代码
      pr.disable()
      stats = pstats.Stats(pr)
      stats.sort_stats('cumulative')
      stats.print_stats(10)
      "
  allow_failure: true
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

# 代码审查提醒
review_reminder:
  stage: quality
  image: alpine:latest
  before_script:
    - apk add --no-cache curl jq
  script:
    - |
      if [ "$CI_PIPELINE_SOURCE" = "merge_request_event" ]; then
        echo "🔍 代码审查提醒:"
        echo "✅ 请确保代码符合项目规范"
        echo "✅ 请检查安全性和性能"
        echo "✅ 请验证测试覆盖率"
        echo "✅ 请确保文档更新"
        echo "📋 参考: CODE_REVIEW_GUIDELINES.md"
      fi
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

# 部署到开发环境
deploy_dev:
  stage: deploy
  image: alpine:latest
  script:
    - echo "部署到开发环境"
    - echo "验证MCP服务器配置"
    - echo "检查环境变量设置"
  environment:
    name: development
    url: https://dev.example.com
  rules:
    - if: $CI_COMMIT_BRANCH == "develop"
  when: manual

# 部署到生产环境
deploy_prod:
  stage: deploy
  image: alpine:latest
  script:
    - echo "部署到生产环境"
    - echo "验证生产环境配置"
    - echo "执行健康检查"
  environment:
    name: production
    url: https://prod.example.com
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
  when: manual
  only:
    - main
    - master

# 代码覆盖率检查
coverage_check:
  stage: quality
  image: python:${PYTHON_VERSION}
  before_script:
    - python -m pip install --upgrade pip
    - pip install -r requirements.txt
    - pip install -r requirements-dev.txt
  script:
    - python -m pytest tests/ --cov=. --cov-report=term --cov-fail-under=70
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
  allow_failure: true

# 生成代码质量报告
quality_report:
  stage: quality
  image: python:${PYTHON_VERSION}
  before_script:
    - python -m pip install --upgrade pip
    - pip install -r requirements.txt
  script:
    - |
      echo "📊 代码质量报告" > quality-report.md
      echo "==================" >> quality-report.md
      echo "" >> quality-report.md
      echo "## 检查项目:" >> quality-report.md
      echo "- ✅ 代码格式检查" >> quality-report.md
      echo "- ✅ 类型检查" >> quality-report.md
      echo "- ✅ 单元测试" >> quality-report.md
      echo "- ✅ 代码质量分析" >> quality-report.md
      echo "- ✅ 安全扫描" >> quality-report.md
      echo "- ✅ 依赖检查" >> quality-report.md
      echo "" >> quality-report.md
      echo "详细报告请查看各个阶段的输出。" >> quality-report.md
  artifacts:
    paths:
      - quality-report.md
    expire_in: 1 week
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

```

--------------------------------------------------------------------------------
/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/lininn/gitlab-code-review-mcp.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.

## Code Review Standards

This project follows strict code review standards to ensure quality and maintainability:

- 📋 **Code Review Guidelines**: This project follows a strict set of code review guidelines to ensure quality and consistency. For detailed information on the review process, standards, and best practices, please see the [Code Review Guidelines](CODE_REVIEW_GUIDELINES.md).
- ✅ **Review Checklist**: All pull requests should be checked against the [PULL_REQUEST_CHECKLIST.md](PULL_REQUEST_CHECKLIST.md) before submission.
- 🔄 **CI/CD Pipeline**: We use GitLab CI for automated quality checks. Ensure all pipeline checks pass before requesting a review.
- 📝 **Templates**: Please use the provided merge request and issue templates to ensure all necessary information is included.

### Quick Start for Contributors

1. Read the [Code Review Guidelines](CODE_REVIEW_GUIDELINES.md)
2. Use the appropriate MR template when creating pull requests
3. Ensure all CI checks pass before requesting review
4. Address all reviewer feedback promptly

## 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() 
```

--------------------------------------------------------------------------------
/test_gitlab_connection.py:
--------------------------------------------------------------------------------

```python
import os
import requests
from dotenv import load_dotenv

# Load environment variables
load_dotenv()

# Get configuration from environment
GITLAB_HOST = os.getenv("GITLAB_HOST", "gitlab.com")
GITLAB_TOKEN = os.getenv("GITLAB_TOKEN")
API_VERSION = os.getenv("GITLAB_API_VERSION", "v4")

if not GITLAB_TOKEN:
    print("Error: GITLAB_TOKEN not set in environment")
    exit(1)

# Remove https:// prefix if present for API calls
if GITLAB_HOST.startswith("https://"):
    GITLAB_HOST = GITLAB_HOST.replace("https://", "")
elif GITLAB_HOST.startswith("http://"):
    GITLAB_HOST = GITLAB_HOST.replace("http://", "")

print(f"Testing connection to GitLab host: {GITLAB_HOST}")
print(f"Using API version: {API_VERSION}")

# Test basic API connection
url = f"https://{GITLAB_HOST}/api/{API_VERSION}/version"
headers = {
    'Accept': 'application/json',
    'User-Agent': 'GitLabConnectionTest/1.0',
    'Private-Token': GITLAB_TOKEN
}

try:
    print(f"Making request to: {url}")
    response = requests.get(url, headers=headers, verify=True, timeout=30)
    
    if response.status_code == 200:
        print("✅ Connection successful!")
        version_info = response.json()
        print(f"GitLab version: {version_info.get('version', 'Unknown')}")
        print(f"Revision: {version_info.get('revision', 'Unknown')}")
    else:
        print(f"❌ Connection failed with status code: {response.status_code}")
        print(f"Response: {response.text}")
        
except requests.exceptions.RequestException as e:
    print(f"❌ Request failed: {str(e)}")
    if hasattr(e, 'response') and e.response:
        print(f"Response status: {e.response.status_code}")
        print(f"Response text: {e.response.text}")

# Test project access
project_id = "front-end/wmflight"
project_url = f"https://{GITLAB_HOST}/api/{API_VERSION}/projects/{project_id.replace('/', '%2F')}"

print(f"\nTesting access to project: {project_id}")
print(f"Project URL: {project_url}")

try:
    response = requests.get(project_url, headers=headers, verify=True, timeout=30)
    
    if response.status_code == 200:
        project_info = response.json()
        print("✅ Project access successful!")
        print(f"Project name: {project_info.get('name', 'Unknown')}")
        print(f"Project ID: {project_info.get('id', 'Unknown')}")
        print(f"Visibility: {project_info.get('visibility', 'Unknown')}")
    else:
        print(f"❌ Project access failed with status code: {response.status_code}")
        print(f"Response: {response.text}")
        
except requests.exceptions.RequestException as e:
    print(f"❌ Project request failed: {str(e)}")
```

--------------------------------------------------------------------------------
/fetch_mr_details.py:
--------------------------------------------------------------------------------

```python
import os
import requests
import json
from dotenv import load_dotenv
from urllib.parse import quote

# Load environment variables
load_dotenv()

# Get configuration from environment
GITLAB_HOST = os.getenv("GITLAB_HOST", "gitlab.com")
GITLAB_TOKEN = os.getenv("GITLAB_TOKEN")
API_VERSION = os.getenv("GITLAB_API_VERSION", "v4")

if not GITLAB_TOKEN:
    print("Error: GITLAB_TOKEN not set in environment")
    exit(1)

# Remove https:// prefix if present for API calls
if GITLAB_HOST.startswith("https://"):
    GITLAB_HOST = GITLAB_HOST.replace("https://", "")
elif GITLAB_HOST.startswith("http://"):
    GITLAB_HOST = GITLAB_HOST.replace("http://", "")

headers = {
    'Accept': 'application/json',
    'User-Agent': 'GitLabMRReview/1.0',
    'Private-Token': GITLAB_TOKEN
}

def make_request(url):
    """Make API request and handle response"""
    try:
        response = requests.get(url, headers=headers, verify=True, timeout=30)
        response.raise_for_status()
        return response.json()
    except requests.exceptions.RequestException as e:
        print(f"Request failed: {str(e)}")
        if hasattr(e, 'response') and e.response:
            print(f"Status code: {e.response.status_code}")
            print(f"Response: {e.response.text}")
        return None

def fetch_merge_request_details(project_id, mr_iid):
    """Fetch complete details for a merge request"""
    print(f"Fetching details for merge request #{mr_iid} in project {project_id}")
    
    # Get merge request basic info
    mr_endpoint = f"https://{GITLAB_HOST}/api/{API_VERSION}/projects/{quote(project_id, safe='')}/merge_requests/{mr_iid}"
    mr_info = make_request(mr_endpoint)
    
    if not mr_info:
        print("Failed to get merge request info")
        return None
    
    # Get changes/diffs
    changes_endpoint = f"{mr_endpoint}/changes"
    changes_info = make_request(changes_endpoint)
    
    # Get commits
    commits_endpoint = f"{mr_endpoint}/commits"
    commits_info = make_request(commits_endpoint)
    
    # Get notes/comments
    notes_endpoint = f"{mr_endpoint}/notes"
    notes_info = make_request(notes_endpoint)
    
    return {
        "merge_request": mr_info,
        "changes": changes_info,
        "commits": commits_info,
        "notes": notes_info
    }

def print_mr_summary(mr_data):
    """Print a summary of the merge request"""
    if not mr_data:
        return
    
    mr_info = mr_data["merge_request"]
    changes = mr_data["changes"]
    commits = mr_data["commits"]
    notes = mr_data["notes"]
    
    print("\n" + "="*80)
    print("MERGE REQUEST SUMMARY")
    print("="*80)
    print(f"Title: {mr_info.get('title', 'No title')}")
    print(f"Author: {mr_info.get('author', {}).get('name', 'Unknown')}")
    print(f"State: {mr_info.get('state', 'Unknown')}")
    print(f"Created: {mr_info.get('created_at', 'Unknown')}")
    print(f"Updated: {mr_info.get('updated_at', 'Unknown')}")
    print(f"Source branch: {mr_info.get('source_branch', 'Unknown')}")
    print(f"Target branch: {mr_info.get('target_branch', 'Unknown')}")
    print(f"Merge status: {mr_info.get('merge_status', 'Unknown')}")
    
    print(f"\nChanges: {len(changes.get('changes', [])) if changes else 0} files changed")
    print(f"Commits: {len(commits) if commits else 0} commits")
    print(f"Comments: {len(notes) if notes else 0} notes")
    
    # Print changed files
    if changes and 'changes' in changes:
        print(f"\nChanged files:")
        for change in changes['changes']:
            old_path = change.get('old_path', 'N/A')
            new_path = change.get('new_path', 'N/A')
            diff = change.get('diff', '')
            lines_added = diff.count('\n+') - diff.count('\n+++')
            lines_removed = diff.count('\n-') - diff.count('\n---')
            
            print(f"  - {new_path} ({old_path})")
            print(f"    +{lines_added} -{lines_removed} lines")

# Main execution
if __name__ == "__main__":
    project_id = "front-end/wmflight"
    mr_iid = "924"
    
    mr_data = fetch_merge_request_details(project_id, mr_iid)
    
    if mr_data:
        print_mr_summary(mr_data)
        
        # Save detailed data to file for analysis
        with open(f"mr_{mr_iid}_details.json", "w", encoding="utf-8") as f:
            json.dump(mr_data, f, indent=2, ensure_ascii=False)
        print(f"\nDetailed data saved to mr_{mr_iid}_details.json")
    else:
        print("Failed to fetch merge request data")
```

--------------------------------------------------------------------------------
/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:
        logger.info(f"Making {method} request to {url}")
        logger.debug(f"Headers: {headers}")
        response = None
        if method.upper() == "GET":
            response = requests.get(url, headers=headers, verify=True)
        elif method.upper() == "POST":
            logger.debug(f"Request data: {data}")
            response = requests.post(url, headers=headers, json=data, verify=True)
        else:
            raise ValueError(f"Unsupported HTTP method: {method}")
        
        if response is None:
            logger.error("Request did not return a response.")
            raise Exception("Request did not return a response.")

        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(
    name="GitLab MCP for Code Review",
    instructions="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

@mcp.tool()
def get_review_guidelines(ctx: Context) -> str:
    """
    Get the code review guidelines.
    
    Returns:
        The content of the code review guidelines file.
    """
    try:
        with open("CODE_REVIEW_GUIDELINES.md", "r", encoding="utf-8") as f:
            return f.read()
    except FileNotFoundError:
        logger.error("CODE_REVIEW_GUIDELINES.md not found.")
        raise FileNotFoundError("CODE_REVIEW_GUIDELINES.md not found.")
    except Exception as e:
        logger.error(f"Failed to read CODE_REVIEW_GUIDELINES.md: {str(e)}")
        raise

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
```