# 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
[](https://www.python.org/downloads/)
[](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
```