# 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:
--------------------------------------------------------------------------------
```
1 | [flake8]
2 | max-line-length = 88
3 | extend-ignore = E203
4 | exclude = .git,__pycache__,.venv,venv,dist,build,*.egg-info
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | # Python-specific
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 | *.so
6 | .Python
7 | env/
8 | build/
9 | develop-eggs/
10 | dist/
11 | downloads/
12 | eggs/
13 | .eggs/
14 | lib/
15 | lib64/
16 | parts/
17 | sdist/
18 | var/
19 | *.egg-info/
20 | .installed.cfg
21 | *.egg
22 |
23 | # Virtual environments
24 | .venv/
25 | venv/
26 | ENV/
27 |
28 | # Environment variables
29 | .env
30 |
31 | # Editor/IDE specific
32 | .idea/
33 | .vscode/
34 | *.swp
35 | *.swo
36 | .DS_Store
37 |
38 | # Log files
39 | *.log
40 |
41 | # Local development
42 | uv.lock
```
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
```yaml
1 | repos:
2 | - repo: https://github.com/pre-commit/pre-commit-hooks
3 | rev: v4.4.0
4 | hooks:
5 | - id: trailing-whitespace
6 | - id: end-of-file-fixer
7 | - id: check-yaml
8 | - id: check-toml
9 | - id: check-added-large-files
10 |
11 | - repo: https://github.com/pycqa/isort
12 | rev: 5.12.0
13 | hooks:
14 | - id: isort
15 | name: isort (python)
16 |
17 | - repo: https://github.com/psf/black
18 | rev: 23.3.0
19 | hooks:
20 | - id: black
21 |
22 | - repo: https://github.com/pycqa/flake8
23 | rev: 6.0.0
24 | hooks:
25 | - id: flake8
26 | additional_dependencies: [flake8-docstrings]
27 |
28 | - repo: https://github.com/pre-commit/mirrors-mypy
29 | rev: v1.3.0
30 | hooks:
31 | - id: mypy
32 | files: ^server\.py$
33 | additional_dependencies: [types-requests, types-PyYAML]
```
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
```
1 | # GitLab API Configuration
2 | # =====================
3 |
4 | # Required: Your GitLab personal access token with appropriate scopes
5 | # You can generate one at: https://gitlab.com/-/profile/personal_access_tokens
6 | # Required scopes: api, read_api
7 | GITLAB_TOKEN=your_personal_access_token_here
8 |
9 | # Optional: Your GitLab host URL (defaults to gitlab.com if not specified)
10 | # For self-hosted GitLab instances, use your domain, e.g., gitlab.example.com
11 | GITLAB_HOST=gitlab.com
12 |
13 | # Optional: API version (defaults to v4 if not specified)
14 | # Only change this if you need to use a different API version
15 | GITLAB_API_VERSION=v4
16 |
17 | # Logging Configuration
18 | # ====================
19 |
20 | # Optional: Log level - one of: DEBUG, INFO, WARNING, ERROR, CRITICAL
21 | # Defaults to INFO if not specified
22 | LOG_LEVEL=INFO
23 |
24 | # Optional: Enable debugging (true/false)
25 | # Set to true only during development
26 | DEBUG=false
27 |
28 | # Application Settings
29 | # ===================
30 |
31 | # Optional: Request timeout in seconds
32 | # Maximum time to wait for GitLab API responses
33 | REQUEST_TIMEOUT=30
34 |
35 | # Optional: Maximum retries for failed requests
36 | MAX_RETRIES=3
37 |
38 | # Optional: User-Agent header for API requests
39 | # Helps GitLab identify your application
40 | USER_AGENT=GitLabMCPCodeReview/1.0
41 |
```
--------------------------------------------------------------------------------
/.gitlab-ci.yml:
--------------------------------------------------------------------------------
```yaml
1 | # GitLab CI/CD Pipeline for Code Review Quality Assurance
2 |
3 | stages:
4 | - validate
5 | - test
6 | - quality
7 | - security
8 | - deploy
9 |
10 | variables:
11 | PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
12 | PYTHON_VERSION: "3.12"
13 |
14 | cache:
15 | paths:
16 | - .cache/pip
17 | - venv/
18 |
19 | # 代码格式检查
20 | code_format:
21 | stage: validate
22 | image: python:${PYTHON_VERSION}
23 | before_script:
24 | - python -m pip install --upgrade pip
25 | - pip install black isort flake8
26 | script:
27 | - black --check --diff .
28 | - isort --check-only --diff .
29 | - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
30 | allow_failure: false
31 | rules:
32 | - if: $CI_PIPELINE_SOURCE == "merge_request_event"
33 | - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
34 |
35 | # 类型检查
36 | type_check:
37 | stage: validate
38 | image: python:${PYTHON_VERSION}
39 | before_script:
40 | - python -m pip install --upgrade pip
41 | - pip install mypy types-requests
42 | - pip install -r requirements.txt
43 | script:
44 | - mypy server.py --ignore-missing-imports
45 | allow_failure: true
46 | rules:
47 | - if: $CI_PIPELINE_SOURCE == "merge_request_event"
48 | - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
49 |
50 | # 单元测试
51 | unit_tests:
52 | stage: test
53 | image: python:${PYTHON_VERSION}
54 | before_script:
55 | - python -m pip install --upgrade pip
56 | - pip install -r requirements.txt
57 | - pip install -r requirements-dev.txt
58 | script:
59 | - python -m pytest tests/ -v --cov=. --cov-report=xml --cov-report=term
60 | coverage: '/TOTAL.*\s+(\d+%)$/'
61 | artifacts:
62 | reports:
63 | coverage_report:
64 | coverage_format: cobertura
65 | path: coverage.xml
66 | expire_in: 1 week
67 | rules:
68 | - if: $CI_PIPELINE_SOURCE == "merge_request_event"
69 | - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
70 |
71 | # 代码质量分析
72 | code_quality:
73 | stage: quality
74 | image: python:${PYTHON_VERSION}
75 | before_script:
76 | - python -m pip install --upgrade pip
77 | - pip install pylint radon xenon
78 | - pip install -r requirements.txt
79 | script:
80 | - pylint server.py --output-format=text --reports=yes --exit-zero > pylint-report.txt
81 | - radon cc server.py --show-complexity --average
82 | - radon mi server.py --show
83 | - xenon --max-absolute B --max-modules A --max-average A server.py
84 | artifacts:
85 | paths:
86 | - pylint-report.txt
87 | expire_in: 1 week
88 | allow_failure: true
89 | rules:
90 | - if: $CI_PIPELINE_SOURCE == "merge_request_event"
91 | - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
92 |
93 | # 安全扫描
94 | security_scan:
95 | stage: security
96 | image: python:${PYTHON_VERSION}
97 | before_script:
98 | - python -m pip install --upgrade pip
99 | - pip install bandit safety
100 | - pip install -r requirements.txt
101 | script:
102 | - bandit -r . -f json -o bandit-report.json || true
103 | - safety check --json --output safety-report.json || true
104 | - echo "Security scan completed"
105 | artifacts:
106 | paths:
107 | - bandit-report.json
108 | - safety-report.json
109 | expire_in: 1 week
110 | allow_failure: true
111 | rules:
112 | - if: $CI_PIPELINE_SOURCE == "merge_request_event"
113 | - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
114 |
115 | # 依赖扫描
116 | dependency_scan:
117 | stage: security
118 | image: python:${PYTHON_VERSION}
119 | before_script:
120 | - python -m pip install --upgrade pip
121 | - pip install pip-audit
122 | script:
123 | - pip-audit --output=json --output-file=dependency-audit.json || true
124 | artifacts:
125 | paths:
126 | - dependency-audit.json
127 | expire_in: 1 week
128 | allow_failure: true
129 | rules:
130 | - if: $CI_PIPELINE_SOURCE == "merge_request_event"
131 | - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
132 |
133 | # MCP服务器功能测试
134 | mcp_server_test:
135 | stage: test
136 | image: python:${PYTHON_VERSION}
137 | before_script:
138 | - python -m pip install --upgrade pip
139 | - pip install -r requirements.txt
140 | - pip install -r requirements-dev.txt
141 | # 创建测试用的环境变量
142 | - echo "GITLAB_TOKEN=test_token" > .env
143 | - echo "GITLAB_HOST=gitlab.example.com" >> .env
144 | script:
145 | - python -c "import server; print('Server imports successfully')"
146 | - python -m pytest tests/test_server.py -v
147 | allow_failure: false
148 | rules:
149 | - if: $CI_PIPELINE_SOURCE == "merge_request_event"
150 | - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
151 |
152 | # 文档检查
153 | docs_check:
154 | stage: validate
155 | image: python:${PYTHON_VERSION}
156 | before_script:
157 | - python -m pip install --upgrade pip
158 | - pip install pydocstyle
159 | script:
160 | - pydocstyle server.py --convention=google
161 | allow_failure: true
162 | rules:
163 | - if: $CI_PIPELINE_SOURCE == "merge_request_event"
164 | - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
165 |
166 | # 性能测试
167 | performance_test:
168 | stage: test
169 | image: python:${PYTHON_VERSION}
170 | before_script:
171 | - python -m pip install --upgrade pip
172 | - pip install -r requirements.txt
173 | - pip install memory-profiler psutil
174 | script:
175 | - python -c "
176 | import cProfile
177 | import pstats
178 | import server
179 | pr = cProfile.Profile()
180 | pr.enable()
181 | # 这里添加性能测试代码
182 | pr.disable()
183 | stats = pstats.Stats(pr)
184 | stats.sort_stats('cumulative')
185 | stats.print_stats(10)
186 | "
187 | allow_failure: true
188 | rules:
189 | - if: $CI_PIPELINE_SOURCE == "merge_request_event"
190 | - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
191 |
192 | # 代码审查提醒
193 | review_reminder:
194 | stage: quality
195 | image: alpine:latest
196 | before_script:
197 | - apk add --no-cache curl jq
198 | script:
199 | - |
200 | if [ "$CI_PIPELINE_SOURCE" = "merge_request_event" ]; then
201 | echo "🔍 代码审查提醒:"
202 | echo "✅ 请确保代码符合项目规范"
203 | echo "✅ 请检查安全性和性能"
204 | echo "✅ 请验证测试覆盖率"
205 | echo "✅ 请确保文档更新"
206 | echo "📋 参考: CODE_REVIEW_GUIDELINES.md"
207 | fi
208 | rules:
209 | - if: $CI_PIPELINE_SOURCE == "merge_request_event"
210 |
211 | # 部署到开发环境
212 | deploy_dev:
213 | stage: deploy
214 | image: alpine:latest
215 | script:
216 | - echo "部署到开发环境"
217 | - echo "验证MCP服务器配置"
218 | - echo "检查环境变量设置"
219 | environment:
220 | name: development
221 | url: https://dev.example.com
222 | rules:
223 | - if: $CI_COMMIT_BRANCH == "develop"
224 | when: manual
225 |
226 | # 部署到生产环境
227 | deploy_prod:
228 | stage: deploy
229 | image: alpine:latest
230 | script:
231 | - echo "部署到生产环境"
232 | - echo "验证生产环境配置"
233 | - echo "执行健康检查"
234 | environment:
235 | name: production
236 | url: https://prod.example.com
237 | rules:
238 | - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
239 | when: manual
240 | only:
241 | - main
242 | - master
243 |
244 | # 代码覆盖率检查
245 | coverage_check:
246 | stage: quality
247 | image: python:${PYTHON_VERSION}
248 | before_script:
249 | - python -m pip install --upgrade pip
250 | - pip install -r requirements.txt
251 | - pip install -r requirements-dev.txt
252 | script:
253 | - python -m pytest tests/ --cov=. --cov-report=term --cov-fail-under=70
254 | rules:
255 | - if: $CI_PIPELINE_SOURCE == "merge_request_event"
256 | - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
257 | allow_failure: true
258 |
259 | # 生成代码质量报告
260 | quality_report:
261 | stage: quality
262 | image: python:${PYTHON_VERSION}
263 | before_script:
264 | - python -m pip install --upgrade pip
265 | - pip install -r requirements.txt
266 | script:
267 | - |
268 | echo "📊 代码质量报告" > quality-report.md
269 | echo "==================" >> quality-report.md
270 | echo "" >> quality-report.md
271 | echo "## 检查项目:" >> quality-report.md
272 | echo "- ✅ 代码格式检查" >> quality-report.md
273 | echo "- ✅ 类型检查" >> quality-report.md
274 | echo "- ✅ 单元测试" >> quality-report.md
275 | echo "- ✅ 代码质量分析" >> quality-report.md
276 | echo "- ✅ 安全扫描" >> quality-report.md
277 | echo "- ✅ 依赖检查" >> quality-report.md
278 | echo "" >> quality-report.md
279 | echo "详细报告请查看各个阶段的输出。" >> quality-report.md
280 | artifacts:
281 | paths:
282 | - quality-report.md
283 | expire_in: 1 week
284 | rules:
285 | - if: $CI_PIPELINE_SOURCE == "merge_request_event"
286 |
```
--------------------------------------------------------------------------------
/.gitlabci.yml:
--------------------------------------------------------------------------------
```yaml
1 | # GitLab CI/CD Pipeline for Code Review Quality Assurance
2 |
3 | stages:
4 | - validate
5 | - test
6 | - quality
7 | - security
8 | - deploy
9 |
10 | variables:
11 | PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
12 | PYTHON_VERSION: "3.12"
13 |
14 | cache:
15 | paths:
16 | - .cache/pip
17 | - venv/
18 |
19 | # 代码格式检查
20 | code_format:
21 | stage: validate
22 | image: python:${PYTHON_VERSION}
23 | before_script:
24 | - python -m pip install --upgrade pip
25 | - pip install black isort flake8
26 | script:
27 | - black --check --diff .
28 | - isort --check-only --diff .
29 | - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
30 | allow_failure: false
31 | rules:
32 | - if: $CI_PIPELINE_SOURCE == "merge_request_event"
33 | - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
34 |
35 | # 类型检查
36 | type_check:
37 | stage: validate
38 | image: python:${PYTHON_VERSION}
39 | before_script:
40 | - python -m pip install --upgrade pip
41 | - pip install mypy types-requests
42 | - pip install -r requirements.txt
43 | script:
44 | - mypy server.py --ignore-missing-imports
45 | allow_failure: true
46 | rules:
47 | - if: $CI_PIPELINE_SOURCE == "merge_request_event"
48 | - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
49 |
50 | # 单元测试
51 | unit_tests:
52 | stage: test
53 | image: python:${PYTHON_VERSION}
54 | before_script:
55 | - python -m pip install --upgrade pip
56 | - pip install -r requirements.txt
57 | - pip install -r requirements-dev.txt
58 | script:
59 | - python -m pytest tests/ -v --cov=. --cov-report=xml --cov-report=term
60 | coverage: '/TOTAL.*\s+(\d+%)$/'
61 | artifacts:
62 | reports:
63 | coverage_report:
64 | coverage_format: cobertura
65 | path: coverage.xml
66 | expire_in: 1 week
67 | rules:
68 | - if: $CI_PIPELINE_SOURCE == "merge_request_event"
69 | - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
70 |
71 | # 代码质量分析
72 | code_quality:
73 | stage: quality
74 | image: python:${PYTHON_VERSION}
75 | before_script:
76 | - python -m pip install --upgrade pip
77 | - pip install pylint radon xenon
78 | - pip install -r requirements.txt
79 | script:
80 | - pylint server.py --output-format=text --reports=yes --exit-zero > pylint-report.txt
81 | - radon cc server.py --show-complexity --average
82 | - radon mi server.py --show
83 | - xenon --max-absolute B --max-modules A --max-average A server.py
84 | artifacts:
85 | paths:
86 | - pylint-report.txt
87 | expire_in: 1 week
88 | allow_failure: true
89 | rules:
90 | - if: $CI_PIPELINE_SOURCE == "merge_request_event"
91 | - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
92 |
93 | # 安全扫描
94 | security_scan:
95 | stage: security
96 | image: python:${PYTHON_VERSION}
97 | before_script:
98 | - python -m pip install --upgrade pip
99 | - pip install bandit safety
100 | - pip install -r requirements.txt
101 | script:
102 | - bandit -r . -f json -o bandit-report.json || true
103 | - safety check --json --output safety-report.json || true
104 | - echo "Security scan completed"
105 | artifacts:
106 | paths:
107 | - bandit-report.json
108 | - safety-report.json
109 | expire_in: 1 week
110 | allow_failure: true
111 | rules:
112 | - if: $CI_PIPELINE_SOURCE == "merge_request_event"
113 | - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
114 |
115 | # 依赖扫描
116 | dependency_scan:
117 | stage: security
118 | image: python:${PYTHON_VERSION}
119 | before_script:
120 | - python -m pip install --upgrade pip
121 | - pip install pip-audit
122 | script:
123 | - pip-audit --output=json --output-file=dependency-audit.json || true
124 | artifacts:
125 | paths:
126 | - dependency-audit.json
127 | expire_in: 1 week
128 | allow_failure: true
129 | rules:
130 | - if: $CI_PIPELINE_SOURCE == "merge_request_event"
131 | - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
132 |
133 | # MCP服务器功能测试
134 | mcp_server_test:
135 | stage: test
136 | image: python:${PYTHON_VERSION}
137 | before_script:
138 | - python -m pip install --upgrade pip
139 | - pip install -r requirements.txt
140 | - pip install -r requirements-dev.txt
141 | # 创建测试用的环境变量
142 | - echo "GITLAB_TOKEN=test_token" > .env
143 | - echo "GITLAB_HOST=gitlab.example.com" >> .env
144 | script:
145 | - python -c "import server; print('Server imports successfully')"
146 | - python -m pytest tests/test_server.py -v
147 | allow_failure: false
148 | rules:
149 | - if: $CI_PIPELINE_SOURCE == "merge_request_event"
150 | - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
151 |
152 | # 文档检查
153 | docs_check:
154 | stage: validate
155 | image: python:${PYTHON_VERSION}
156 | before_script:
157 | - python -m pip install --upgrade pip
158 | - pip install pydocstyle
159 | script:
160 | - pydocstyle server.py --convention=google
161 | allow_failure: true
162 | rules:
163 | - if: $CI_PIPELINE_SOURCE == "merge_request_event"
164 | - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
165 |
166 | # 性能测试
167 | performance_test:
168 | stage: test
169 | image: python:${PYTHON_VERSION}
170 | before_script:
171 | - python -m pip install --upgrade pip
172 | - pip install -r requirements.txt
173 | - pip install memory-profiler psutil
174 | script:
175 | - python -c "
176 | import cProfile
177 | import pstats
178 | import server
179 | pr = cProfile.Profile()
180 | pr.enable()
181 | # 这里添加性能测试代码
182 | pr.disable()
183 | stats = pstats.Stats(pr)
184 | stats.sort_stats('cumulative')
185 | stats.print_stats(10)
186 | "
187 | allow_failure: true
188 | rules:
189 | - if: $CI_PIPELINE_SOURCE == "merge_request_event"
190 | - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
191 |
192 | # 代码审查提醒
193 | review_reminder:
194 | stage: quality
195 | image: alpine:latest
196 | before_script:
197 | - apk add --no-cache curl jq
198 | script:
199 | - |
200 | if [ "$CI_PIPELINE_SOURCE" = "merge_request_event" ]; then
201 | echo "🔍 代码审查提醒:"
202 | echo "✅ 请确保代码符合项目规范"
203 | echo "✅ 请检查安全性和性能"
204 | echo "✅ 请验证测试覆盖率"
205 | echo "✅ 请确保文档更新"
206 | echo "📋 参考: CODE_REVIEW_GUIDELINES.md"
207 | fi
208 | rules:
209 | - if: $CI_PIPELINE_SOURCE == "merge_request_event"
210 |
211 | # 部署到开发环境
212 | deploy_dev:
213 | stage: deploy
214 | image: alpine:latest
215 | script:
216 | - echo "部署到开发环境"
217 | - echo "验证MCP服务器配置"
218 | - echo "检查环境变量设置"
219 | environment:
220 | name: development
221 | url: https://dev.example.com
222 | rules:
223 | - if: $CI_COMMIT_BRANCH == "develop"
224 | when: manual
225 |
226 | # 部署到生产环境
227 | deploy_prod:
228 | stage: deploy
229 | image: alpine:latest
230 | script:
231 | - echo "部署到生产环境"
232 | - echo "验证生产环境配置"
233 | - echo "执行健康检查"
234 | environment:
235 | name: production
236 | url: https://prod.example.com
237 | rules:
238 | - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
239 | when: manual
240 | only:
241 | - main
242 | - master
243 |
244 | # 代码覆盖率检查
245 | coverage_check:
246 | stage: quality
247 | image: python:${PYTHON_VERSION}
248 | before_script:
249 | - python -m pip install --upgrade pip
250 | - pip install -r requirements.txt
251 | - pip install -r requirements-dev.txt
252 | script:
253 | - python -m pytest tests/ --cov=. --cov-report=term --cov-fail-under=70
254 | rules:
255 | - if: $CI_PIPELINE_SOURCE == "merge_request_event"
256 | - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
257 | allow_failure: true
258 |
259 | # 生成代码质量报告
260 | quality_report:
261 | stage: quality
262 | image: python:${PYTHON_VERSION}
263 | before_script:
264 | - python -m pip install --upgrade pip
265 | - pip install -r requirements.txt
266 | script:
267 | - |
268 | echo "📊 代码质量报告" > quality-report.md
269 | echo "==================" >> quality-report.md
270 | echo "" >> quality-report.md
271 | echo "## 检查项目:" >> quality-report.md
272 | echo "- ✅ 代码格式检查" >> quality-report.md
273 | echo "- ✅ 类型检查" >> quality-report.md
274 | echo "- ✅ 单元测试" >> quality-report.md
275 | echo "- ✅ 代码质量分析" >> quality-report.md
276 | echo "- ✅ 安全扫描" >> quality-report.md
277 | echo "- ✅ 依赖检查" >> quality-report.md
278 | echo "" >> quality-report.md
279 | echo "详细报告请查看各个阶段的输出。" >> quality-report.md
280 | artifacts:
281 | paths:
282 | - quality-report.md
283 | expire_in: 1 week
284 | rules:
285 | - if: $CI_PIPELINE_SOURCE == "merge_request_event"
286 |
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # GitLab MCP for Code Review
2 |
3 | [](https://www.python.org/downloads/)
4 | [](https://opensource.org/licenses/MIT)
5 |
6 | > This project is forked from [cayirtepeomer/gerrit-code-review-mcp](https://github.com/cayirtepeomer/gerrit-code-review-mcp) and adapted for GitLab integration.
7 |
8 | 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.
9 |
10 | ## Features
11 |
12 | - **Complete Merge Request Analysis**: Fetch full details about merge requests including diffs, commits, and comments
13 | - **File-Specific Diffs**: Analyze changes to specific files within merge requests
14 | - **Version Comparison**: Compare different branches, tags, or commits
15 | - **Review Management**: Add comments, approve, or unapprove merge requests
16 | - **Project Overview**: Get lists of all merge requests in a project
17 |
18 | ## Installation
19 |
20 | ### Prerequisites
21 |
22 | - Python 3.10+
23 | - GitLab personal access token with API scope (read_api, api)
24 | - [Cursor IDE](https://cursor.sh/) or [Claude Desktop App](https://claude.ai/desktop) for MCP integration
25 |
26 | ### Quick Start
27 |
28 | 1. Clone this repository:
29 |
30 | ```bash
31 | git clone https://github.com/lininn/gitlab-code-review-mcp.git
32 | cd gitlab-mcp-code-review
33 | ```
34 |
35 | 2. Create and activate a virtual environment:
36 |
37 | ```bash
38 | python -m venv .venv
39 | source .venv/bin/activate # On Windows: .venv\Scripts\activate
40 | ```
41 |
42 | 3. Install dependencies:
43 |
44 | ```bash
45 | pip install -r requirements.txt
46 | ```
47 |
48 | 4. Create a `.env` file with your GitLab configuration (see `.env.example` for all options):
49 |
50 | ```
51 | # Required
52 | GITLAB_TOKEN=your_personal_access_token_here
53 |
54 | # Optional settings
55 | GITLAB_HOST=gitlab.com
56 | GITLAB_API_VERSION=v4
57 | LOG_LEVEL=INFO
58 | ```
59 |
60 | ## Configuration Options
61 |
62 | The following environment variables can be configured in your `.env` file:
63 |
64 | | Variable | Required | Default | Description |
65 | |----------|----------|---------|-------------|
66 | | GITLAB_TOKEN | Yes | - | Your GitLab personal access token |
67 | | GITLAB_HOST | No | gitlab.com | GitLab instance hostname |
68 | | GITLAB_API_VERSION | No | v4 | GitLab API version to use |
69 | | LOG_LEVEL | No | INFO | Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL) |
70 | | DEBUG | No | false | Enable debug mode |
71 | | REQUEST_TIMEOUT | No | 30 | API request timeout in seconds |
72 | | MAX_RETRIES | No | 3 | Maximum retry attempts for failed requests |
73 |
74 | ## Cursor IDE Integration
75 |
76 | To use this MCP with Cursor IDE, add this configuration to your `~/.cursor/mcp.json` file:
77 |
78 | ```json
79 | {
80 | "mcpServers": {
81 | "gitlab-mcp-code-review": {
82 | "command": "/path/to/your/gitlab-mcp-code-review/.venv/bin/python",
83 | "args": [
84 | "/path/to/your/gitlab-mcp-code-review/server.py",
85 | "--transport",
86 | "stdio"
87 | ],
88 | "cwd": "/path/to/your/gitlab-mcp-code-review",
89 | "env": {
90 | "PYTHONPATH": "/path/to/your/gitlab-mcp-code-review",
91 | "VIRTUAL_ENV": "/path/to/your/gitlab-mcp-code-review/.venv",
92 | "PATH": "/path/to/your/gitlab-mcp-code-review/.venv/bin:/usr/local/bin:/usr/bin:/bin"
93 | },
94 | "stdio": true
95 | }
96 | }
97 | }
98 | ```
99 |
100 | Replace `/path/to/your/gitlab-mcp-code-review` with the actual path to your cloned repository.
101 |
102 | ## Claude Desktop App Integration
103 |
104 | To use this MCP with the Claude Desktop App:
105 |
106 | 1. Open the Claude Desktop App
107 | 2. Go to Settings → Advanced → MCP Configuration
108 | 3. Add the following configuration:
109 |
110 | ```json
111 | {
112 | "mcpServers": {
113 | "gitlab-mcp-code-review": {
114 | "command": "/path/to/your/gitlab-mcp-code-review/.venv/bin/python",
115 | "args": [
116 | "/path/to/your/gitlab-mcp-code-review/server.py",
117 | "--transport",
118 | "stdio"
119 | ],
120 | "cwd": "/path/to/your/gitlab-mcp-code-review",
121 | "env": {
122 | "PYTHONPATH": "/path/to/your/gitlab-mcp-code-review",
123 | "VIRTUAL_ENV": "/path/to/your/gitlab-mcp-code-review/.venv",
124 | "PATH": "/path/to/your/gitlab-mcp-code-review/.venv/bin:/usr/local/bin:/usr/bin:/bin"
125 | },
126 | "stdio": true
127 | }
128 | }
129 | }
130 | ```
131 |
132 | Replace `/path/to/your/gitlab-mcp-code-review` with the actual path to your cloned repository.
133 |
134 | ## Available Tools
135 |
136 | The MCP server provides the following tools for interacting with GitLab:
137 |
138 | | Tool | Description |
139 | |------|-------------|
140 | | `fetch_merge_request` | Get complete information about a merge request |
141 | | `fetch_merge_request_diff` | Get diffs for a specific merge request |
142 | | `fetch_commit_diff` | Get diff information for a specific commit |
143 | | `compare_versions` | Compare different branches, tags, or commits |
144 | | `add_merge_request_comment` | Add a comment to a merge request |
145 | | `approve_merge_request` | Approve a merge request |
146 | | `unapprove_merge_request` | Unapprove a merge request |
147 | | `get_project_merge_requests` | Get a list of merge requests for a project |
148 |
149 | ## Usage Examples
150 |
151 | ### Fetch a Merge Request
152 |
153 | ```python
154 | # Get details of merge request #5 in project with ID 123
155 | mr = fetch_merge_request("123", "5")
156 | ```
157 |
158 | ### View Specific File Changes
159 |
160 | ```python
161 | # Get diff for a specific file in a merge request
162 | file_diff = fetch_merge_request_diff("123", "5", "path/to/file.js")
163 | ```
164 |
165 | ### Compare Branches
166 |
167 | ```python
168 | # Compare develop branch with master branch
169 | diff = compare_versions("123", "develop", "master")
170 | ```
171 |
172 | ### Add a Comment to a Merge Request
173 |
174 | ```python
175 | # Add a comment to a merge request
176 | comment = add_merge_request_comment("123", "5", "This code looks good!")
177 | ```
178 |
179 | ### Approve a Merge Request
180 |
181 | ```python
182 | # Approve a merge request and set required approvals to 2
183 | approval = approve_merge_request("123", "5", approvals_required=2)
184 | ```
185 |
186 | ## Troubleshooting
187 |
188 | If you encounter issues:
189 |
190 | 1. Verify your GitLab token has the appropriate permissions (api, read_api)
191 | 2. Check your `.env` file settings
192 | 3. Ensure your MCP configuration paths are correct
193 | 4. Test connection with: `curl -H "Private-Token: your-token" https://gitlab.com/api/v4/projects`
194 | 5. Set LOG_LEVEL=DEBUG in your .env file for more detailed logging
195 |
196 | ## Contributing
197 |
198 | Contributions are welcome! Please feel free to submit a Pull Request.
199 |
200 | 1. Fork the repository
201 | 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
202 | 3. Commit your changes (`git commit -m 'Add some amazing feature'`)
203 | 4. Push to the branch (`git push origin feature/amazing-feature`)
204 | 5. Open a Pull Request
205 |
206 | See the [CONTRIBUTING.md](CONTRIBUTING.md) file for more details on the development process.
207 |
208 | ## Code Review Standards
209 |
210 | This project follows strict code review standards to ensure quality and maintainability:
211 |
212 | - 📋 **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).
213 | - ✅ **Review Checklist**: All pull requests should be checked against the [PULL_REQUEST_CHECKLIST.md](PULL_REQUEST_CHECKLIST.md) before submission.
214 | - 🔄 **CI/CD Pipeline**: We use GitLab CI for automated quality checks. Ensure all pipeline checks pass before requesting a review.
215 | - 📝 **Templates**: Please use the provided merge request and issue templates to ensure all necessary information is included.
216 |
217 | ### Quick Start for Contributors
218 |
219 | 1. Read the [Code Review Guidelines](CODE_REVIEW_GUIDELINES.md)
220 | 2. Use the appropriate MR template when creating pull requests
221 | 3. Ensure all CI checks pass before requesting review
222 | 4. Address all reviewer feedback promptly
223 |
224 | ## License
225 |
226 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
227 |
```
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
```markdown
1 | # Contributing to GitLab Code Review MCP
2 |
3 | Thank you for considering contributing to GitLab Code Review MCP! Here's how you can help:
4 |
5 | ## Development Process
6 |
7 | 1. Fork the repository
8 | 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
9 | 3. Set up the development environment:
10 | ```bash
11 | python -m venv .venv
12 | source .venv/bin/activate # On Windows: .venv\Scripts\activate
13 | pip install -e ".[dev]"
14 | ```
15 | 4. Make your changes
16 | 5. Run linting and tests:
17 | ```bash
18 | black .
19 | isort .
20 | mypy server.py
21 | pytest
22 | ```
23 | 6. Commit your changes with meaningful commit messages:
24 | ```bash
25 | git commit -m "Add some amazing feature"
26 | ```
27 | 7. Push to your branch:
28 | ```bash
29 | git push origin feature/amazing-feature
30 | ```
31 | 8. Open a Pull Request
32 |
33 | ## Pull Request Guidelines
34 |
35 | - Update the README.md if needed
36 | - Keep pull requests focused on a single change
37 | - Write tests for your changes when possible
38 | - Document new code based using docstrings
39 | - End all files with a newline
40 |
41 | ## Code Style
42 |
43 | This project uses:
44 | - Black for code formatting
45 | - isort for import sorting
46 | - mypy for type checking
47 |
48 | ## License
49 |
50 | By contributing, you agree that your contributions will be licensed under the project's MIT License.
```
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
```python
1 | # Tests package
```
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
```python
1 | from setuptools import setup
2 |
3 | if __name__ == "__main__":
4 | setup()
```
--------------------------------------------------------------------------------
/pytest.ini:
--------------------------------------------------------------------------------
```
1 | [pytest]
2 | testpaths = tests
3 | python_files = test_*.py
4 | python_classes = Test*
5 | python_functions = test_*
6 | addopts = --cov=. --cov-report=term-missing
```
--------------------------------------------------------------------------------
/requirements-dev.txt:
--------------------------------------------------------------------------------
```
1 | # Include main requirements
2 | -r requirements.txt
3 |
4 | # Testing
5 | pytest>=7.0.0
6 | pytest-cov>=4.0.0
7 |
8 | # Code quality
9 | black>=23.0.0
10 | isort>=5.0.0
11 | mypy>=1.0.0
12 | pre-commit>=3.0.0
```
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
```
1 | # Core dependencies
2 | mcp[cli]>=1.6.0
3 | python-dotenv>=1.0.0
4 | requests>=2.31.0
5 |
6 | # Development dependencies (optional)
7 | # Install with: pip install -r requirements-dev.txt
```
--------------------------------------------------------------------------------
/mcp-config-example.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "mcpServers": {
3 | "gitlab-review-mcp": {
4 | "command": "${WORKSPACE_PATH}/.venv/bin/python",
5 | "args": [
6 | "${WORKSPACE_PATH}/server.py",
7 | "--transport",
8 | "stdio"
9 | ],
10 | "cwd": "${WORKSPACE_PATH}",
11 | "env": {
12 | "PYTHONPATH": "${WORKSPACE_PATH}",
13 | "VIRTUAL_ENV": "${WORKSPACE_PATH}/.venv",
14 | "PATH": "${WORKSPACE_PATH}/.venv/bin:/usr/local/bin:/usr/bin:/bin"
15 | },
16 | "stdio": true
17 | }
18 | }
19 | }
```
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
```markdown
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: '[FEATURE] '
5 | labels: enhancement
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
```
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
```markdown
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: '[BUG] '
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Import '...'
16 | 2. Call function '....'
17 | 3. Pass arguments '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Error messages**
24 | If applicable, add error messages or exception tracebacks.
25 |
26 | **Environment:**
27 | - OS: [e.g. Ubuntu 22.04, macOS 13.0]
28 | - Python version: [e.g. 3.10.5]
29 | - GitLab API version: [e.g. v4]
30 | - Package version: [e.g. 0.1.0]
31 |
32 | **Additional context**
33 | Add any other context about the problem here.
```
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
```toml
1 | [project]
2 | name = "gitlab-mcp-code-review"
3 | version = "0.1.0"
4 | description = "MCP server for GitLab Code Review"
5 | authors = [{name = "GitLab MCP Code Review Contributors"}]
6 | license = "MIT"
7 | readme = "README.md"
8 | requires-python = ">=3.10"
9 | classifiers = [
10 | "Development Status :: 4 - Beta",
11 | "Intended Audience :: Developers",
12 | "License :: OSI Approved :: MIT License",
13 | "Programming Language :: Python :: 3.10",
14 | "Programming Language :: Python :: 3.11",
15 | "Programming Language :: Python :: 3.12",
16 | ]
17 | dependencies = [
18 | "mcp[cli]>=1.6.0",
19 | "python-dotenv>=1.0.0",
20 | "requests>=2.31.0",
21 | ]
22 |
23 | [project.optional-dependencies]
24 | dev = [
25 | "pytest>=7.0.0",
26 | "pytest-cov>=4.0.0",
27 | "black>=23.0.0",
28 | "isort>=5.0.0",
29 | "mypy>=1.0.0",
30 | "pre-commit>=3.0.0",
31 | ]
32 |
33 | [build-system]
34 | requires = ["setuptools>=42", "wheel"]
35 | build-backend = "setuptools.build_meta"
36 |
37 | [tool.black]
38 | line-length = 88
39 |
40 | [tool.isort]
41 | profile = "black"
42 |
43 | [tool.mypy]
44 | strict = true
45 |
```
--------------------------------------------------------------------------------
/tests/test_server.py:
--------------------------------------------------------------------------------
```python
1 | import unittest
2 | from unittest.mock import patch, MagicMock
3 |
4 | # This is a basic test skeleton for the server
5 | # You would need to add more comprehensive tests
6 |
7 |
8 | class TestGitLabMCP(unittest.TestCase):
9 | """Test cases for GitLab MCP server"""
10 |
11 | def setUp(self):
12 | """Set up test fixtures"""
13 | self.mock_ctx = MagicMock()
14 | self.mock_lifespan_context = MagicMock()
15 | self.mock_ctx.request_context.lifespan_context = self.mock_lifespan_context
16 | self.mock_lifespan_context.token = "fake_token"
17 | self.mock_lifespan_context.host = "gitlab.com"
18 |
19 | @patch('requests.get')
20 | def test_make_gitlab_api_request(self, mock_get):
21 | """Test the GitLab API request function"""
22 | # Import here to avoid module-level imports before patching
23 | from server import make_gitlab_api_request
24 |
25 | # Setup mock response
26 | mock_response = MagicMock()
27 | mock_response.status_code = 200
28 | mock_response.json.return_value = {"id": 123, "name": "test_project"}
29 | mock_get.return_value = mock_response
30 |
31 | # Test the function
32 | result = make_gitlab_api_request(self.mock_ctx, "projects/123")
33 |
34 | # Assertions
35 | mock_get.assert_called_once()
36 | self.assertEqual(result, {"id": 123, "name": "test_project"})
37 |
38 |
39 | if __name__ == '__main__':
40 | unittest.main()
```
--------------------------------------------------------------------------------
/test_gitlab_connection.py:
--------------------------------------------------------------------------------
```python
1 | import os
2 | import requests
3 | from dotenv import load_dotenv
4 |
5 | # Load environment variables
6 | load_dotenv()
7 |
8 | # Get configuration from environment
9 | GITLAB_HOST = os.getenv("GITLAB_HOST", "gitlab.com")
10 | GITLAB_TOKEN = os.getenv("GITLAB_TOKEN")
11 | API_VERSION = os.getenv("GITLAB_API_VERSION", "v4")
12 |
13 | if not GITLAB_TOKEN:
14 | print("Error: GITLAB_TOKEN not set in environment")
15 | exit(1)
16 |
17 | # Remove https:// prefix if present for API calls
18 | if GITLAB_HOST.startswith("https://"):
19 | GITLAB_HOST = GITLAB_HOST.replace("https://", "")
20 | elif GITLAB_HOST.startswith("http://"):
21 | GITLAB_HOST = GITLAB_HOST.replace("http://", "")
22 |
23 | print(f"Testing connection to GitLab host: {GITLAB_HOST}")
24 | print(f"Using API version: {API_VERSION}")
25 |
26 | # Test basic API connection
27 | url = f"https://{GITLAB_HOST}/api/{API_VERSION}/version"
28 | headers = {
29 | 'Accept': 'application/json',
30 | 'User-Agent': 'GitLabConnectionTest/1.0',
31 | 'Private-Token': GITLAB_TOKEN
32 | }
33 |
34 | try:
35 | print(f"Making request to: {url}")
36 | response = requests.get(url, headers=headers, verify=True, timeout=30)
37 |
38 | if response.status_code == 200:
39 | print("✅ Connection successful!")
40 | version_info = response.json()
41 | print(f"GitLab version: {version_info.get('version', 'Unknown')}")
42 | print(f"Revision: {version_info.get('revision', 'Unknown')}")
43 | else:
44 | print(f"❌ Connection failed with status code: {response.status_code}")
45 | print(f"Response: {response.text}")
46 |
47 | except requests.exceptions.RequestException as e:
48 | print(f"❌ Request failed: {str(e)}")
49 | if hasattr(e, 'response') and e.response:
50 | print(f"Response status: {e.response.status_code}")
51 | print(f"Response text: {e.response.text}")
52 |
53 | # Test project access
54 | project_id = "front-end/wmflight"
55 | project_url = f"https://{GITLAB_HOST}/api/{API_VERSION}/projects/{project_id.replace('/', '%2F')}"
56 |
57 | print(f"\nTesting access to project: {project_id}")
58 | print(f"Project URL: {project_url}")
59 |
60 | try:
61 | response = requests.get(project_url, headers=headers, verify=True, timeout=30)
62 |
63 | if response.status_code == 200:
64 | project_info = response.json()
65 | print("✅ Project access successful!")
66 | print(f"Project name: {project_info.get('name', 'Unknown')}")
67 | print(f"Project ID: {project_info.get('id', 'Unknown')}")
68 | print(f"Visibility: {project_info.get('visibility', 'Unknown')}")
69 | else:
70 | print(f"❌ Project access failed with status code: {response.status_code}")
71 | print(f"Response: {response.text}")
72 |
73 | except requests.exceptions.RequestException as e:
74 | print(f"❌ Project request failed: {str(e)}")
```
--------------------------------------------------------------------------------
/fetch_mr_details.py:
--------------------------------------------------------------------------------
```python
1 | import os
2 | import requests
3 | import json
4 | from dotenv import load_dotenv
5 | from urllib.parse import quote
6 |
7 | # Load environment variables
8 | load_dotenv()
9 |
10 | # Get configuration from environment
11 | GITLAB_HOST = os.getenv("GITLAB_HOST", "gitlab.com")
12 | GITLAB_TOKEN = os.getenv("GITLAB_TOKEN")
13 | API_VERSION = os.getenv("GITLAB_API_VERSION", "v4")
14 |
15 | if not GITLAB_TOKEN:
16 | print("Error: GITLAB_TOKEN not set in environment")
17 | exit(1)
18 |
19 | # Remove https:// prefix if present for API calls
20 | if GITLAB_HOST.startswith("https://"):
21 | GITLAB_HOST = GITLAB_HOST.replace("https://", "")
22 | elif GITLAB_HOST.startswith("http://"):
23 | GITLAB_HOST = GITLAB_HOST.replace("http://", "")
24 |
25 | headers = {
26 | 'Accept': 'application/json',
27 | 'User-Agent': 'GitLabMRReview/1.0',
28 | 'Private-Token': GITLAB_TOKEN
29 | }
30 |
31 | def make_request(url):
32 | """Make API request and handle response"""
33 | try:
34 | response = requests.get(url, headers=headers, verify=True, timeout=30)
35 | response.raise_for_status()
36 | return response.json()
37 | except requests.exceptions.RequestException as e:
38 | print(f"Request failed: {str(e)}")
39 | if hasattr(e, 'response') and e.response:
40 | print(f"Status code: {e.response.status_code}")
41 | print(f"Response: {e.response.text}")
42 | return None
43 |
44 | def fetch_merge_request_details(project_id, mr_iid):
45 | """Fetch complete details for a merge request"""
46 | print(f"Fetching details for merge request #{mr_iid} in project {project_id}")
47 |
48 | # Get merge request basic info
49 | mr_endpoint = f"https://{GITLAB_HOST}/api/{API_VERSION}/projects/{quote(project_id, safe='')}/merge_requests/{mr_iid}"
50 | mr_info = make_request(mr_endpoint)
51 |
52 | if not mr_info:
53 | print("Failed to get merge request info")
54 | return None
55 |
56 | # Get changes/diffs
57 | changes_endpoint = f"{mr_endpoint}/changes"
58 | changes_info = make_request(changes_endpoint)
59 |
60 | # Get commits
61 | commits_endpoint = f"{mr_endpoint}/commits"
62 | commits_info = make_request(commits_endpoint)
63 |
64 | # Get notes/comments
65 | notes_endpoint = f"{mr_endpoint}/notes"
66 | notes_info = make_request(notes_endpoint)
67 |
68 | return {
69 | "merge_request": mr_info,
70 | "changes": changes_info,
71 | "commits": commits_info,
72 | "notes": notes_info
73 | }
74 |
75 | def print_mr_summary(mr_data):
76 | """Print a summary of the merge request"""
77 | if not mr_data:
78 | return
79 |
80 | mr_info = mr_data["merge_request"]
81 | changes = mr_data["changes"]
82 | commits = mr_data["commits"]
83 | notes = mr_data["notes"]
84 |
85 | print("\n" + "="*80)
86 | print("MERGE REQUEST SUMMARY")
87 | print("="*80)
88 | print(f"Title: {mr_info.get('title', 'No title')}")
89 | print(f"Author: {mr_info.get('author', {}).get('name', 'Unknown')}")
90 | print(f"State: {mr_info.get('state', 'Unknown')}")
91 | print(f"Created: {mr_info.get('created_at', 'Unknown')}")
92 | print(f"Updated: {mr_info.get('updated_at', 'Unknown')}")
93 | print(f"Source branch: {mr_info.get('source_branch', 'Unknown')}")
94 | print(f"Target branch: {mr_info.get('target_branch', 'Unknown')}")
95 | print(f"Merge status: {mr_info.get('merge_status', 'Unknown')}")
96 |
97 | print(f"\nChanges: {len(changes.get('changes', [])) if changes else 0} files changed")
98 | print(f"Commits: {len(commits) if commits else 0} commits")
99 | print(f"Comments: {len(notes) if notes else 0} notes")
100 |
101 | # Print changed files
102 | if changes and 'changes' in changes:
103 | print(f"\nChanged files:")
104 | for change in changes['changes']:
105 | old_path = change.get('old_path', 'N/A')
106 | new_path = change.get('new_path', 'N/A')
107 | diff = change.get('diff', '')
108 | lines_added = diff.count('\n+') - diff.count('\n+++')
109 | lines_removed = diff.count('\n-') - diff.count('\n---')
110 |
111 | print(f" - {new_path} ({old_path})")
112 | print(f" +{lines_added} -{lines_removed} lines")
113 |
114 | # Main execution
115 | if __name__ == "__main__":
116 | project_id = "front-end/wmflight"
117 | mr_iid = "924"
118 |
119 | mr_data = fetch_merge_request_details(project_id, mr_iid)
120 |
121 | if mr_data:
122 | print_mr_summary(mr_data)
123 |
124 | # Save detailed data to file for analysis
125 | with open(f"mr_{mr_iid}_details.json", "w", encoding="utf-8") as f:
126 | json.dump(mr_data, f, indent=2, ensure_ascii=False)
127 | print(f"\nDetailed data saved to mr_{mr_iid}_details.json")
128 | else:
129 | print("Failed to fetch merge request data")
```
--------------------------------------------------------------------------------
/server.py:
--------------------------------------------------------------------------------
```python
1 | import os
2 | import json
3 | import logging
4 | from typing import Optional, Dict, Any, Union, List
5 | from dataclasses import dataclass
6 | from contextlib import asynccontextmanager
7 | from collections.abc import AsyncIterator
8 | from urllib.parse import quote
9 | import requests
10 |
11 | from dotenv import load_dotenv
12 | from mcp.server.fastmcp import FastMCP, Context
13 |
14 | # Configure logging
15 | logging.basicConfig(level=logging.INFO)
16 | logger = logging.getLogger(__name__)
17 |
18 | # Load environment variables
19 | load_dotenv()
20 |
21 | @dataclass
22 | class GitLabContext:
23 | host: str
24 | token: str
25 | api_version: str = "v4"
26 |
27 | def make_gitlab_api_request(ctx: Context, endpoint: str, method: str = "GET", data: Optional[Dict[str, Any]] = None) -> Any:
28 | """Make a REST API request to GitLab and handle the response"""
29 | gitlab_ctx = ctx.request_context.lifespan_context
30 |
31 | if not gitlab_ctx.token:
32 | logger.error("GitLab token not set in context")
33 | raise ValueError("GitLab token not set. Please set GITLAB_TOKEN in your environment.")
34 |
35 | url = f"https://{gitlab_ctx.host}/api/{gitlab_ctx.api_version}/{endpoint}"
36 | headers = {
37 | 'Accept': 'application/json',
38 | 'User-Agent': 'GitLabMCPCodeReview/1.0',
39 | 'Private-Token': gitlab_ctx.token
40 | }
41 |
42 | try:
43 | logger.info(f"Making {method} request to {url}")
44 | logger.debug(f"Headers: {headers}")
45 | response = None
46 | if method.upper() == "GET":
47 | response = requests.get(url, headers=headers, verify=True)
48 | elif method.upper() == "POST":
49 | logger.debug(f"Request data: {data}")
50 | response = requests.post(url, headers=headers, json=data, verify=True)
51 | else:
52 | raise ValueError(f"Unsupported HTTP method: {method}")
53 |
54 | if response is None:
55 | logger.error("Request did not return a response.")
56 | raise Exception("Request did not return a response.")
57 |
58 | if response.status_code == 401:
59 | logger.error("Authentication failed. Check your GitLab token.")
60 | raise Exception("Authentication failed. Please check your GitLab token.")
61 |
62 | response.raise_for_status()
63 |
64 | if not response.content:
65 | return {}
66 |
67 | try:
68 | return response.json()
69 | except json.JSONDecodeError as e:
70 | logger.error(f"Failed to parse JSON response: {str(e)}")
71 | raise Exception(f"Failed to parse GitLab response as JSON: {str(e)}")
72 |
73 | except requests.exceptions.RequestException as e:
74 | logger.error(f"REST request failed: {str(e)}")
75 | if hasattr(e, 'response'):
76 | logger.error(f"Response status: {e.response.status_code}")
77 | raise Exception(f"Failed to make GitLab API request: {str(e)}")
78 |
79 | @asynccontextmanager
80 | async def gitlab_lifespan(server: FastMCP) -> AsyncIterator[GitLabContext]:
81 | """Manage GitLab connection details"""
82 | host = os.getenv("GITLAB_HOST", "gitlab.com")
83 | token = os.getenv("GITLAB_TOKEN", "")
84 |
85 | if not token:
86 | logger.error("Missing required environment variable: GITLAB_TOKEN")
87 | raise ValueError(
88 | "Missing required environment variable: GITLAB_TOKEN. "
89 | "Please set this in your environment or .env file."
90 | )
91 |
92 | ctx = GitLabContext(host=host, token=token)
93 | try:
94 | yield ctx
95 | finally:
96 | pass
97 |
98 | # Create MCP server
99 | mcp = FastMCP(
100 | name="GitLab MCP for Code Review",
101 | instructions="MCP server for reviewing GitLab code changes",
102 | lifespan=gitlab_lifespan,
103 | dependencies=["python-dotenv", "requests"]
104 | )
105 |
106 | @mcp.tool()
107 | def fetch_merge_request(ctx: Context, project_id: str, merge_request_iid: str) -> Dict[str, Any]:
108 | """
109 | Fetch a GitLab merge request and its contents.
110 |
111 | Args:
112 | project_id: The GitLab project ID or URL-encoded path
113 | merge_request_iid: The merge request IID (project-specific ID)
114 | Returns:
115 | Dict containing the merge request information
116 | """
117 | # Get merge request details
118 | mr_endpoint = f"projects/{quote(project_id, safe='')}/merge_requests/{merge_request_iid}"
119 | mr_info = make_gitlab_api_request(ctx, mr_endpoint)
120 |
121 | if not mr_info:
122 | raise ValueError(f"Merge request {merge_request_iid} not found in project {project_id}")
123 |
124 | # Get the changes (diffs) for this merge request
125 | changes_endpoint = f"{mr_endpoint}/changes"
126 | changes_info = make_gitlab_api_request(ctx, changes_endpoint)
127 |
128 | # Get the commit information
129 | commits_endpoint = f"{mr_endpoint}/commits"
130 | commits_info = make_gitlab_api_request(ctx, commits_endpoint)
131 |
132 | # Get the notes (comments) for this merge request
133 | notes_endpoint = f"{mr_endpoint}/notes"
134 | notes_info = make_gitlab_api_request(ctx, notes_endpoint)
135 |
136 | return {
137 | "merge_request": mr_info,
138 | "changes": changes_info,
139 | "commits": commits_info,
140 | "notes": notes_info
141 | }
142 |
143 | @mcp.tool()
144 | def fetch_merge_request_diff(ctx: Context, project_id: str, merge_request_iid: str, file_path: Optional[str] = None) -> Dict[str, Any]:
145 | """
146 | Fetch the diff for a specific file in a merge request, or all files if none specified.
147 |
148 | Args:
149 | project_id: The GitLab project ID or URL-encoded path
150 | merge_request_iid: The merge request IID (project-specific ID)
151 | file_path: Optional specific file path to get diff for
152 | Returns:
153 | Dict containing the diff information
154 | """
155 | # Get the changes for this merge request
156 | changes_endpoint = f"projects/{quote(project_id, safe='')}/merge_requests/{merge_request_iid}/changes"
157 | changes_info = make_gitlab_api_request(ctx, changes_endpoint)
158 |
159 | if not changes_info:
160 | raise ValueError(f"Changes not found for merge request {merge_request_iid}")
161 |
162 | # Extract all changes
163 | files = changes_info.get("changes", [])
164 |
165 | # Filter by file path if specified
166 | if file_path:
167 | files = [f for f in files if f.get("new_path") == file_path or f.get("old_path") == file_path]
168 | if not files:
169 | raise ValueError(f"File '{file_path}' not found in the merge request changes")
170 |
171 | return {
172 | "merge_request_iid": merge_request_iid,
173 | "files": files
174 | }
175 |
176 | @mcp.tool()
177 | def fetch_commit_diff(ctx: Context, project_id: str, commit_sha: str, file_path: Optional[str] = None) -> Dict[str, Any]:
178 | """
179 | Fetch the diff for a specific commit, or for a specific file in that commit.
180 |
181 | Args:
182 | project_id: The GitLab project ID or URL-encoded path
183 | commit_sha: The commit SHA
184 | file_path: Optional specific file path to get diff for
185 | Returns:
186 | Dict containing the diff information
187 | """
188 | # Get the diff for this commit
189 | diff_endpoint = f"projects/{quote(project_id, safe='')}/repository/commits/{commit_sha}/diff"
190 | diff_info = make_gitlab_api_request(ctx, diff_endpoint)
191 |
192 | if not diff_info:
193 | raise ValueError(f"Diff not found for commit {commit_sha}")
194 |
195 | # Filter by file path if specified
196 | if file_path:
197 | diff_info = [d for d in diff_info if d.get("new_path") == file_path or d.get("old_path") == file_path]
198 | if not diff_info:
199 | raise ValueError(f"File '{file_path}' not found in the commit diff")
200 |
201 | # Get the commit details
202 | commit_endpoint = f"projects/{quote(project_id, safe='')}/repository/commits/{commit_sha}"
203 | commit_info = make_gitlab_api_request(ctx, commit_endpoint)
204 |
205 | return {
206 | "commit": commit_info,
207 | "diffs": diff_info
208 | }
209 |
210 | @mcp.tool()
211 | def compare_versions(ctx: Context, project_id: str, from_sha: str, to_sha: str) -> Dict[str, Any]:
212 | """
213 | Compare two commits/branches/tags to see the differences between them.
214 |
215 | Args:
216 | project_id: The GitLab project ID or URL-encoded path
217 | from_sha: The source commit/branch/tag
218 | to_sha: The target commit/branch/tag
219 | Returns:
220 | Dict containing the comparison information
221 | """
222 | # Compare the versions
223 | compare_endpoint = f"projects/{quote(project_id, safe='')}/repository/compare?from={quote(from_sha, safe='')}&to={quote(to_sha, safe='')}"
224 | compare_info = make_gitlab_api_request(ctx, compare_endpoint)
225 |
226 | if not compare_info:
227 | raise ValueError(f"Comparison failed between {from_sha} and {to_sha}")
228 |
229 | return compare_info
230 |
231 | @mcp.tool()
232 | 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]:
233 | """
234 | Add a comment to a merge request, optionally at a specific position in a file.
235 |
236 | Args:
237 | project_id: The GitLab project ID or URL-encoded path
238 | merge_request_iid: The merge request IID (project-specific ID)
239 | body: The comment text
240 | position: Optional position data for line comments
241 | Returns:
242 | Dict containing the created comment information
243 | """
244 | # Create the comment data
245 | data = {
246 | "body": body
247 | }
248 |
249 | # Add position data if provided
250 | if position:
251 | data["position"] = position
252 |
253 | # Add the comment
254 | comment_endpoint = f"projects/{quote(project_id, safe='')}/merge_requests/{merge_request_iid}/notes"
255 | comment_info = make_gitlab_api_request(ctx, comment_endpoint, method="POST", data=data)
256 |
257 | if not comment_info:
258 | raise ValueError("Failed to add comment to merge request")
259 |
260 | return comment_info
261 |
262 | @mcp.tool()
263 | def approve_merge_request(ctx: Context, project_id: str, merge_request_iid: str, approvals_required: Optional[int] = None) -> Dict[str, Any]:
264 | """
265 | Approve a merge request.
266 |
267 | Args:
268 | project_id: The GitLab project ID or URL-encoded path
269 | merge_request_iid: The merge request IID (project-specific ID)
270 | approvals_required: Optional number of required approvals to set
271 | Returns:
272 | Dict containing the approval information
273 | """
274 | # Approve the merge request
275 | approve_endpoint = f"projects/{quote(project_id, safe='')}/merge_requests/{merge_request_iid}/approve"
276 | approve_info = make_gitlab_api_request(ctx, approve_endpoint, method="POST")
277 |
278 | # Set required approvals if specified
279 | if approvals_required is not None:
280 | approvals_endpoint = f"projects/{quote(project_id, safe='')}/merge_requests/{merge_request_iid}/approvals"
281 | data = {
282 | "approvals_required": approvals_required
283 | }
284 | make_gitlab_api_request(ctx, approvals_endpoint, method="POST", data=data)
285 |
286 | return approve_info
287 |
288 | @mcp.tool()
289 | def unapprove_merge_request(ctx: Context, project_id: str, merge_request_iid: str) -> Dict[str, Any]:
290 | """
291 | Unapprove a merge request.
292 |
293 | Args:
294 | project_id: The GitLab project ID or URL-encoded path
295 | merge_request_iid: The merge request IID (project-specific ID)
296 | Returns:
297 | Dict containing the unapproval information
298 | """
299 | # Unapprove the merge request
300 | unapprove_endpoint = f"projects/{quote(project_id, safe='')}/merge_requests/{merge_request_iid}/unapprove"
301 | unapprove_info = make_gitlab_api_request(ctx, unapprove_endpoint, method="POST")
302 |
303 | return unapprove_info
304 |
305 | @mcp.tool()
306 | def get_project_merge_requests(ctx: Context, project_id: str, state: str = "all", limit: int = 20) -> List[Dict[str, Any]]:
307 | """
308 | Get all merge requests for a project.
309 |
310 | Args:
311 | project_id: The GitLab project ID or URL-encoded path
312 | state: Filter merge requests by state (all, opened, closed, merged, or locked)
313 | limit: Maximum number of merge requests to return
314 | Returns:
315 | List of merge request objects
316 | """
317 | # Get the merge requests
318 | mrs_endpoint = f"projects/{quote(project_id, safe='')}/merge_requests?state={state}&per_page={limit}"
319 | mrs_info = make_gitlab_api_request(ctx, mrs_endpoint)
320 |
321 | return mrs_info
322 |
323 | @mcp.tool()
324 | def get_review_guidelines(ctx: Context) -> str:
325 | """
326 | Get the code review guidelines.
327 |
328 | Returns:
329 | The content of the code review guidelines file.
330 | """
331 | try:
332 | with open("CODE_REVIEW_GUIDELINES.md", "r", encoding="utf-8") as f:
333 | return f.read()
334 | except FileNotFoundError:
335 | logger.error("CODE_REVIEW_GUIDELINES.md not found.")
336 | raise FileNotFoundError("CODE_REVIEW_GUIDELINES.md not found.")
337 | except Exception as e:
338 | logger.error(f"Failed to read CODE_REVIEW_GUIDELINES.md: {str(e)}")
339 | raise
340 |
341 | if __name__ == "__main__":
342 | try:
343 | logger.info("Starting GitLab Review MCP server")
344 | # Initialize and run the server
345 | mcp.run(transport='stdio')
346 | except Exception as e:
347 | logger.error(f"Failed to start MCP server: {str(e)}")
348 | raise
```