@zereight/mcp-gitlab 2.1.22 → 2.1.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +44 -44
- package/README.md +30 -24
- package/README.zh-CN.md +43 -43
- package/build/index.js +29 -14
- package/build/oauth.js +9 -9
- package/build/schemas.js +6 -3
- package/build/scripts/generate-tool-docs.js +404 -0
- package/build/test/test-oauth-proxy-rate-limit.js +133 -0
- package/build/test/utils/proxy-client-ip.test.js +28 -0
- package/build/utils/proxy-client-ip.js +11 -0
- package/package.json +3 -2
package/README.ko.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[English](./README.md) | [한국어](./README.ko.md) | [简体中文](./README.zh-CN.md)
|
|
4
4
|
|
|
5
|
-
> **새 기능**: 커넥션 풀링을 포함한 동적 GitLab API URL을 지원합니다. 자세한 내용은 [Dynamic API URL 문서](docs/dynamic-api-url.md)를 참고하세요.
|
|
5
|
+
> **새 기능**: 커넥션 풀링을 포함한 동적 GitLab API URL을 지원합니다. 자세한 내용은 [Dynamic API URL 문서](docs/configuration/dynamic-api-url.md)를 참고하세요.
|
|
6
6
|
|
|
7
7
|
[](https://www.star-history.com/#zereight/gitlab-mcp&Date)
|
|
8
8
|
|
|
@@ -24,14 +24,14 @@ PAT, OAuth, 읽기 전용 모드, 동적 API URL, 원격 인증을 지원하며
|
|
|
24
24
|
|
|
25
25
|
### 클라이언트 설정 가이드
|
|
26
26
|
|
|
27
|
-
- [Claude Code 설정 가이드](./docs/claude-code
|
|
28
|
-
- [VS Code 설정 가이드](./docs/vscode
|
|
29
|
-
- [GitHub Copilot 설정 가이드](./docs/copilot
|
|
30
|
-
- [Codex 설정 가이드](./docs/codex
|
|
31
|
-
- [Cursor 설정 가이드](./docs/cursor
|
|
32
|
-
- [JSON 기반 MCP 클라이언트 설정 가이드](./docs/json-
|
|
33
|
-
- [OAuth2 인증 설정 가이드](./docs/oauth-setup.md)
|
|
34
|
-
- [환경 변수 레퍼런스](./docs/environment-variables.md)
|
|
27
|
+
- [Claude Code 설정 가이드](./docs/clients/claude-code.md)
|
|
28
|
+
- [VS Code 설정 가이드](./docs/clients/vscode.md)
|
|
29
|
+
- [GitHub Copilot 설정 가이드](./docs/clients/copilot.md)
|
|
30
|
+
- [Codex 설정 가이드](./docs/clients/codex.md)
|
|
31
|
+
- [Cursor 설정 가이드](./docs/clients/cursor.md)
|
|
32
|
+
- [JSON 기반 MCP 클라이언트 설정 가이드](./docs/clients/json-clients.md) - Factory AI Droid, OpenClaw, OpenCode 스타일 클라이언트용
|
|
33
|
+
- [OAuth2 인증 설정 가이드](./docs/auth/oauth-setup.md)
|
|
34
|
+
- [환경 변수 레퍼런스](./docs/configuration/environment-variables.md)
|
|
35
35
|
|
|
36
36
|
## 사용법
|
|
37
37
|
|
|
@@ -53,13 +53,13 @@ PAT, OAuth, 읽기 전용 모드, 동적 API URL, 원격 인증을 지원하며
|
|
|
53
53
|
|
|
54
54
|
#### 빠른 설정 경로
|
|
55
55
|
|
|
56
|
-
- **Claude Code**: [Claude Code 설정 가이드](./docs/claude-code
|
|
57
|
-
- **VS Code**: [VS Code 설정 가이드](./docs/vscode
|
|
58
|
-
- **GitHub Copilot**: [GitHub Copilot 설정 가이드](./docs/copilot
|
|
59
|
-
- **Codex**: [Codex 설정 가이드](./docs/codex
|
|
60
|
-
- **Cursor**: [Cursor 설정 가이드](./docs/cursor
|
|
61
|
-
- **Factory AI Droid / OpenClaw / OpenCode 스타일 클라이언트**: [JSON 기반 MCP 클라이언트 설정 가이드](./docs/json-
|
|
62
|
-
- **OAuth 브라우저 플로우 상세**: [OAuth2 인증 설정 가이드](./docs/oauth-setup.md)
|
|
56
|
+
- **Claude Code**: [Claude Code 설정 가이드](./docs/clients/claude-code.md)
|
|
57
|
+
- **VS Code**: [VS Code 설정 가이드](./docs/clients/vscode.md)
|
|
58
|
+
- **GitHub Copilot**: [GitHub Copilot 설정 가이드](./docs/clients/copilot.md)
|
|
59
|
+
- **Codex**: [Codex 설정 가이드](./docs/clients/codex.md)
|
|
60
|
+
- **Cursor**: [Cursor 설정 가이드](./docs/clients/cursor.md)
|
|
61
|
+
- **Factory AI Droid / OpenClaw / OpenCode 스타일 클라이언트**: [JSON 기반 MCP 클라이언트 설정 가이드](./docs/clients/json-clients.md)
|
|
62
|
+
- **OAuth 브라우저 플로우 상세**: [OAuth2 인증 설정 가이드](./docs/auth/oauth-setup.md)
|
|
63
63
|
|
|
64
64
|
가장 단순한 로컬 설정은 Personal Access Token으로 시작하세요. 브라우저 기반 로컬 인증은 OAuth2를 사용하세요. 원격 또는 멀티 유저 배포는 아래 MCP OAuth 및 원격 인증 섹션을 참고하세요.
|
|
65
65
|
|
|
@@ -163,10 +163,10 @@ OpenCode, MCPJam, Claude.ai 같은 원격 MCP 클라이언트는 인증 중에
|
|
|
163
163
|
|
|
164
164
|
원격 MCP OAuth는 다릅니다. `GITLAB_MCP_OAUTH=true` 모드에서는 MCP 클라이언트가 `/authorize` 요청 중에 자체 callback URL을 제공합니다. `GITLAB_OAUTH_REDIRECT_URI`는 그 클라이언트 제공 URL을 대체하지 않습니다.
|
|
165
165
|
|
|
166
|
-
| 모드
|
|
167
|
-
|
|
|
168
|
-
| 로컬 OAuth
|
|
169
|
-
| 원격 MCP OAuth | `GITLAB_MCP_OAUTH=true` | `GITLAB_OAUTH_CALLBACK_PROXY=true` | `{MCP_SERVER_URL}/callback`
|
|
166
|
+
| 모드 | 활성화 변수 | Callback 변수 | GitLab Redirect URI |
|
|
167
|
+
| -------------- | ----------------------- | ---------------------------------- | --------------------------------------------------- |
|
|
168
|
+
| 로컬 OAuth | `GITLAB_USE_OAUTH=true` | `GITLAB_OAUTH_REDIRECT_URI` | `http://127.0.0.1:8888/callback` 또는 로컬 callback |
|
|
169
|
+
| 원격 MCP OAuth | `GITLAB_MCP_OAUTH=true` | `GITLAB_OAUTH_CALLBACK_PROXY=true` | `{MCP_SERVER_URL}/callback` |
|
|
170
170
|
|
|
171
171
|
MCP 서버가 직접 로컬 브라우저 callback을 받을 때만 `GITLAB_OAUTH_REDIRECT_URI`를 사용하세요. 원격 MCP 클라이언트가 callback URL을 소유하는 경우에는 `GITLAB_OAUTH_CALLBACK_PROXY=true`를 사용하세요.
|
|
172
172
|
|
|
@@ -178,15 +178,15 @@ MCP 서버가 직접 로컬 브라우저 callback을 받을 때만 `GITLAB_OAUTH
|
|
|
178
178
|
2. `api` 또는 `read_api` scope가 있는 사전 등록 GitLab OAuth 애플리케이션
|
|
179
179
|
— `Admin area` → `Applications`에서 Redirect URI를 `{MCP_SERVER_URL}/callback`으로 설정하세요.
|
|
180
180
|
|
|
181
|
-
| 환경 변수
|
|
182
|
-
|
|
|
183
|
-
| `GITLAB_MCP_OAUTH`
|
|
184
|
-
| `GITLAB_API_URL`
|
|
185
|
-
| `GITLAB_OAUTH_APP_ID`
|
|
186
|
-
| `MCP_SERVER_URL`
|
|
187
|
-
| `STREAMABLE_HTTP`
|
|
188
|
-
| `GITLAB_OAUTH_CALLBACK_PROXY` | 선택 | MCP 서버의 고정 `/callback` URL을 사용하려면 `true`
|
|
189
|
-
| `GITLAB_OAUTH_SCOPES`
|
|
181
|
+
| 환경 변수 | 필수 | 설명 |
|
|
182
|
+
| ----------------------------- | ---- | ---------------------------------------------------------- |
|
|
183
|
+
| `GITLAB_MCP_OAUTH` | 예 | 활성화하려면 `true` |
|
|
184
|
+
| `GITLAB_API_URL` | 예 | GitLab API base URL |
|
|
185
|
+
| `GITLAB_OAUTH_APP_ID` | 예 | GitLab OAuth Application ID |
|
|
186
|
+
| `MCP_SERVER_URL` | 예 | 이 MCP 서버의 공개 HTTPS URL |
|
|
187
|
+
| `STREAMABLE_HTTP` | 예 | 반드시 `true` |
|
|
188
|
+
| `GITLAB_OAUTH_CALLBACK_PROXY` | 선택 | MCP 서버의 고정 `/callback` URL을 사용하려면 `true` |
|
|
189
|
+
| `GITLAB_OAUTH_SCOPES` | 선택 | 쉼표로 구분된 scope 목록(기본값: `api,read_api,read_user`) |
|
|
190
190
|
| `GITLAB_OAUTH_ALLOWED_GROUPS` | 선택 | 쉼표로 구분된 GitLab 그룹 전체 경로 — 해당 그룹 및 하위 그룹 멤버만 토큰을 발급받을 수 있음 (기존 `GITLAB_ALLOWED_GROUPS` 대체) |
|
|
191
191
|
|
|
192
192
|
> **`Unregistered redirect_uri` 문제 해결**
|
|
@@ -233,10 +233,10 @@ MCP 클라이언트 설정:
|
|
|
233
233
|
|
|
234
234
|
**헤더 우선순위**: `Private-Token` > `JOB-TOKEN` > `Authorization: Bearer`
|
|
235
235
|
|
|
236
|
-
| 환경 변수
|
|
237
|
-
|
|
|
238
|
-
| `REMOTE_AUTHORIZATION`
|
|
239
|
-
| `STREAMABLE_HTTP`
|
|
236
|
+
| 환경 변수 | 필수 | 설명 |
|
|
237
|
+
| ------------------------ | ---- | ----------------------------------- |
|
|
238
|
+
| `REMOTE_AUTHORIZATION` | 예 | 활성화하려면 `true` |
|
|
239
|
+
| `STREAMABLE_HTTP` | 예 | 반드시 `true` |
|
|
240
240
|
| `ENABLE_DYNAMIC_API_URL` | 선택 | 요청별 `X-GitLab-API-URL` 헤더 허용 |
|
|
241
241
|
|
|
242
242
|
**예시 요청 헤더:**
|
|
@@ -257,7 +257,7 @@ Authorization: Bearer glpat-xxxxxxxxxxxxxxxxxxxx
|
|
|
257
257
|
|
|
258
258
|
전체 환경 변수 목록은 전용 문서를 참고하세요.
|
|
259
259
|
|
|
260
|
-
- [환경 변수 레퍼런스](./docs/environment-variables.md)
|
|
260
|
+
- [환경 변수 레퍼런스](./docs/configuration/environment-variables.md)
|
|
261
261
|
|
|
262
262
|
대부분의 사용자는 아래 시작 조합 중 하나만 필요합니다.
|
|
263
263
|
|
|
@@ -283,7 +283,7 @@ Authorization: Bearer glpat-xxxxxxxxxxxxxxxxxxxx
|
|
|
283
283
|
- 전송 및 세션 변수
|
|
284
284
|
- 프록시 및 TLS 변수
|
|
285
285
|
|
|
286
|
-
콜백 프록시 모드 상세는 [GitLab MCP OAuth Callback Proxy](./docs/oauth-callback-proxy.md)를 참고하세요.
|
|
286
|
+
콜백 프록시 모드 상세는 [GitLab MCP OAuth Callback Proxy](./docs/auth/oauth-callback-proxy.md)를 참고하세요.
|
|
287
287
|
|
|
288
288
|
### 원격 인증 설정(멀티 유저 지원)
|
|
289
289
|
|
|
@@ -411,15 +411,15 @@ node build/index.js
|
|
|
411
411
|
|
|
412
412
|
**환경 변수:**
|
|
413
413
|
|
|
414
|
-
| 변수
|
|
415
|
-
|
|
|
416
|
-
| `GITLAB_MCP_OAUTH`
|
|
417
|
-
| `GITLAB_OAUTH_APP_ID`
|
|
418
|
-
| `MCP_SERVER_URL`
|
|
419
|
-
| `GITLAB_API_URL`
|
|
420
|
-
| `STREAMABLE_HTTP`
|
|
421
|
-
| `GITLAB_OAUTH_SCOPES`
|
|
422
|
-
| `MCP_DANGEROUSLY_ALLOW_INSECURE_ISSUER_URL` | 아니오 | 로컬 HTTP 개발에서만 `true`
|
|
414
|
+
| 변수 | 필수 | 설명 |
|
|
415
|
+
| ------------------------------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
416
|
+
| `GITLAB_MCP_OAUTH` | 예 | 활성화하려면 `true` |
|
|
417
|
+
| `GITLAB_OAUTH_APP_ID` | 예 | 사전 등록 GitLab OAuth 애플리케이션의 client ID |
|
|
418
|
+
| `MCP_SERVER_URL` | 예 | MCP 서버의 공개 HTTPS URL |
|
|
419
|
+
| `GITLAB_API_URL` | 예 | GitLab 인스턴스 API URL(예: `https://gitlab.com/api/v4`) |
|
|
420
|
+
| `STREAMABLE_HTTP` | 예 | 반드시 `true`(SSE 미지원) |
|
|
421
|
+
| `GITLAB_OAUTH_SCOPES` | 아니오 | 요청할 GitLab scope 목록(쉼표 구분). 기본값은 `api` 또는 `GITLAB_READ_ONLY_MODE=true`일 때 `read_api`입니다. 사전 등록 애플리케이션에 해당 scope가 설정되어 있어야 합니다. |
|
|
422
|
+
| `MCP_DANGEROUSLY_ALLOW_INSECURE_ISSUER_URL` | 아니오 | 로컬 HTTP 개발에서만 `true` |
|
|
423
423
|
|
|
424
424
|
**중요 사항:**
|
|
425
425
|
|
package/README.md
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
[English](./README.md) | [한국어](./README.ko.md) | [简体中文](./README.zh-CN.md)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
📖 **[Documentation →](https://zereight.github.io/gitlab-mcp/)** Setup guides, environment variables, and the full tool reference live on the hosted docs site.
|
|
6
|
+
|
|
7
|
+
> **New Feature**: Dynamic GitLab API URL support with connection pooling! See [Dynamic API URL Documentation](docs/configuration/dynamic-api-url.md) for details.
|
|
6
8
|
|
|
7
9
|
[](https://www.star-history.com/#zereight/gitlab-mcp&Date)
|
|
8
10
|
|
|
@@ -24,16 +26,16 @@ Quick start: choose either Personal Access Token or OAuth2 setup below and use `
|
|
|
24
26
|
|
|
25
27
|
### Client Setup Guides
|
|
26
28
|
|
|
27
|
-
- [Claude Code Setup Guide](./docs/claude-code
|
|
28
|
-
- [VS Code Setup Guide](./docs/vscode
|
|
29
|
-
- [GitHub Copilot Setup Guide](./docs/copilot
|
|
30
|
-
- [Codex Setup Guide](./docs/codex
|
|
31
|
-
- [Cursor Setup Guide](./docs/cursor
|
|
32
|
-
- [JSON-Based MCP Clients Setup Guide](./docs/json-
|
|
33
|
-
- [OAuth2 Authentication Setup Guide](./docs/oauth-setup.md)
|
|
34
|
-
- [Environment Variables Reference](./docs/environment-variables.md)
|
|
35
|
-
- [Stateless Mode — Multi-Pod HPA](./docs/stateless-mode.md)
|
|
36
|
-
- [Custom Agents and Multiple PAT Setup](./docs/custom-agent-multiple-pat.md)
|
|
29
|
+
- [Claude Code Setup Guide](./docs/clients/claude-code.md)
|
|
30
|
+
- [VS Code Setup Guide](./docs/clients/vscode.md)
|
|
31
|
+
- [GitHub Copilot Setup Guide](./docs/clients/copilot.md)
|
|
32
|
+
- [Codex Setup Guide](./docs/clients/codex.md)
|
|
33
|
+
- [Cursor Setup Guide](./docs/clients/cursor.md)
|
|
34
|
+
- [JSON-Based MCP Clients Setup Guide](./docs/clients/json-clients.md) - for Factory AI Droid, OpenClaw, and OpenCode style clients
|
|
35
|
+
- [OAuth2 Authentication Setup Guide](./docs/auth/oauth-setup.md)
|
|
36
|
+
- [Environment Variables Reference](./docs/configuration/environment-variables.md)
|
|
37
|
+
- [Stateless Mode — Multi-Pod HPA](./docs/configuration/stateless-mode.md)
|
|
38
|
+
- [Custom Agents and Multiple PAT Setup](./docs/auth/custom-agent-multiple-pat.md)
|
|
37
39
|
|
|
38
40
|
## Usage
|
|
39
41
|
|
|
@@ -55,13 +57,13 @@ The server supports four authentication methods:
|
|
|
55
57
|
|
|
56
58
|
#### Quick setup paths
|
|
57
59
|
|
|
58
|
-
- **Claude Code**: see [Claude Code Setup Guide](./docs/claude-code
|
|
59
|
-
- **VS Code**: see [VS Code Setup Guide](./docs/vscode
|
|
60
|
-
- **GitHub Copilot**: see [GitHub Copilot Setup Guide](./docs/copilot
|
|
61
|
-
- **Codex**: see [Codex Setup Guide](./docs/codex
|
|
62
|
-
- **Cursor**: see [Cursor Setup Guide](./docs/cursor
|
|
63
|
-
- **Factory AI Droid / OpenClaw / OpenCode style clients**: see [JSON-Based MCP Clients Setup Guide](./docs/json-
|
|
64
|
-
- **OAuth browser flow details**: see [OAuth2 Authentication Setup Guide](./docs/oauth-setup.md)
|
|
60
|
+
- **Claude Code**: see [Claude Code Setup Guide](./docs/clients/claude-code.md)
|
|
61
|
+
- **VS Code**: see [VS Code Setup Guide](./docs/clients/vscode.md)
|
|
62
|
+
- **GitHub Copilot**: see [GitHub Copilot Setup Guide](./docs/clients/copilot.md)
|
|
63
|
+
- **Codex**: see [Codex Setup Guide](./docs/clients/codex.md)
|
|
64
|
+
- **Cursor**: see [Cursor Setup Guide](./docs/clients/cursor.md)
|
|
65
|
+
- **Factory AI Droid / OpenClaw / OpenCode style clients**: see [JSON-Based MCP Clients Setup Guide](./docs/clients/json-clients.md)
|
|
66
|
+
- **OAuth browser flow details**: see [OAuth2 Authentication Setup Guide](./docs/auth/oauth-setup.md)
|
|
65
67
|
|
|
66
68
|
For the simplest local setup, start with a Personal Access Token. For browser-based local auth, use OAuth2. For remote or multi-user deployments, continue to the MCP OAuth and Remote Authorization sections later in this README.
|
|
67
69
|
|
|
@@ -265,13 +267,17 @@ the token to GitLab on behalf of the caller.
|
|
|
265
267
|
| `REMOTE_AUTHORIZATION` | ✅ | Set to `true` to enable |
|
|
266
268
|
| `STREAMABLE_HTTP` | ✅ | Must be `true` |
|
|
267
269
|
| `ENABLE_DYNAMIC_API_URL` | optional | Allow per-request GitLab URL via `X-GitLab-API-URL` header |
|
|
268
|
-
| `MCP_TRUST_PROXY` | optional | Trust `Forwarded` / `X-Forwarded-*` headers
|
|
270
|
+
| `MCP_TRUST_PROXY` | optional | Trust `Forwarded` / `X-Forwarded-*` headers behind a reverse proxy (download URLs, Express `req.ip`, OAuth rate limits) |
|
|
269
271
|
|
|
270
272
|
When `MCP_SERVER_URL` is not set, remote download URLs fall back to the local
|
|
271
273
|
server address. Set `MCP_TRUST_PROXY=true` only if the server is reachable through a
|
|
272
274
|
trusted reverse proxy and direct client access to the MCP server is blocked.
|
|
273
|
-
This
|
|
274
|
-
`
|
|
275
|
+
This enables Express `trust proxy` for Streamable HTTP and SSE, derives public
|
|
276
|
+
download URLs from `Forwarded` / `X-Forwarded-Proto` / `X-Forwarded-Host` /
|
|
277
|
+
`X-Forwarded-Prefix`, and keeps OAuth endpoint rate limiting working when
|
|
278
|
+
proxies send `X-Forwarded-For` with a client port (for example `1.2.3.4:5678`).
|
|
279
|
+
Existing OAuth+proxy deployments must set this explicitly after the flag was
|
|
280
|
+
introduced.
|
|
275
281
|
|
|
276
282
|
**Example request headers**:
|
|
277
283
|
|
|
@@ -291,14 +297,14 @@ Authorization: Bearer glpat-xxxxxxxxxxxxxxxxxxxx
|
|
|
291
297
|
|
|
292
298
|
Use the dedicated reference for the full environment variable list:
|
|
293
299
|
|
|
294
|
-
- [Environment Variables Reference](./docs/environment-variables.md)
|
|
300
|
+
- [Environment Variables Reference](./docs/configuration/environment-variables.md)
|
|
295
301
|
|
|
296
302
|
Most users only need one of these starting sets:
|
|
297
303
|
|
|
298
304
|
- **Local PAT**: `GITLAB_PERSONAL_ACCESS_TOKEN`, `GITLAB_API_URL`
|
|
299
305
|
- **Local OAuth**: `GITLAB_USE_OAUTH=true`, `GITLAB_OAUTH_CLIENT_ID`, `GITLAB_OAUTH_REDIRECT_URI`, `GITLAB_API_URL`
|
|
300
306
|
- **Remote multi-user HTTP**: `STREAMABLE_HTTP=true`, `REMOTE_AUTHORIZATION=true`, `HOST`, `PORT`
|
|
301
|
-
- **Multi-pod HPA (stateless)**: above + `OAUTH_STATELESS_MODE=true`, `OAUTH_STATELESS_SECRET` (same across all pods). See [Stateless Mode](./docs/stateless-mode.md).
|
|
307
|
+
- **Multi-pod HPA (stateless)**: above + `OAUTH_STATELESS_MODE=true`, `OAUTH_STATELESS_SECRET` (same across all pods). See [Stateless Mode](./docs/configuration/stateless-mode.md).
|
|
302
308
|
|
|
303
309
|
Commonly referenced variables:
|
|
304
310
|
|
|
@@ -321,7 +327,7 @@ The reference document also covers:
|
|
|
321
327
|
- transport and session variables
|
|
322
328
|
- proxy and TLS variables
|
|
323
329
|
|
|
324
|
-
For callback proxy mode details, see [GitLab MCP OAuth Callback Proxy](./docs/oauth-callback-proxy.md).
|
|
330
|
+
For callback proxy mode details, see [GitLab MCP OAuth Callback Proxy](./docs/auth/oauth-callback-proxy.md).
|
|
325
331
|
|
|
326
332
|
### Remote Authorization Setup (Multi-User Support)
|
|
327
333
|
|
package/README.zh-CN.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[English](./README.md) | [한국어](./README.ko.md) | [简体中文](./README.zh-CN.md)
|
|
4
4
|
|
|
5
|
-
> **新功能**:支持带连接池的动态 GitLab API URL。详情请参阅 [Dynamic API URL 文档](docs/dynamic-api-url.md)。
|
|
5
|
+
> **新功能**:支持带连接池的动态 GitLab API URL。详情请参阅 [Dynamic API URL 文档](docs/configuration/dynamic-api-url.md)。
|
|
6
6
|
|
|
7
7
|
[](https://www.star-history.com/#zereight/gitlab-mcp&Date)
|
|
8
8
|
|
|
@@ -24,14 +24,14 @@
|
|
|
24
24
|
|
|
25
25
|
### 客户端设置指南
|
|
26
26
|
|
|
27
|
-
- [Claude Code 设置指南](./docs/claude-code
|
|
28
|
-
- [VS Code 设置指南](./docs/vscode
|
|
29
|
-
- [GitHub Copilot 设置指南](./docs/copilot
|
|
30
|
-
- [Codex 设置指南](./docs/codex
|
|
31
|
-
- [Cursor 设置指南](./docs/cursor
|
|
32
|
-
- [基于 JSON 的 MCP 客户端设置指南](./docs/json-
|
|
33
|
-
- [OAuth2 认证设置指南](./docs/oauth-setup.md)
|
|
34
|
-
- [环境变量参考](./docs/environment-variables.md)
|
|
27
|
+
- [Claude Code 设置指南](./docs/clients/claude-code.md)
|
|
28
|
+
- [VS Code 设置指南](./docs/clients/vscode.md)
|
|
29
|
+
- [GitHub Copilot 设置指南](./docs/clients/copilot.md)
|
|
30
|
+
- [Codex 设置指南](./docs/clients/codex.md)
|
|
31
|
+
- [Cursor 设置指南](./docs/clients/cursor.md)
|
|
32
|
+
- [基于 JSON 的 MCP 客户端设置指南](./docs/clients/json-clients.md) - 适用于 Factory AI Droid、OpenClaw 和 OpenCode 风格客户端
|
|
33
|
+
- [OAuth2 认证设置指南](./docs/auth/oauth-setup.md)
|
|
34
|
+
- [环境变量参考](./docs/configuration/environment-variables.md)
|
|
35
35
|
|
|
36
36
|
## 使用方法
|
|
37
37
|
|
|
@@ -53,13 +53,13 @@
|
|
|
53
53
|
|
|
54
54
|
#### 快速设置路径
|
|
55
55
|
|
|
56
|
-
- **Claude Code**:[Claude Code 设置指南](./docs/claude-code
|
|
57
|
-
- **VS Code**:[VS Code 设置指南](./docs/vscode
|
|
58
|
-
- **GitHub Copilot**:[GitHub Copilot 设置指南](./docs/copilot
|
|
59
|
-
- **Codex**:[Codex 设置指南](./docs/codex
|
|
60
|
-
- **Cursor**:[Cursor 设置指南](./docs/cursor
|
|
61
|
-
- **Factory AI Droid / OpenClaw / OpenCode 风格客户端**:[基于 JSON 的 MCP 客户端设置指南](./docs/json-
|
|
62
|
-
- **OAuth 浏览器流程详情**:[OAuth2 认证设置指南](./docs/oauth-setup.md)
|
|
56
|
+
- **Claude Code**:[Claude Code 设置指南](./docs/clients/claude-code.md)
|
|
57
|
+
- **VS Code**:[VS Code 设置指南](./docs/clients/vscode.md)
|
|
58
|
+
- **GitHub Copilot**:[GitHub Copilot 设置指南](./docs/clients/copilot.md)
|
|
59
|
+
- **Codex**:[Codex 设置指南](./docs/clients/codex.md)
|
|
60
|
+
- **Cursor**:[Cursor 设置指南](./docs/clients/cursor.md)
|
|
61
|
+
- **Factory AI Droid / OpenClaw / OpenCode 风格客户端**:[基于 JSON 的 MCP 客户端设置指南](./docs/clients/json-clients.md)
|
|
62
|
+
- **OAuth 浏览器流程详情**:[OAuth2 认证设置指南](./docs/auth/oauth-setup.md)
|
|
63
63
|
|
|
64
64
|
最简单的本地设置可以从 Personal Access Token 开始。基于浏览器的本地认证使用 OAuth2。远程或多用户部署请继续查看下面的 MCP OAuth 和远程授权部分。
|
|
65
65
|
|
|
@@ -163,10 +163,10 @@ OpenCode、MCPJam、Claude.ai 等远程 MCP 客户端可能会在授权时发送
|
|
|
163
163
|
|
|
164
164
|
远程 MCP OAuth 不同。在 `GITLAB_MCP_OAUTH=true` 模式下,MCP 客户端会在 `/authorize` 请求中提供自己的 callback URL。`GITLAB_OAUTH_REDIRECT_URI` 不会替换这个客户端提供的 URL。
|
|
165
165
|
|
|
166
|
-
| 模式
|
|
167
|
-
|
|
|
168
|
-
| 本地 OAuth
|
|
169
|
-
| 远程 MCP OAuth | `GITLAB_MCP_OAUTH=true` | `GITLAB_OAUTH_CALLBACK_PROXY=true` | `{MCP_SERVER_URL}/callback`
|
|
166
|
+
| 模式 | 启用方式 | Callback 变量 | GitLab Redirect URI |
|
|
167
|
+
| -------------- | ----------------------- | ---------------------------------- | ---------------------------------------------------- |
|
|
168
|
+
| 本地 OAuth | `GITLAB_USE_OAUTH=true` | `GITLAB_OAUTH_REDIRECT_URI` | `http://127.0.0.1:8888/callback` 或你的本地 callback |
|
|
169
|
+
| 远程 MCP OAuth | `GITLAB_MCP_OAUTH=true` | `GITLAB_OAUTH_CALLBACK_PROXY=true` | `{MCP_SERVER_URL}/callback` |
|
|
170
170
|
|
|
171
171
|
只有当 MCP 服务器自己接收本地浏览器 callback 时,才使用 `GITLAB_OAUTH_REDIRECT_URI`。当远程 MCP 客户端拥有 callback URL 时,请使用 `GITLAB_OAUTH_CALLBACK_PROXY=true`。
|
|
172
172
|
|
|
@@ -178,15 +178,15 @@ OpenCode、MCPJam、Claude.ai 等远程 MCP 客户端可能会在授权时发送
|
|
|
178
178
|
2. 预先注册的 GitLab OAuth 应用,包含 `api` 或 `read_api` scopes
|
|
179
179
|
— 前往 `Admin area` → `Applications`,将 Redirect URI 设置为 `{MCP_SERVER_URL}/callback`
|
|
180
180
|
|
|
181
|
-
| 环境变量
|
|
182
|
-
|
|
|
183
|
-
| `GITLAB_MCP_OAUTH`
|
|
184
|
-
| `GITLAB_API_URL`
|
|
185
|
-
| `GITLAB_OAUTH_APP_ID`
|
|
186
|
-
| `MCP_SERVER_URL`
|
|
187
|
-
| `STREAMABLE_HTTP`
|
|
181
|
+
| 环境变量 | 必需 | 说明 |
|
|
182
|
+
| ----------------------------- | ---- | ----------------------------------------------------- |
|
|
183
|
+
| `GITLAB_MCP_OAUTH` | 是 | 设置为 `true` 以启用 |
|
|
184
|
+
| `GITLAB_API_URL` | 是 | GitLab API base URL |
|
|
185
|
+
| `GITLAB_OAUTH_APP_ID` | 是 | GitLab OAuth Application ID |
|
|
186
|
+
| `MCP_SERVER_URL` | 是 | 此 MCP 服务器的公开 HTTPS URL |
|
|
187
|
+
| `STREAMABLE_HTTP` | 是 | 必须为 `true` |
|
|
188
188
|
| `GITLAB_OAUTH_CALLBACK_PROXY` | 可选 | 设置为 `true` 时使用 MCP 服务器固定的 `/callback` URL |
|
|
189
|
-
| `GITLAB_OAUTH_SCOPES`
|
|
189
|
+
| `GITLAB_OAUTH_SCOPES` | 可选 | 逗号分隔的 scope(默认:`api,read_api,read_user`) |
|
|
190
190
|
| `GITLAB_OAUTH_ALLOWED_GROUPS` | 可选 | 逗号分隔的 GitLab 群组完整路径 — 仅该群组及其子群组的成员可获取令牌(替代已废弃的 `GITLAB_ALLOWED_GROUPS`)|
|
|
191
191
|
|
|
192
192
|
> **排查 `Unregistered redirect_uri`**
|
|
@@ -233,10 +233,10 @@ MCP 客户端配置:
|
|
|
233
233
|
|
|
234
234
|
**请求头优先级**:`Private-Token` > `JOB-TOKEN` > `Authorization: Bearer`
|
|
235
235
|
|
|
236
|
-
| 环境变量
|
|
237
|
-
|
|
|
238
|
-
| `REMOTE_AUTHORIZATION`
|
|
239
|
-
| `STREAMABLE_HTTP`
|
|
236
|
+
| 环境变量 | 必需 | 说明 |
|
|
237
|
+
| ------------------------ | ---- | ------------------------------------------------------- |
|
|
238
|
+
| `REMOTE_AUTHORIZATION` | 是 | 设置为 `true` 以启用 |
|
|
239
|
+
| `STREAMABLE_HTTP` | 是 | 必须为 `true` |
|
|
240
240
|
| `ENABLE_DYNAMIC_API_URL` | 可选 | 允许按请求通过 `X-GitLab-API-URL` 请求头指定 GitLab URL |
|
|
241
241
|
|
|
242
242
|
**示例请求头:**
|
|
@@ -257,7 +257,7 @@ Authorization: Bearer glpat-xxxxxxxxxxxxxxxxxxxx
|
|
|
257
257
|
|
|
258
258
|
完整环境变量列表请查看专门的参考文档:
|
|
259
259
|
|
|
260
|
-
- [环境变量参考](./docs/environment-variables.md)
|
|
260
|
+
- [环境变量参考](./docs/configuration/environment-variables.md)
|
|
261
261
|
|
|
262
262
|
大多数用户只需要以下起始组合之一:
|
|
263
263
|
|
|
@@ -283,7 +283,7 @@ Authorization: Bearer glpat-xxxxxxxxxxxxxxxxxxxx
|
|
|
283
283
|
- 传输和会话变量
|
|
284
284
|
- 代理和 TLS 变量
|
|
285
285
|
|
|
286
|
-
回调代理模式详情请参阅 [GitLab MCP OAuth Callback Proxy](./docs/oauth-callback-proxy.md)。
|
|
286
|
+
回调代理模式详情请参阅 [GitLab MCP OAuth Callback Proxy](./docs/auth/oauth-callback-proxy.md)。
|
|
287
287
|
|
|
288
288
|
### 远程授权设置(多用户支持)
|
|
289
289
|
|
|
@@ -411,15 +411,15 @@ node build/index.js
|
|
|
411
411
|
|
|
412
412
|
**环境变量:**
|
|
413
413
|
|
|
414
|
-
| 变量
|
|
415
|
-
|
|
|
416
|
-
| `GITLAB_MCP_OAUTH`
|
|
417
|
-
| `GITLAB_OAUTH_APP_ID`
|
|
418
|
-
| `MCP_SERVER_URL`
|
|
419
|
-
| `GITLAB_API_URL`
|
|
420
|
-
| `STREAMABLE_HTTP`
|
|
421
|
-
| `GITLAB_OAUTH_SCOPES`
|
|
422
|
-
| `MCP_DANGEROUSLY_ALLOW_INSECURE_ISSUER_URL` | 否
|
|
414
|
+
| 变量 | 必需 | 说明 |
|
|
415
|
+
| ------------------------------------------- | ---- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
|
416
|
+
| `GITLAB_MCP_OAUTH` | 是 | 设置为 `true` 以启用 |
|
|
417
|
+
| `GITLAB_OAUTH_APP_ID` | 是 | 预先注册的 GitLab OAuth 应用 client ID |
|
|
418
|
+
| `MCP_SERVER_URL` | 是 | MCP 服务器的公开 HTTPS URL |
|
|
419
|
+
| `GITLAB_API_URL` | 是 | GitLab 实例 API URL(例如 `https://gitlab.com/api/v4`) |
|
|
420
|
+
| `STREAMABLE_HTTP` | 是 | 必须为 `true`(不支持 SSE) |
|
|
421
|
+
| `GITLAB_OAUTH_SCOPES` | 否 | 要请求的 GitLab scopes,以逗号分隔。默认值为 `api`,当 `GITLAB_READ_ONLY_MODE=true` 时为 `read_api`。预注册应用必须配置至少这些 scopes。 |
|
|
422
|
+
| `MCP_DANGEROUSLY_ALLOW_INSECURE_ISSUER_URL` | 否 | 仅用于本地 HTTP 开发 |
|
|
423
423
|
|
|
424
424
|
**重要说明:**
|
|
425
425
|
|
package/build/index.js
CHANGED
|
@@ -183,6 +183,8 @@ import { z } from "zod";
|
|
|
183
183
|
import { initializeOAuthClient } from "./oauth.js";
|
|
184
184
|
import { createGitLabOAuthProvider } from "./oauth-proxy.js";
|
|
185
185
|
import { mcpAuthRouter } from "@modelcontextprotocol/sdk/server/auth/router.js";
|
|
186
|
+
import { ipKeyGenerator } from "express-rate-limit";
|
|
187
|
+
import { normalizeProxyClientIpForRateLimit } from "./utils/proxy-client-ip.js";
|
|
186
188
|
import { normalizeGitLabApiUrl } from "./utils/url.js";
|
|
187
189
|
import { estimateMergeCommitCount, filterDiffsByPatterns, summarizeWebhookEvents } from "./utils/helpers.js";
|
|
188
190
|
import { graphqlQueryContainsWriteOperation } from "./utils/graphql-query.js";
|
|
@@ -718,7 +720,7 @@ async function ensureValidOAuthToken() {
|
|
|
718
720
|
logger.info("OAuth token refreshed successfully");
|
|
719
721
|
}
|
|
720
722
|
catch (error) {
|
|
721
|
-
logger.error("Failed to refresh OAuth token
|
|
723
|
+
logger.error({ err: error }, "Failed to refresh OAuth token");
|
|
722
724
|
throw error;
|
|
723
725
|
}
|
|
724
726
|
}
|
|
@@ -1074,7 +1076,7 @@ async function handleGitLabError(response) {
|
|
|
1074
1076
|
const errorBody = await response.text();
|
|
1075
1077
|
// Check specifically for Rate Limit error
|
|
1076
1078
|
if (response.status === 403 && errorBody.includes("User API Key Rate limit exceeded")) {
|
|
1077
|
-
logger.error("GitLab API Rate Limit Exceeded
|
|
1079
|
+
logger.error({ err: errorBody }, "GitLab API Rate Limit Exceeded");
|
|
1078
1080
|
logger.error("User API Key Rate limit exceeded. Please try again later.");
|
|
1079
1081
|
throw new Error(`GitLab API Rate Limit Exceeded: ${errorBody}`);
|
|
1080
1082
|
}
|
|
@@ -5498,7 +5500,7 @@ async function getUser(username) {
|
|
|
5498
5500
|
return null;
|
|
5499
5501
|
}
|
|
5500
5502
|
catch (error) {
|
|
5501
|
-
logger.error(`Error fetching user by username '${username}'
|
|
5503
|
+
logger.error({ err: error }, `Error fetching user by username '${username}'`);
|
|
5502
5504
|
return null;
|
|
5503
5505
|
}
|
|
5504
5506
|
}
|
|
@@ -5517,7 +5519,7 @@ async function getUsers(usernames) {
|
|
|
5517
5519
|
users[username] = user;
|
|
5518
5520
|
}
|
|
5519
5521
|
catch (error) {
|
|
5520
|
-
logger.error(`Error processing username '${username}'
|
|
5522
|
+
logger.error({ err: error }, `Error processing username '${username}'`);
|
|
5521
5523
|
users[username] = null;
|
|
5522
5524
|
}
|
|
5523
5525
|
}
|
|
@@ -6498,7 +6500,7 @@ async function handleToolCall(params) {
|
|
|
6498
6500
|
};
|
|
6499
6501
|
}
|
|
6500
6502
|
catch (forkError) {
|
|
6501
|
-
logger.error("Error forking repository
|
|
6503
|
+
logger.error({ err: forkError }, "Error forking repository");
|
|
6502
6504
|
let forkErrorMessage = "Failed to fork repository";
|
|
6503
6505
|
if (forkError instanceof Error) {
|
|
6504
6506
|
forkErrorMessage = `${forkErrorMessage}: ${forkError.message}`;
|
|
@@ -8702,7 +8704,7 @@ function registerDownloadProxy(app, maxRequestsPerMinute = Number.parseInt(proce
|
|
|
8702
8704
|
}
|
|
8703
8705
|
}
|
|
8704
8706
|
catch (error) {
|
|
8705
|
-
logger.error("Download proxy error
|
|
8707
|
+
logger.error({ err: error }, "Download proxy error");
|
|
8706
8708
|
if (!res.headersSent) {
|
|
8707
8709
|
res.status(502).json({ error: "Failed to proxy download from GitLab" });
|
|
8708
8710
|
}
|
|
@@ -8714,6 +8716,9 @@ function registerDownloadProxy(app, maxRequestsPerMinute = Number.parseInt(proce
|
|
|
8714
8716
|
*/
|
|
8715
8717
|
async function startSSEServer() {
|
|
8716
8718
|
const app = express();
|
|
8719
|
+
if (MCP_TRUST_PROXY) {
|
|
8720
|
+
app.set("trust proxy", 1);
|
|
8721
|
+
}
|
|
8717
8722
|
const transports = {};
|
|
8718
8723
|
let shuttingDown = false;
|
|
8719
8724
|
app.get("/sse", async (_, res) => {
|
|
@@ -8758,7 +8763,7 @@ async function startSSEServer() {
|
|
|
8758
8763
|
await transport.close();
|
|
8759
8764
|
}
|
|
8760
8765
|
catch (error) {
|
|
8761
|
-
logger.error("Error closing SSE transport
|
|
8766
|
+
logger.error({ err: error }, "Error closing SSE transport");
|
|
8762
8767
|
}
|
|
8763
8768
|
}));
|
|
8764
8769
|
clientPool.closeAll();
|
|
@@ -9176,12 +9181,22 @@ async function startStreamableHTTPServer() {
|
|
|
9176
9181
|
}
|
|
9177
9182
|
// Mounts /.well-known/oauth-authorization-server (shadowed above when basePath set),
|
|
9178
9183
|
// /.well-known/oauth-protected-resource, /authorize, /token, /register, /revoke
|
|
9184
|
+
// Some proxies include the port in X-Forwarded-For (e.g. "1.2.3.4:5678" or
|
|
9185
|
+
// "[2001:db8::1]:5678"), which makes express-rate-limit throw
|
|
9186
|
+
// ERR_ERL_INVALID_IP_ADDRESS. Strip the port first, then delegate to
|
|
9187
|
+
// ipKeyGenerator for correct IPv6 subnet handling.
|
|
9188
|
+
const rateLimitKeyGenerator = (req) => ipKeyGenerator(normalizeProxyClientIpForRateLimit(req.ip ?? ""));
|
|
9189
|
+
const rateLimitOptions = { keyGenerator: rateLimitKeyGenerator };
|
|
9179
9190
|
app.use(mcpAuthRouter({
|
|
9180
9191
|
provider: oauthProvider,
|
|
9181
9192
|
issuerUrl,
|
|
9182
9193
|
baseUrl: issuerUrl,
|
|
9183
9194
|
scopesSupported,
|
|
9184
9195
|
resourceName: "GitLab MCP Server",
|
|
9196
|
+
authorizationOptions: { rateLimit: rateLimitOptions },
|
|
9197
|
+
tokenOptions: { rateLimit: rateLimitOptions },
|
|
9198
|
+
revocationOptions: { rateLimit: rateLimitOptions },
|
|
9199
|
+
clientRegistrationOptions: { rateLimit: rateLimitOptions },
|
|
9185
9200
|
}));
|
|
9186
9201
|
// Expose provider so the /mcp route middleware can reference it
|
|
9187
9202
|
app._mcpOAuthProvider = oauthProvider;
|
|
@@ -9432,7 +9447,7 @@ async function startStreamableHTTPServer() {
|
|
|
9432
9447
|
}
|
|
9433
9448
|
}
|
|
9434
9449
|
catch (error) {
|
|
9435
|
-
logger.error("Streamable HTTP error
|
|
9450
|
+
logger.error({ err: error }, "Streamable HTTP error");
|
|
9436
9451
|
res.status(500).json({
|
|
9437
9452
|
error: "Internal server error",
|
|
9438
9453
|
message: error instanceof Error ? error.message : "Unknown error",
|
|
@@ -9516,7 +9531,7 @@ async function startStreamableHTTPServer() {
|
|
|
9516
9531
|
res.status(204).send();
|
|
9517
9532
|
}
|
|
9518
9533
|
catch (error) {
|
|
9519
|
-
logger.error(`Error closing session ${sessionId}
|
|
9534
|
+
logger.error({ err: error }, `Error closing session ${sessionId}`);
|
|
9520
9535
|
res.status(500).json({ error: "Failed to close session" });
|
|
9521
9536
|
}
|
|
9522
9537
|
}
|
|
@@ -9551,7 +9566,7 @@ async function startStreamableHTTPServer() {
|
|
|
9551
9566
|
}
|
|
9552
9567
|
}
|
|
9553
9568
|
catch (error) {
|
|
9554
|
-
logger.error(`Error closing session ${sessionId}
|
|
9569
|
+
logger.error({ err: error }, `Error closing session ${sessionId}`);
|
|
9555
9570
|
}
|
|
9556
9571
|
});
|
|
9557
9572
|
await Promise.allSettled(closePromises);
|
|
@@ -9572,7 +9587,7 @@ async function startStreamableHTTPServer() {
|
|
|
9572
9587
|
* Handle transport-specific initialization logic
|
|
9573
9588
|
*/
|
|
9574
9589
|
async function initializeServerByTransportMode(mode) {
|
|
9575
|
-
logger.info("Initializing server with transport mode
|
|
9590
|
+
logger.info({ mode }, "Initializing server with transport mode");
|
|
9576
9591
|
switch (mode) {
|
|
9577
9592
|
case TransportMode.STDIO:
|
|
9578
9593
|
logger.warn("Starting GitLab MCP Server with stdio transport");
|
|
@@ -9612,7 +9627,7 @@ async function runServer() {
|
|
|
9612
9627
|
logger.info("OAuth authentication successful");
|
|
9613
9628
|
}
|
|
9614
9629
|
catch (error) {
|
|
9615
|
-
logger.error("OAuth authentication failed
|
|
9630
|
+
logger.error({ err: error }, "OAuth authentication failed");
|
|
9616
9631
|
process.exit(1);
|
|
9617
9632
|
}
|
|
9618
9633
|
}
|
|
@@ -9633,12 +9648,12 @@ async function runServer() {
|
|
|
9633
9648
|
}
|
|
9634
9649
|
}
|
|
9635
9650
|
catch (error) {
|
|
9636
|
-
logger.error("Error initializing server
|
|
9651
|
+
logger.error({ err: error }, "Error initializing server");
|
|
9637
9652
|
process.exit(1);
|
|
9638
9653
|
}
|
|
9639
9654
|
}
|
|
9640
9655
|
// 下記の2行を追記
|
|
9641
9656
|
runServer().catch(error => {
|
|
9642
|
-
logger.error
|
|
9657
|
+
logger.fatal({ err: error }, "Fatal error in main()");
|
|
9643
9658
|
process.exit(1);
|
|
9644
9659
|
});
|
package/build/oauth.js
CHANGED
|
@@ -193,7 +193,7 @@ export class GitLabOAuth {
|
|
|
193
193
|
logger.info(`Token saved to ${this.tokenStoragePath}`);
|
|
194
194
|
}
|
|
195
195
|
catch (error) {
|
|
196
|
-
logger.error("Failed to save token
|
|
196
|
+
logger.error({ err: error }, "Failed to save token");
|
|
197
197
|
throw error;
|
|
198
198
|
}
|
|
199
199
|
}
|
|
@@ -209,7 +209,7 @@ export class GitLabOAuth {
|
|
|
209
209
|
return JSON.parse(data);
|
|
210
210
|
}
|
|
211
211
|
catch (error) {
|
|
212
|
-
logger.error("Failed to load token
|
|
212
|
+
logger.error({ err: error }, "Failed to load token");
|
|
213
213
|
return null;
|
|
214
214
|
}
|
|
215
215
|
}
|
|
@@ -240,7 +240,7 @@ export class GitLabOAuth {
|
|
|
240
240
|
return await requestAuthFromExistingServer(callbackPort, requestId);
|
|
241
241
|
}
|
|
242
242
|
catch (error) {
|
|
243
|
-
logger.error("Failed to connect to existing OAuth server
|
|
243
|
+
logger.error({ err: error }, "Failed to connect to existing OAuth server");
|
|
244
244
|
throw new Error(`Port ${callbackPort} is in use but cannot connect to existing OAuth server. Please close other instances or use a different port.`);
|
|
245
245
|
}
|
|
246
246
|
}
|
|
@@ -285,7 +285,7 @@ export class GitLabOAuth {
|
|
|
285
285
|
logger.info("Opening browser for new authentication request...");
|
|
286
286
|
logger.info(`If browser doesn't open, visit: ${authUrl}`);
|
|
287
287
|
open(authUrl).catch(err => {
|
|
288
|
-
logger.error("Failed to open browser
|
|
288
|
+
logger.error({ err }, "Failed to open browser");
|
|
289
289
|
logger.info(`Please manually open: ${authUrl}`);
|
|
290
290
|
});
|
|
291
291
|
// Wait for the auth to complete
|
|
@@ -430,7 +430,7 @@ export class GitLabOAuth {
|
|
|
430
430
|
}
|
|
431
431
|
}
|
|
432
432
|
catch (error) {
|
|
433
|
-
logger.error("Error handling request
|
|
433
|
+
logger.error({ err: error }, "Error handling request");
|
|
434
434
|
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
435
435
|
res.end("Internal Server Error");
|
|
436
436
|
}
|
|
@@ -441,12 +441,12 @@ export class GitLabOAuth {
|
|
|
441
441
|
logger.info("Opening browser for authentication...");
|
|
442
442
|
logger.info(`If browser doesn't open, visit: ${authUrl}`);
|
|
443
443
|
open(authUrl).catch(err => {
|
|
444
|
-
logger.error("Failed to open browser
|
|
444
|
+
logger.error({ err }, "Failed to open browser");
|
|
445
445
|
logger.info(`Please manually open: ${authUrl}`);
|
|
446
446
|
});
|
|
447
447
|
});
|
|
448
448
|
server.on("error", error => {
|
|
449
|
-
logger.error("OAuth server error
|
|
449
|
+
logger.error({ err: error }, "OAuth server error");
|
|
450
450
|
const pending = pendingAuthRequests.get(initialRequestId);
|
|
451
451
|
if (pending) {
|
|
452
452
|
clearTimeout(pending.timeout);
|
|
@@ -474,7 +474,7 @@ export class GitLabOAuth {
|
|
|
474
474
|
this.saveToken(tokenData);
|
|
475
475
|
}
|
|
476
476
|
catch (error) {
|
|
477
|
-
logger.error("Token refresh failed. Starting new OAuth flow..."
|
|
477
|
+
logger.error({ err: error }, "Token refresh failed. Starting new OAuth flow...");
|
|
478
478
|
tokenData = await this.startOAuthFlow();
|
|
479
479
|
}
|
|
480
480
|
}
|
|
@@ -496,7 +496,7 @@ export class GitLabOAuth {
|
|
|
496
496
|
}
|
|
497
497
|
}
|
|
498
498
|
catch (error) {
|
|
499
|
-
logger.error("Failed to clear token
|
|
499
|
+
logger.error({ err: error }, "Failed to clear token");
|
|
500
500
|
}
|
|
501
501
|
}
|
|
502
502
|
/**
|
package/build/schemas.js
CHANGED
|
@@ -467,7 +467,10 @@ export const RetryPipelineSchema = z.object({
|
|
|
467
467
|
pipeline_id: z.coerce.string().describe("The ID of the pipeline to retry"),
|
|
468
468
|
});
|
|
469
469
|
// Schema for canceling a pipeline
|
|
470
|
-
export const CancelPipelineSchema =
|
|
470
|
+
export const CancelPipelineSchema = z.object({
|
|
471
|
+
project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
|
|
472
|
+
pipeline_id: z.coerce.string().describe("The ID of the pipeline to cancel"),
|
|
473
|
+
});
|
|
471
474
|
// Schema for the input parameters for pipeline job operations
|
|
472
475
|
export const GetPipelineJobOutputSchema = z.object({
|
|
473
476
|
project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
|
|
@@ -1668,7 +1671,7 @@ export const MergeMergeRequestSchema = ProjectParamsSchema.extend({
|
|
|
1668
1671
|
.boolean()
|
|
1669
1672
|
.optional()
|
|
1670
1673
|
.default(false)
|
|
1671
|
-
.describe("If true, the merge request merges when the pipeline succeeds.in GitLab 17.11. Use"),
|
|
1674
|
+
.describe("If true, the merge request merges when the pipeline succeeds. Deprecated in GitLab 17.11. Use `auto_merge` instead."),
|
|
1672
1675
|
should_remove_source_branch: z.coerce
|
|
1673
1676
|
.boolean()
|
|
1674
1677
|
.optional()
|
|
@@ -2091,7 +2094,7 @@ export const ListLabelsSchema = z
|
|
|
2091
2094
|
with_counts: z
|
|
2092
2095
|
.coerce.boolean()
|
|
2093
2096
|
.optional()
|
|
2094
|
-
.describe("Whether
|
|
2097
|
+
.describe("Whether to include issue and merge request counts"),
|
|
2095
2098
|
include_ancestor_groups: z.coerce.boolean().optional().describe("Include ancestor groups"),
|
|
2096
2099
|
search: z.string().optional().describe("Keyword to filter labels by"),
|
|
2097
2100
|
})
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Generate per-group tool reference pages under docs/tools/ from the
|
|
4
|
+
* authoritative tool registry (tools/registry.ts).
|
|
5
|
+
*
|
|
6
|
+
* Run with: npx tsx scripts/generate-tool-docs.ts
|
|
7
|
+
* Or via: make tools-docs
|
|
8
|
+
*/
|
|
9
|
+
import { writeFileSync, mkdirSync, readdirSync, unlinkSync, statSync } from "node:fs";
|
|
10
|
+
import { join, dirname } from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { allTools, readOnlyTools, TOOLSET_DEFINITIONS } from "../tools/registry.js";
|
|
13
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const REPO_ROOT = join(__dirname, "..");
|
|
15
|
+
const OUT_DIR = join(REPO_ROOT, "docs", "tools");
|
|
16
|
+
// Legacy single-group env flags that pre-date GITLAB_TOOLSETS.
|
|
17
|
+
// Only three groups have these for backward compatibility; everything else
|
|
18
|
+
// opt-in is configured via GITLAB_TOOLSETS / GITLAB_TOOLS / discover_tools.
|
|
19
|
+
const LEGACY_TOGGLE_ENV = {
|
|
20
|
+
pipelines: "USE_PIPELINE",
|
|
21
|
+
milestones: "USE_MILESTONE",
|
|
22
|
+
wiki: "USE_GITLAB_WIKI",
|
|
23
|
+
};
|
|
24
|
+
function isDefaultToolset(id) {
|
|
25
|
+
return TOOLSET_DEFINITIONS.find(d => d.id === id)?.isDefault ?? false;
|
|
26
|
+
}
|
|
27
|
+
function computeToggleNote(id) {
|
|
28
|
+
if (isDefaultToolset(id))
|
|
29
|
+
return undefined;
|
|
30
|
+
// Synthetic group for tools not in any TOOLSET_DEFINITIONS entry.
|
|
31
|
+
// discover_tools is always exposed; execute_graphql is opt-in via GITLAB_TOOLS.
|
|
32
|
+
if (id === "meta") {
|
|
33
|
+
return "Mixed availability. `discover_tools` is always exposed (the server re-adds it after every toolset filter). `execute_graphql` is not part of any toolset — enable it explicitly with `GITLAB_TOOLS=execute_graphql`.";
|
|
34
|
+
}
|
|
35
|
+
const legacy = LEGACY_TOGGLE_ENV[id];
|
|
36
|
+
if (legacy) {
|
|
37
|
+
return `Opt-in. Enable via \`GITLAB_TOOLSETS=${id}\` (or \`GITLAB_TOOLSETS=all\`), or use the legacy \`${legacy}=true\` flag for backward compatibility.`;
|
|
38
|
+
}
|
|
39
|
+
return `Opt-in. Enable via \`GITLAB_TOOLSETS=${id}\` (or \`GITLAB_TOOLSETS=all\`), list individual tools in \`GITLAB_TOOLS=\`, or activate at runtime with the \`discover_tools\` MCP tool.`;
|
|
40
|
+
}
|
|
41
|
+
const GROUP_META = {
|
|
42
|
+
merge_requests: {
|
|
43
|
+
title: "Merge Requests",
|
|
44
|
+
blurb: "MR lifecycle — create, update, merge, approve, plus diff/conflict inspection and the full discussion/note/draft API.",
|
|
45
|
+
},
|
|
46
|
+
issues: {
|
|
47
|
+
title: "Issues",
|
|
48
|
+
blurb: "Issue CRUD, links, discussions and notes, todos, and emoji reactions.",
|
|
49
|
+
},
|
|
50
|
+
repositories: {
|
|
51
|
+
title: "Projects & Files",
|
|
52
|
+
blurb: "Project search/creation/fork plus the Files API for reading and writing repository content without shelling out to git.",
|
|
53
|
+
},
|
|
54
|
+
branches: {
|
|
55
|
+
title: "Branches & Commits",
|
|
56
|
+
blurb: "Branch management, commit listing/inspection, file blame, and CI commit-status manipulation.",
|
|
57
|
+
},
|
|
58
|
+
projects: {
|
|
59
|
+
title: "Projects & Namespaces",
|
|
60
|
+
blurb: "Project/namespace listing, member queries, group iterations, and server health.",
|
|
61
|
+
},
|
|
62
|
+
labels: {
|
|
63
|
+
title: "Labels",
|
|
64
|
+
blurb: "Project label CRUD.",
|
|
65
|
+
},
|
|
66
|
+
ci: {
|
|
67
|
+
title: "CI Lint",
|
|
68
|
+
blurb: "Validate `.gitlab-ci.yml` snippets and project pipeline configs.",
|
|
69
|
+
},
|
|
70
|
+
groups: {
|
|
71
|
+
title: "Groups",
|
|
72
|
+
blurb: "Create new groups and subgroups.",
|
|
73
|
+
},
|
|
74
|
+
pipelines: {
|
|
75
|
+
title: "Pipelines, Jobs & Deployments",
|
|
76
|
+
blurb: "Pipeline + job control (trigger, retry, cancel, play manual jobs, fetch logs/artifacts), and the deployments/environments view.",
|
|
77
|
+
},
|
|
78
|
+
milestones: {
|
|
79
|
+
title: "Milestones",
|
|
80
|
+
blurb: "Project milestone CRUD plus associated issues/MRs and burndown events.",
|
|
81
|
+
},
|
|
82
|
+
wiki: {
|
|
83
|
+
title: "Wiki",
|
|
84
|
+
blurb: "Project and group wiki page CRUD. Attachment uploads where supported.",
|
|
85
|
+
},
|
|
86
|
+
releases: {
|
|
87
|
+
title: "Releases",
|
|
88
|
+
blurb: "Release lifecycle, release evidence, and asset download.",
|
|
89
|
+
},
|
|
90
|
+
tags: {
|
|
91
|
+
title: "Tags",
|
|
92
|
+
blurb: "Tag listing, creation, deletion, and signature inspection.",
|
|
93
|
+
},
|
|
94
|
+
users: {
|
|
95
|
+
title: "Users & Events",
|
|
96
|
+
blurb: "User lookup, the authenticated user (`whoami`), event streams, and markdown attachment upload/download.",
|
|
97
|
+
},
|
|
98
|
+
workitems: {
|
|
99
|
+
title: "Work Items",
|
|
100
|
+
blurb: "Modern unified API for issues, tasks, incidents, and other typed work items — including notes, emoji reactions, and incident timeline events.",
|
|
101
|
+
},
|
|
102
|
+
webhooks: {
|
|
103
|
+
title: "Webhooks",
|
|
104
|
+
blurb: "List webhooks configured on projects or groups, and inspect recent webhook events.",
|
|
105
|
+
},
|
|
106
|
+
search: {
|
|
107
|
+
title: "Search",
|
|
108
|
+
blurb: "Code search across all visible projects, a specific project, or a specific group.",
|
|
109
|
+
},
|
|
110
|
+
variables: {
|
|
111
|
+
title: "Variables",
|
|
112
|
+
blurb: "Project and group CI/CD variable CRUD.",
|
|
113
|
+
},
|
|
114
|
+
dependency_proxy: {
|
|
115
|
+
title: "Dependency Proxy",
|
|
116
|
+
blurb: "Inspect and manage the GitLab dependency proxy cache settings, blob storage, and purge operations.",
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
const GROUP_ORDER = [
|
|
120
|
+
"projects",
|
|
121
|
+
"repositories",
|
|
122
|
+
"branches",
|
|
123
|
+
"groups",
|
|
124
|
+
"merge_requests",
|
|
125
|
+
"issues",
|
|
126
|
+
"labels",
|
|
127
|
+
"workitems",
|
|
128
|
+
"ci",
|
|
129
|
+
"pipelines",
|
|
130
|
+
"milestones",
|
|
131
|
+
"wiki",
|
|
132
|
+
"releases",
|
|
133
|
+
"tags",
|
|
134
|
+
"users",
|
|
135
|
+
"variables",
|
|
136
|
+
"webhooks",
|
|
137
|
+
"search",
|
|
138
|
+
"dependency_proxy",
|
|
139
|
+
];
|
|
140
|
+
// Authoritative classification — uses the `readOnlyTools` set from
|
|
141
|
+
// tools/registry.ts. That set is what the server itself consults to decide
|
|
142
|
+
// which tools survive `GITLAB_READ_ONLY_MODE=true`, so the badges here
|
|
143
|
+
// match real runtime behavior exactly (no prefix heuristics).
|
|
144
|
+
function classify(name) {
|
|
145
|
+
return readOnlyTools.has(name) ? "read" : "write";
|
|
146
|
+
}
|
|
147
|
+
function rwBadge(name) {
|
|
148
|
+
return classify(name) === "read" ? "📖 Read-only" : "✏️ Writes";
|
|
149
|
+
}
|
|
150
|
+
function describeType(prop) {
|
|
151
|
+
if (prop.enum) {
|
|
152
|
+
return `enum (\`${prop.enum.map(String).join("` \\| `")}\`)`;
|
|
153
|
+
}
|
|
154
|
+
if (prop.anyOf || prop.oneOf) {
|
|
155
|
+
const variants = (prop.anyOf || prop.oneOf || []).map(describeType).filter(Boolean);
|
|
156
|
+
return variants.join(" \\| ") || "any";
|
|
157
|
+
}
|
|
158
|
+
if (Array.isArray(prop.type)) {
|
|
159
|
+
return prop.type.join(" \\| ");
|
|
160
|
+
}
|
|
161
|
+
if (prop.type === "array") {
|
|
162
|
+
return `array<${prop.items ? describeType(prop.items) : "any"}>`;
|
|
163
|
+
}
|
|
164
|
+
if (prop.format && prop.type) {
|
|
165
|
+
return `${prop.type} (${prop.format})`;
|
|
166
|
+
}
|
|
167
|
+
return prop.type || "any";
|
|
168
|
+
}
|
|
169
|
+
function escapePipe(text) {
|
|
170
|
+
return text.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
171
|
+
}
|
|
172
|
+
function paramTable(schema) {
|
|
173
|
+
if (!schema || !schema.properties || Object.keys(schema.properties).length === 0) {
|
|
174
|
+
return "_No parameters._";
|
|
175
|
+
}
|
|
176
|
+
const required = new Set(schema.required || []);
|
|
177
|
+
const rows = ["| Parameter | Type | Required | Description |", "|---|---|:-:|---|"];
|
|
178
|
+
for (const [name, prop] of Object.entries(schema.properties)) {
|
|
179
|
+
const type = describeType(prop);
|
|
180
|
+
const req = required.has(name) ? "✓" : "";
|
|
181
|
+
const desc = escapePipe(prop.description || "");
|
|
182
|
+
rows.push(`| \`${name}\` | ${type} | ${req} | ${desc} |`);
|
|
183
|
+
}
|
|
184
|
+
return rows.join("\n");
|
|
185
|
+
}
|
|
186
|
+
function toolSection(name, description, schema) {
|
|
187
|
+
return [
|
|
188
|
+
`### \`${name}\``,
|
|
189
|
+
"",
|
|
190
|
+
`*${rwBadge(name)}*`,
|
|
191
|
+
"",
|
|
192
|
+
description,
|
|
193
|
+
"",
|
|
194
|
+
"**Parameters**",
|
|
195
|
+
"",
|
|
196
|
+
paramTable(schema),
|
|
197
|
+
"",
|
|
198
|
+
].join("\n");
|
|
199
|
+
}
|
|
200
|
+
function buildGroupPage(id, toolNames) {
|
|
201
|
+
const meta = GROUP_META[id];
|
|
202
|
+
const lines = [`# ${meta.title}`, "", meta.blurb, ""];
|
|
203
|
+
const toggle = computeToggleNote(id);
|
|
204
|
+
if (toggle) {
|
|
205
|
+
lines.push(`!!! note "Feature toggle"`);
|
|
206
|
+
lines.push(` ${toggle}`);
|
|
207
|
+
lines.push("");
|
|
208
|
+
}
|
|
209
|
+
// Quick index
|
|
210
|
+
lines.push("## Tools in this group");
|
|
211
|
+
lines.push("");
|
|
212
|
+
for (const name of toolNames) {
|
|
213
|
+
lines.push(`- [\`${name}\`](#${name.replace(/_/g, "_")}) — ${rwBadge(name)}`);
|
|
214
|
+
}
|
|
215
|
+
lines.push("");
|
|
216
|
+
lines.push("---");
|
|
217
|
+
lines.push("");
|
|
218
|
+
for (const name of toolNames) {
|
|
219
|
+
const tool = allTools.find(t => t.name === name);
|
|
220
|
+
if (!tool) {
|
|
221
|
+
throw new Error(`Tool '${name}' referenced in toolset '${id}' but missing from allTools registry`);
|
|
222
|
+
}
|
|
223
|
+
lines.push(toolSection(name, tool.description, tool.inputSchema));
|
|
224
|
+
}
|
|
225
|
+
return lines.join("\n");
|
|
226
|
+
}
|
|
227
|
+
function buildToggleSection(groupedToolsList) {
|
|
228
|
+
const grouped = groupedToolsList.filter(([id]) => GROUP_META[id]);
|
|
229
|
+
const defaults = grouped.filter(([id]) => isDefaultToolset(id));
|
|
230
|
+
const optins = grouped.filter(([id]) => !isDefaultToolset(id));
|
|
231
|
+
const formatList = (items) => items
|
|
232
|
+
.map(([id]) => {
|
|
233
|
+
const slug = id.replace(/_/g, "-");
|
|
234
|
+
const legacy = LEGACY_TOGGLE_ENV[id];
|
|
235
|
+
const suffix = legacy ? ` (also \`${legacy}=true\`)` : "";
|
|
236
|
+
return `[${GROUP_META[id].title}](${slug}.md)${suffix}`;
|
|
237
|
+
})
|
|
238
|
+
.join(", ");
|
|
239
|
+
return [
|
|
240
|
+
"| Status | Groups |",
|
|
241
|
+
"|---|---|",
|
|
242
|
+
`| **Default** — always exposed | ${formatList(defaults)} |`,
|
|
243
|
+
`| **Opt-in** — must be enabled | ${formatList(optins)} |`,
|
|
244
|
+
"",
|
|
245
|
+
"**How to enable opt-in groups** (any one is sufficient):",
|
|
246
|
+
"",
|
|
247
|
+
"- `GITLAB_TOOLSETS=<group,…>` — comma-separated toolset IDs.",
|
|
248
|
+
"- `GITLAB_TOOLSETS=all` — enables every group.",
|
|
249
|
+
"- `GITLAB_TOOLS=<tool,…>` — enables individual tools regardless of group.",
|
|
250
|
+
"- `USE_PIPELINE=true` / `USE_MILESTONE=true` / `USE_GITLAB_WIKI=true` —" +
|
|
251
|
+
" legacy single-group flags (Pipelines, Milestones, Wiki only).",
|
|
252
|
+
"- Call the `discover_tools` MCP tool at runtime to activate categories" +
|
|
253
|
+
" for the current session.",
|
|
254
|
+
];
|
|
255
|
+
}
|
|
256
|
+
function buildIndexPage(groupedToolsList) {
|
|
257
|
+
const lines = [
|
|
258
|
+
"# Tools Reference",
|
|
259
|
+
"",
|
|
260
|
+
"Complete catalog of every tool the GitLab MCP server exposes.",
|
|
261
|
+
"",
|
|
262
|
+
"> **Setup first** — if you haven't connected your Personal Access Token or",
|
|
263
|
+
"> OAuth credentials yet, follow one of the [client setup guides](../clients/claude-code.md)",
|
|
264
|
+
"> or read [Getting Started](../getting-started/index.md). Tools listed below",
|
|
265
|
+
"> will be unavailable until the server is authenticated.",
|
|
266
|
+
"",
|
|
267
|
+
"## Feature toggles",
|
|
268
|
+
"",
|
|
269
|
+
"Toolsets are split into a **default** set (exposed automatically) and an",
|
|
270
|
+
"**opt-in** set (must be explicitly enabled). The lists below are derived",
|
|
271
|
+
"directly from `TOOLSET_DEFINITIONS` in",
|
|
272
|
+
"[`tools/registry.ts`](https://github.com/zereight/gitlab-mcp/blob/main/tools/registry.ts).",
|
|
273
|
+
"",
|
|
274
|
+
...buildToggleSection(groupedToolsList),
|
|
275
|
+
"",
|
|
276
|
+
"Read-only mode (`GITLAB_READ_ONLY_MODE=true`) hides every write tool",
|
|
277
|
+
"regardless of toggles. See [Environment Variables](../configuration/environment-variables.md)",
|
|
278
|
+
"and [CLI Arguments](../getting-started/cli-arguments.md) for the full list.",
|
|
279
|
+
"",
|
|
280
|
+
"## Legend",
|
|
281
|
+
"",
|
|
282
|
+
"| Marker | Meaning |",
|
|
283
|
+
"|---|---|",
|
|
284
|
+
"| 📖 | **Read-only** — fetches data, does not modify GitLab state. Safe to invoke freely. |",
|
|
285
|
+
"| ✏️ | **Writes** — creates, updates, or deletes data on GitLab. Confirm intent before running. |",
|
|
286
|
+
"",
|
|
287
|
+
"## Browse by group",
|
|
288
|
+
"",
|
|
289
|
+
"Each group has its own page with full parameter tables — click any tool name to jump to its details, or click the group title for the per-group view.",
|
|
290
|
+
"",
|
|
291
|
+
];
|
|
292
|
+
for (const [id, tools] of groupedToolsList) {
|
|
293
|
+
const meta = GROUP_META[id];
|
|
294
|
+
const slug = id.replace(/_/g, "-");
|
|
295
|
+
lines.push(`### [${meta.title}](${slug}.md)`);
|
|
296
|
+
lines.push("");
|
|
297
|
+
lines.push(`${meta.blurb} *(${tools.length} tools)*`);
|
|
298
|
+
lines.push("");
|
|
299
|
+
const toggle = computeToggleNote(id);
|
|
300
|
+
if (toggle) {
|
|
301
|
+
lines.push(`> ${toggle}`);
|
|
302
|
+
lines.push("");
|
|
303
|
+
}
|
|
304
|
+
lines.push("| Tool | What it does | R/W |");
|
|
305
|
+
lines.push("|---|---|:-:|");
|
|
306
|
+
for (const name of tools) {
|
|
307
|
+
const tool = allTools.find(t => t.name === name);
|
|
308
|
+
if (!tool)
|
|
309
|
+
continue;
|
|
310
|
+
const desc = escapePipe(tool.description);
|
|
311
|
+
const marker = classify(name) === "read" ? "📖" : "✏️";
|
|
312
|
+
lines.push(`| [\`${name}\`](${slug}.md#${name}) | ${desc} | ${marker} |`);
|
|
313
|
+
}
|
|
314
|
+
lines.push("");
|
|
315
|
+
}
|
|
316
|
+
lines.push("---");
|
|
317
|
+
lines.push("");
|
|
318
|
+
lines.push("## Argument schemas");
|
|
319
|
+
lines.push("");
|
|
320
|
+
lines.push("Each group page includes a parameter table per tool, generated from");
|
|
321
|
+
lines.push("the authoritative Zod schemas in");
|
|
322
|
+
lines.push("[`schemas.ts`](https://github.com/zereight/gitlab-mcp/blob/main/schemas.ts).");
|
|
323
|
+
lines.push("For runtime schema inspection from a connected MCP client, call the");
|
|
324
|
+
lines.push("`discover_tools` tool.");
|
|
325
|
+
return lines.join("\n");
|
|
326
|
+
}
|
|
327
|
+
// --- Main -----------------------------------------------------------------
|
|
328
|
+
function main() {
|
|
329
|
+
mkdirSync(OUT_DIR, { recursive: true });
|
|
330
|
+
// Clean previously generated per-group files (keep index.md until last)
|
|
331
|
+
for (const file of readdirSync(OUT_DIR)) {
|
|
332
|
+
const full = join(OUT_DIR, file);
|
|
333
|
+
if (statSync(full).isFile() && file.endsWith(".md")) {
|
|
334
|
+
unlinkSync(full);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
const grouped = [];
|
|
338
|
+
const allCategorizedNames = new Set();
|
|
339
|
+
for (const id of GROUP_ORDER) {
|
|
340
|
+
const def = TOOLSET_DEFINITIONS.find(d => d.id === id);
|
|
341
|
+
if (!def) {
|
|
342
|
+
throw new Error(`Toolset '${id}' listed in GROUP_ORDER but missing from TOOLSET_DEFINITIONS — keep them in sync`);
|
|
343
|
+
}
|
|
344
|
+
// Preserve insertion order from TOOLSET_DEFINITIONS
|
|
345
|
+
const tools = [...def.tools];
|
|
346
|
+
for (const t of tools)
|
|
347
|
+
allCategorizedNames.add(t);
|
|
348
|
+
grouped.push([id, tools]);
|
|
349
|
+
const page = buildGroupPage(id, tools);
|
|
350
|
+
const slug = id.replace(/_/g, "-");
|
|
351
|
+
writeFileSync(join(OUT_DIR, `${slug}.md`), page);
|
|
352
|
+
console.log(`generated docs/tools/${slug}.md — ${tools.length} tools`);
|
|
353
|
+
}
|
|
354
|
+
// Tools NOT in any toolset (e.g., execute_graphql, discover_tools)
|
|
355
|
+
const uncategorized = allTools.map(t => t.name).filter(n => !allCategorizedNames.has(n));
|
|
356
|
+
if (uncategorized.length > 0) {
|
|
357
|
+
const metaToggle = computeToggleNote("meta");
|
|
358
|
+
const lines = [
|
|
359
|
+
`# Meta & GraphQL`,
|
|
360
|
+
"",
|
|
361
|
+
"Tools the MCP exposes that aren't tied to a specific GitLab feature group — server diagnostics and the GraphQL escape hatch.",
|
|
362
|
+
"",
|
|
363
|
+
];
|
|
364
|
+
if (metaToggle) {
|
|
365
|
+
lines.push(`!!! note "Feature toggle"`);
|
|
366
|
+
lines.push(` ${metaToggle}`);
|
|
367
|
+
lines.push("");
|
|
368
|
+
}
|
|
369
|
+
lines.push("## Tools in this group");
|
|
370
|
+
lines.push("");
|
|
371
|
+
for (const name of uncategorized) {
|
|
372
|
+
lines.push(`- [\`${name}\`](#${name}) — ${rwBadge(name)}`);
|
|
373
|
+
}
|
|
374
|
+
lines.push("");
|
|
375
|
+
lines.push("---");
|
|
376
|
+
lines.push("");
|
|
377
|
+
for (const name of uncategorized) {
|
|
378
|
+
const tool = allTools.find(t => t.name === name);
|
|
379
|
+
if (!tool) {
|
|
380
|
+
throw new Error(`Uncategorized tool '${name}' missing from allTools registry`);
|
|
381
|
+
}
|
|
382
|
+
lines.push(toolSection(name, tool.description, tool.inputSchema));
|
|
383
|
+
}
|
|
384
|
+
writeFileSync(join(OUT_DIR, "meta.md"), lines.join("\n"));
|
|
385
|
+
console.log(`generated docs/tools/meta.md — ${uncategorized.length} tools`);
|
|
386
|
+
grouped.push(["meta", uncategorized]);
|
|
387
|
+
GROUP_META["meta"] = {
|
|
388
|
+
title: "Meta & GraphQL",
|
|
389
|
+
blurb: "Server diagnostics, tool discovery, and the GraphQL escape hatch.",
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
writeFileSync(join(OUT_DIR, "index.md"), buildIndexPage(grouped));
|
|
393
|
+
console.log(`generated docs/tools/index.md`);
|
|
394
|
+
// Emit nav fragment so you can paste it into mkdocs.yml if needed
|
|
395
|
+
console.log("\n--- mkdocs.yml nav fragment ---");
|
|
396
|
+
console.log(" - Tools:");
|
|
397
|
+
console.log(" - Overview: tools/index.md");
|
|
398
|
+
for (const [id] of grouped) {
|
|
399
|
+
const meta = GROUP_META[id];
|
|
400
|
+
const slug = id.replace(/_/g, "-");
|
|
401
|
+
console.log(` - ${meta.title}: tools/${slug}.md`);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
main();
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression tests for OAuth endpoint rate limiting behind trusted proxies.
|
|
3
|
+
*/
|
|
4
|
+
import { describe, test, after, before } from "node:test";
|
|
5
|
+
import assert from "node:assert";
|
|
6
|
+
import { launchServer, findAvailablePort, cleanupServers, TransportMode, HOST, } from "./utils/server-launcher.js";
|
|
7
|
+
import { MockGitLabServer, findMockServerPort } from "./utils/mock-gitlab-server.js";
|
|
8
|
+
const MOCK_GITLAB_PORT_BASE = 9210;
|
|
9
|
+
const MCP_SERVER_PORT_BASE = 3210;
|
|
10
|
+
const MOCK_CLIENT_ID = "mock-app-uid-from-dcr";
|
|
11
|
+
function addOAuthEndpoints(mockGitLab, baseUrl) {
|
|
12
|
+
mockGitLab.addRootHandler("post", "/oauth/register", (req, res) => {
|
|
13
|
+
res.status(201).json({
|
|
14
|
+
client_id: MOCK_CLIENT_ID,
|
|
15
|
+
client_name: req.body?.client_name ?? "test",
|
|
16
|
+
redirect_uris: req.body?.redirect_uris ?? [],
|
|
17
|
+
token_endpoint_auth_method: "none",
|
|
18
|
+
require_pkce: true,
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
mockGitLab.addRootHandler("get", "/.well-known/oauth-authorization-server", (_req, res) => {
|
|
22
|
+
res.json({
|
|
23
|
+
issuer: baseUrl,
|
|
24
|
+
authorization_endpoint: `${baseUrl}/oauth/authorize`,
|
|
25
|
+
token_endpoint: `${baseUrl}/oauth/token`,
|
|
26
|
+
registration_endpoint: `${baseUrl}/oauth/register`,
|
|
27
|
+
revocation_endpoint: `${baseUrl}/oauth/revoke`,
|
|
28
|
+
scopes_supported: ["api", "read_api", "read_user"],
|
|
29
|
+
response_types_supported: ["code"],
|
|
30
|
+
grant_types_supported: ["authorization_code", "refresh_token"],
|
|
31
|
+
code_challenge_methods_supported: ["S256"],
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
describe("When MCP OAuth runs behind a trusted proxy", () => {
|
|
36
|
+
describe("with X-Forwarded-For containing client ports", () => {
|
|
37
|
+
let mcpBaseUrl;
|
|
38
|
+
let mockGitLab;
|
|
39
|
+
let servers = [];
|
|
40
|
+
before(async () => {
|
|
41
|
+
const mockPort = await findMockServerPort(MOCK_GITLAB_PORT_BASE);
|
|
42
|
+
mockGitLab = new MockGitLabServer({ port: mockPort, validTokens: [] });
|
|
43
|
+
await mockGitLab.start();
|
|
44
|
+
const mockGitLabUrl = mockGitLab.getUrl();
|
|
45
|
+
addOAuthEndpoints(mockGitLab, mockGitLabUrl);
|
|
46
|
+
const mcpPort = await findAvailablePort(MCP_SERVER_PORT_BASE);
|
|
47
|
+
mcpBaseUrl = `http://${HOST}:${mcpPort}`;
|
|
48
|
+
const server = await launchServer({
|
|
49
|
+
mode: TransportMode.STREAMABLE_HTTP,
|
|
50
|
+
port: mcpPort,
|
|
51
|
+
timeout: 5000,
|
|
52
|
+
env: {
|
|
53
|
+
STREAMABLE_HTTP: "true",
|
|
54
|
+
GITLAB_MCP_OAUTH: "true",
|
|
55
|
+
MCP_TRUST_PROXY: "true",
|
|
56
|
+
GITLAB_OAUTH_APP_ID: "test-oauth-app-id",
|
|
57
|
+
GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
|
|
58
|
+
MCP_SERVER_URL: mcpBaseUrl,
|
|
59
|
+
MCP_DANGEROUSLY_ALLOW_INSECURE_ISSUER_URL: "true",
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
servers.push(server);
|
|
63
|
+
});
|
|
64
|
+
after(async () => {
|
|
65
|
+
cleanupServers(servers);
|
|
66
|
+
if (mockGitLab) {
|
|
67
|
+
await mockGitLab.stop();
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
test("should not return 500 from /register", async () => {
|
|
71
|
+
const res = await fetch(`${mcpBaseUrl}/register`, {
|
|
72
|
+
method: "POST",
|
|
73
|
+
headers: {
|
|
74
|
+
"Content-Type": "application/json",
|
|
75
|
+
"X-Forwarded-For": "160.79.106.36:38914",
|
|
76
|
+
},
|
|
77
|
+
body: JSON.stringify({
|
|
78
|
+
redirect_uris: ["https://client.example/callback"],
|
|
79
|
+
client_name: "proxy-rate-limit-test",
|
|
80
|
+
}),
|
|
81
|
+
});
|
|
82
|
+
assert.notEqual(res.status, 500, "rate-limit key generation should not crash /register");
|
|
83
|
+
assert.strictEqual(res.status, 201, "DCR should succeed behind forwarded IPv4:port");
|
|
84
|
+
});
|
|
85
|
+
test("should not return 500 from /authorize", async () => {
|
|
86
|
+
const params = new URLSearchParams({
|
|
87
|
+
response_type: "code",
|
|
88
|
+
client_id: MOCK_CLIENT_ID,
|
|
89
|
+
redirect_uri: "https://client.example/callback",
|
|
90
|
+
code_challenge: "challenge",
|
|
91
|
+
code_challenge_method: "S256",
|
|
92
|
+
state: "proxy-rate-limit-state",
|
|
93
|
+
scope: "api",
|
|
94
|
+
});
|
|
95
|
+
const res = await fetch(`${mcpBaseUrl}/authorize?${params}`, {
|
|
96
|
+
redirect: "manual",
|
|
97
|
+
headers: {
|
|
98
|
+
"X-Forwarded-For": "[2001:db8::1]:5678",
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
assert.notEqual(res.status, 500, "rate-limit key generation should not crash /authorize");
|
|
102
|
+
});
|
|
103
|
+
test("should not return 500 from /token", async () => {
|
|
104
|
+
const res = await fetch(`${mcpBaseUrl}/token`, {
|
|
105
|
+
method: "POST",
|
|
106
|
+
headers: {
|
|
107
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
108
|
+
"X-Forwarded-For": "160.79.106.36:38914",
|
|
109
|
+
},
|
|
110
|
+
body: new URLSearchParams({
|
|
111
|
+
grant_type: "authorization_code",
|
|
112
|
+
client_id: MOCK_CLIENT_ID,
|
|
113
|
+
code: "invalid-code",
|
|
114
|
+
redirect_uri: "https://client.example/callback",
|
|
115
|
+
}),
|
|
116
|
+
});
|
|
117
|
+
assert.notEqual(res.status, 500, "rate-limit key generation should not crash /token");
|
|
118
|
+
});
|
|
119
|
+
test("should not return 500 from /revoke", async () => {
|
|
120
|
+
const res = await fetch(`${mcpBaseUrl}/revoke`, {
|
|
121
|
+
method: "POST",
|
|
122
|
+
headers: {
|
|
123
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
124
|
+
"X-Forwarded-For": "[2001:db8::1]",
|
|
125
|
+
},
|
|
126
|
+
body: new URLSearchParams({
|
|
127
|
+
token: "invalid-token",
|
|
128
|
+
}),
|
|
129
|
+
});
|
|
130
|
+
assert.notEqual(res.status, 500, "rate-limit key generation should not crash /revoke");
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, test } from "node:test";
|
|
3
|
+
import { ipKeyGenerator } from "express-rate-limit";
|
|
4
|
+
import { normalizeProxyClientIpForRateLimit } from "../../utils/proxy-client-ip.js";
|
|
5
|
+
function rateLimitKeyFromForwardedIp(ip) {
|
|
6
|
+
return ipKeyGenerator(normalizeProxyClientIpForRateLimit(ip));
|
|
7
|
+
}
|
|
8
|
+
describe("When normalizeProxyClientIpForRateLimit runs", () => {
|
|
9
|
+
describe("with proxy-forwarded client addresses", () => {
|
|
10
|
+
test("should strip IPv4 ports before ipKeyGenerator accepts the address", () => {
|
|
11
|
+
const key = rateLimitKeyFromForwardedIp("160.79.106.36:38914");
|
|
12
|
+
assert.equal(key, "160.79.106.36");
|
|
13
|
+
});
|
|
14
|
+
test("should strip bracketed IPv6 ports before ipKeyGenerator accepts the address", () => {
|
|
15
|
+
const key = rateLimitKeyFromForwardedIp("[2001:db8::1]:5678");
|
|
16
|
+
assert.equal(key, "2001:db8::/56");
|
|
17
|
+
});
|
|
18
|
+
test("should strip brackets from bracketed IPv6 addresses without ports", () => {
|
|
19
|
+
const key = rateLimitKeyFromForwardedIp("[2001:db8::1]");
|
|
20
|
+
assert.equal(key, "2001:db8::/56");
|
|
21
|
+
});
|
|
22
|
+
test("should leave plain IPv4 and IPv6 addresses unchanged", () => {
|
|
23
|
+
assert.equal(rateLimitKeyFromForwardedIp("1.2.3.4"), "1.2.3.4");
|
|
24
|
+
assert.equal(rateLimitKeyFromForwardedIp("2001:db8::1"), "2001:db8::/56");
|
|
25
|
+
assert.equal(rateLimitKeyFromForwardedIp("::1"), "::/56");
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize Express req.ip values from reverse proxies before rate limiting.
|
|
3
|
+
*
|
|
4
|
+
* Some proxies append the client port to X-Forwarded-For (e.g. "1.2.3.4:5678"
|
|
5
|
+
* or "[2001:db8::1]:5678"). express-rate-limit rejects those unless stripped.
|
|
6
|
+
*/
|
|
7
|
+
export function normalizeProxyClientIpForRateLimit(ip) {
|
|
8
|
+
return ip
|
|
9
|
+
.replace(/^(\d+\.\d+\.\d+\.\d+):\d+$/, "$1")
|
|
10
|
+
.replace(/^\[([^\]]+)\](?::\d+)?$/, "$1");
|
|
11
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zereight/mcp-gitlab",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.24",
|
|
4
4
|
"mcpName": "io.github.zereight/gitlab-mcp",
|
|
5
5
|
"description": "GitLab MCP server for projects, merge requests, issues, pipelines, wiki, releases, and more",
|
|
6
6
|
"keywords": [
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"changelog": "auto-changelog -p",
|
|
52
52
|
"test": "npm run test:all",
|
|
53
53
|
"test:all": "npm run build && npm run test:mock && npm run test:live",
|
|
54
|
-
"test:mock": "node --import tsx/esm --test test/remote-auth-simple-test.ts && node --import tsx/esm --test test/mcp-oauth-tests.ts && node --import tsx/esm --test test/streamable-http-static-token-auth.test.ts && tsx test/oauth-tests.ts && tsx test/test-list-merge-requests.ts && tsx test/test-list-issues.ts && node --import tsx/esm --test test/test-merge-request-pipelines.ts && tsx test/test-list-project-members.ts && tsx test/test-download-attachment.ts && node --import tsx/esm --test test/test-upload-markdown.ts && node --import tsx/esm --test test/test-job-artifacts.ts && node --import tsx/esm --test test/test-remote-downloads.ts && node --import tsx/esm --test test/test-deployment-tools.ts && node --import tsx/esm --test test/test-merge-request-approval-state-tools.ts && node --import tsx/esm --test test/test-search-code.ts && node --import tsx/esm --test test/test-tags.ts && node --import tsx/esm --test test/test-protected-branches.ts && node --import tsx/esm --test test/test-toolset-filtering.ts && node --import tsx/esm --test test/test-ci-lint.ts && node --import tsx/esm --test test/test-todos.ts && node --import tsx/esm --test test/test-auth-retry.ts && node --import tsx/esm --test test/test-issue-description-patch.ts && node --import tsx/esm --test test/test-geteffectiveprojectid.ts && node --import tsx/esm --test test/test-get-file-blame.ts && node --import tsx/esm --test test/stateless/codec.test.ts test/stateless/client-id.test.ts test/stateless/callback-proxy.test.ts test/stateless/session-id.test.ts test/stateless/session-id-integration.test.ts test/stateless/config-ttl.test.ts && node --import tsx/esm --test test/utils/tool-args.test.ts && node --import tsx/esm --test test/utils/graphql-query.test.ts && node --import tsx/esm --test test/utils/wiki-title.test.ts && node --import tsx/esm --test test/utils/merge-request-position.test.ts && node --import tsx/esm --test test/nullish-tool-arguments-schema.test.ts && node --import tsx/esm --test test/test-ci-variables.ts && node --import tsx/esm --test test/test-dependency-proxy.ts",
|
|
54
|
+
"test:mock": "node --import tsx/esm --test test/remote-auth-simple-test.ts && node --import tsx/esm --test test/mcp-oauth-tests.ts && node --import tsx/esm --test test/test-oauth-proxy-rate-limit.ts && node --import tsx/esm --test test/streamable-http-static-token-auth.test.ts && tsx test/oauth-tests.ts && tsx test/test-list-merge-requests.ts && tsx test/test-list-issues.ts && node --import tsx/esm --test test/test-merge-request-pipelines.ts && tsx test/test-list-project-members.ts && tsx test/test-download-attachment.ts && node --import tsx/esm --test test/test-upload-markdown.ts && node --import tsx/esm --test test/test-job-artifacts.ts && node --import tsx/esm --test test/test-remote-downloads.ts && node --import tsx/esm --test test/test-deployment-tools.ts && node --import tsx/esm --test test/test-merge-request-approval-state-tools.ts && node --import tsx/esm --test test/test-search-code.ts && node --import tsx/esm --test test/test-tags.ts && node --import tsx/esm --test test/test-protected-branches.ts && node --import tsx/esm --test test/test-toolset-filtering.ts && node --import tsx/esm --test test/test-ci-lint.ts && node --import tsx/esm --test test/test-todos.ts && node --import tsx/esm --test test/test-auth-retry.ts && node --import tsx/esm --test test/test-issue-description-patch.ts && node --import tsx/esm --test test/test-geteffectiveprojectid.ts && node --import tsx/esm --test test/test-get-file-blame.ts && node --import tsx/esm --test test/stateless/codec.test.ts test/stateless/client-id.test.ts test/stateless/callback-proxy.test.ts test/stateless/session-id.test.ts test/stateless/session-id-integration.test.ts test/stateless/config-ttl.test.ts && node --import tsx/esm --test test/utils/tool-args.test.ts && node --import tsx/esm --test test/utils/proxy-client-ip.test.ts && node --import tsx/esm --test test/utils/graphql-query.test.ts && node --import tsx/esm --test test/utils/wiki-title.test.ts && node --import tsx/esm --test test/utils/merge-request-position.test.ts && node --import tsx/esm --test test/nullish-tool-arguments-schema.test.ts && node --import tsx/esm --test test/test-ci-variables.ts && node --import tsx/esm --test test/test-dependency-proxy.ts",
|
|
55
55
|
"test:stateless": "npm run build && node --import tsx/esm --test test/stateless/codec.test.ts test/stateless/client-id.test.ts test/stateless/callback-proxy.test.ts test/stateless/session-id.test.ts test/stateless/session-id-integration.test.ts test/stateless/config-ttl.test.ts",
|
|
56
56
|
"test:mcp-oauth": "npm run build && node --import tsx/esm --test test/mcp-oauth-tests.ts",
|
|
57
57
|
"test:live": "node test/validate-api.js",
|
|
@@ -72,6 +72,7 @@
|
|
|
72
72
|
"@types/node-fetch": "^2.6.12",
|
|
73
73
|
"diff": "^9.0.0",
|
|
74
74
|
"express": "^5.1.0",
|
|
75
|
+
"express-rate-limit": "^8.5.2",
|
|
75
76
|
"fetch-cookie": "^3.1.0",
|
|
76
77
|
"form-data": "^4.0.0",
|
|
77
78
|
"http-proxy-agent": "^7.0.2",
|