@zereight/mcp-gitlab 2.1.22 → 2.1.23

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 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
  [![Star History Chart](https://api.star-history.com/svg?repos=zereight/gitlab-mcp&type=Date)](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-setup.md)
28
- - [VS Code 설정 가이드](./docs/vscode-setup.md)
29
- - [GitHub Copilot 설정 가이드](./docs/copilot-setup.md)
30
- - [Codex 설정 가이드](./docs/codex-setup.md)
31
- - [Cursor 설정 가이드](./docs/cursor-setup.md)
32
- - [JSON 기반 MCP 클라이언트 설정 가이드](./docs/json-mcp-clients-setup.md) - Factory AI Droid, OpenClaw, OpenCode 스타일 클라이언트용
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-setup.md)
57
- - **VS Code**: [VS Code 설정 가이드](./docs/vscode-setup.md)
58
- - **GitHub Copilot**: [GitHub Copilot 설정 가이드](./docs/copilot-setup.md)
59
- - **Codex**: [Codex 설정 가이드](./docs/codex-setup.md)
60
- - **Cursor**: [Cursor 설정 가이드](./docs/cursor-setup.md)
61
- - **Factory AI Droid / OpenClaw / OpenCode 스타일 클라이언트**: [JSON 기반 MCP 클라이언트 설정 가이드](./docs/json-mcp-clients-setup.md)
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
- | 모드 | 활성화 변수 | 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` |
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` | 예 | 활성화하려면 `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`) |
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` | 예 | 활성화하려면 `true` |
239
- | `STREAMABLE_HTTP` | 예 | 반드시 `true` |
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` | 예 | 활성화하려면 `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` |
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
- > **New Feature**: Dynamic GitLab API URL support with connection pooling! See [Dynamic API URL Documentation](docs/dynamic-api-url.md) for details.
5
+ 📖 **[Read the full documentation ](https://zereight.github.io/gitlab-mcp/)**
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
  [![Star History Chart](https://api.star-history.com/svg?repos=zereight/gitlab-mcp&type=Date)](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-setup.md)
28
- - [VS Code Setup Guide](./docs/vscode-setup.md)
29
- - [GitHub Copilot Setup Guide](./docs/copilot-setup.md)
30
- - [Codex Setup Guide](./docs/codex-setup.md)
31
- - [Cursor Setup Guide](./docs/cursor-setup.md)
32
- - [JSON-Based MCP Clients Setup Guide](./docs/json-mcp-clients-setup.md) - for Factory AI Droid, OpenClaw, and OpenCode style clients
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-setup.md)
59
- - **VS Code**: see [VS Code Setup Guide](./docs/vscode-setup.md)
60
- - **GitHub Copilot**: see [GitHub Copilot Setup Guide](./docs/copilot-setup.md)
61
- - **Codex**: see [Codex Setup Guide](./docs/codex-setup.md)
62
- - **Cursor**: see [Cursor Setup Guide](./docs/cursor-setup.md)
63
- - **Factory AI Droid / OpenClaw / OpenCode style clients**: see [JSON-Based MCP Clients Setup Guide](./docs/json-mcp-clients-setup.md)
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 for public download URLs when deployed behind a trusted reverse proxy |
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 lets the server derive public download URLs from `Forwarded` /
274
- `X-Forwarded-Proto`, `X-Forwarded-Host`, and `X-Forwarded-Prefix`.
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
  [![Star History Chart](https://api.star-history.com/svg?repos=zereight/gitlab-mcp&type=Date)](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-setup.md)
28
- - [VS Code 设置指南](./docs/vscode-setup.md)
29
- - [GitHub Copilot 设置指南](./docs/copilot-setup.md)
30
- - [Codex 设置指南](./docs/codex-setup.md)
31
- - [Cursor 设置指南](./docs/cursor-setup.md)
32
- - [基于 JSON 的 MCP 客户端设置指南](./docs/json-mcp-clients-setup.md) - 适用于 Factory AI Droid、OpenClaw 和 OpenCode 风格客户端
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-setup.md)
57
- - **VS Code**:[VS Code 设置指南](./docs/vscode-setup.md)
58
- - **GitHub Copilot**:[GitHub Copilot 设置指南](./docs/copilot-setup.md)
59
- - **Codex**:[Codex 设置指南](./docs/codex-setup.md)
60
- - **Cursor**:[Cursor 设置指南](./docs/cursor-setup.md)
61
- - **Factory AI Droid / OpenClaw / OpenCode 风格客户端**:[基于 JSON 的 MCP 客户端设置指南](./docs/json-mcp-clients-setup.md)
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
- | 模式 | 启用方式 | 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` |
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` | 是 | 设置为 `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` |
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` | 可选 | 逗号分隔的 scope(默认:`api,read_api,read_user`) |
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` | 是 | 设置为 `true` 以启用 |
239
- | `STREAMABLE_HTTP` | 是 | 必须为 `true` |
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` | 是 | 设置为 `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 开发 |
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:", error);
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:", errorBody);
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}':`, error);
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}':`, error);
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:", forkError);
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:", 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:", error);
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:", 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}:`, error);
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}:`, error);
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:", 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:", error);
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:", error);
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("Fatal error in main():", 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:", error);
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:", error);
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:", error);
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:", err);
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:", error);
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:", err);
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:", 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...", error);
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:", error);
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 = RetryPipelineSchema;
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 or not to include issue and merge request counts"),
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.22",
3
+ "version": "2.1.23",
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",