#
tokens: 18022/50000 21/21 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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 | [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
  4 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
```