@zereight/mcp-gitlab 2.1.26 → 2.1.28

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
@@ -20,7 +20,7 @@ PAT, OAuth, 읽기 전용 모드, 동적 API URL, 원격 인증을 지원하며
20
20
  - 클라이언트 친화적 설정: Claude Code, Codex, Antigravity, OpenCode, Copilot, Cline, Roo Code, Cursor, Kilo Code, Amp Code 예시 제공
21
21
  - 셀프 호스팅 대응: 커스텀 GitLab 인스턴스, 프록시 설정, 동적 API URL 라우팅 지원
22
22
 
23
- 빠른 시작: 아래에서 Personal Access Token 또는 OAuth2 설정 중 하나를 선택하고 MCP 클라이언트 설정에서 `@zereight/mcp-gitlab`을 사용하세요.
23
+ 빠른 시작: 아래에서 Personal Access Token 또는 OAuth2 설정 중 하나를 선택하고 `@zereight/mcp-gitlab`을 설치한 뒤 MCP 클라이언트 설정에서 `zereight-mcp-gitlab`을 사용하세요.
24
24
 
25
25
  ### 클라이언트 설정 가이드
26
26
 
@@ -63,6 +63,16 @@ PAT, OAuth, 읽기 전용 모드, 동적 API URL, 원격 인증을 지원하며
63
63
 
64
64
  가장 단순한 로컬 설정은 Personal Access Token으로 시작하세요. 브라우저 기반 로컬 인증은 OAuth2를 사용하세요. 원격 또는 멀티 유저 배포는 아래 MCP OAuth 및 원격 인증 섹션을 참고하세요.
65
65
 
66
+ 서버를 한 번 전역 설치하세요.
67
+
68
+ ```shell
69
+ npm install -g @zereight/mcp-gitlab
70
+ ```
71
+
72
+ 예시는 기존 `mcp-gitlab`보다 충돌 가능성이 낮은 `zereight-mcp-gitlab` 별칭을 사용합니다. MCP 클라이언트가 찾지 못하면 `which zereight-mcp-gitlab`의 절대 경로를 사용하세요.
73
+
74
+ 전역 설치를 쓰지 않으려면 `npx -y @zereight/mcp-gitlab@2.1.27`처럼 버전을 고정하세요.
75
+
66
76
  #### CLI 인자 사용하기(환경 변수 문제가 있는 클라이언트용)
67
77
 
68
78
  일부 MCP 클라이언트(예: GitHub Copilot CLI)는 환경 변수 처리에 문제가 있을 수 있습니다. 이 경우 CLI 인자를 사용하세요.
@@ -71,13 +81,8 @@ PAT, OAuth, 읽기 전용 모드, 동적 API URL, 원격 인증을 지원하며
71
81
  {
72
82
  "mcpServers": {
73
83
  "gitlab": {
74
- "command": "npx",
75
- "args": [
76
- "-y",
77
- "@zereight/mcp-gitlab",
78
- "--token=YOUR_GITLAB_TOKEN",
79
- "--api-url=https://gitlab.com/api/v4"
80
- ],
84
+ "command": "zereight-mcp-gitlab",
85
+ "args": ["--token=YOUR_GITLAB_TOKEN", "--api-url=https://gitlab.com/api/v4"],
81
86
  "tools": ["*"]
82
87
  }
83
88
  }
@@ -178,15 +183,15 @@ MCP 서버가 직접 로컬 브라우저 callback을 받을 때만 `GITLAB_OAUTH
178
183
  2. `api` 또는 `read_api` scope가 있는 사전 등록 GitLab OAuth 애플리케이션
179
184
  — `Admin area` → `Applications`에서 Redirect URI를 `{MCP_SERVER_URL}/callback`으로 설정하세요.
180
185
 
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`) |
186
+ | 환경 변수 | 필수 | 설명 |
187
+ | ----------------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------- |
188
+ | `GITLAB_MCP_OAUTH` | 예 | 활성화하려면 `true` |
189
+ | `GITLAB_API_URL` | 예 | GitLab API base URL |
190
+ | `GITLAB_OAUTH_APP_ID` | 예 | GitLab OAuth Application ID |
191
+ | `MCP_SERVER_URL` | 예 | 이 MCP 서버의 공개 HTTPS URL |
192
+ | `STREAMABLE_HTTP` | 예 | 반드시 `true` |
193
+ | `GITLAB_OAUTH_CALLBACK_PROXY` | 선택 | MCP 서버의 고정 `/callback` URL을 사용하려면 `true` |
194
+ | `GITLAB_OAUTH_SCOPES` | 선택 | 쉼표로 구분된 scope 목록(기본값: `api,read_api,read_user`) |
190
195
  | `GITLAB_OAUTH_ALLOWED_GROUPS` | 선택 | 쉼표로 구분된 GitLab 그룹 전체 경로 — 해당 그룹 및 하위 그룹 멤버만 토큰을 발급받을 수 있음 (기존 `GITLAB_ALLOWED_GROUPS` 대체) |
191
196
 
192
197
  > **`Unregistered redirect_uri` 문제 해결**
@@ -233,11 +238,12 @@ MCP 클라이언트 설정:
233
238
 
234
239
  **헤더 우선순위**: `Private-Token` > `JOB-TOKEN` > `Authorization: Bearer`
235
240
 
236
- | 환경 변수 | 필수 | 설명 |
237
- | ------------------------ | ---- | ----------------------------------- |
238
- | `REMOTE_AUTHORIZATION` | 예 | 활성화하려면 `true` |
239
- | `STREAMABLE_HTTP` | 예 | 반드시 `true` |
240
- | `ENABLE_DYNAMIC_API_URL` | 선택 | 요청별 `X-GitLab-API-URL` 헤더 허용 |
241
+ | 환경 변수 | 필수 | 설명 |
242
+ | ------------------------ | ---- | ------------------------------------------------------------------- |
243
+ | `REMOTE_AUTHORIZATION` | 예 | 활성화하려면 `true` |
244
+ | `STREAMABLE_HTTP` | 예 | 반드시 `true` |
245
+ | `ENABLE_DYNAMIC_API_URL` | 선택 | 요청별 `X-GitLab-API-URL` 헤더 허용 |
246
+ | `GITLAB_ALLOWED_HOSTS` | 선택 | 허용할 호스트의 쉼표 구분 목록; `GITLAB_API_URL` 호스트는 항상 허용 |
241
247
 
242
248
  **예시 요청 헤더:**
243
249
 
package/README.md CHANGED
@@ -22,7 +22,7 @@ Supports PAT, OAuth, read-only mode, dynamic API URLs, and remote authorization
22
22
  - Client-friendly setup — examples for Claude Code, Codex, Antigravity, OpenCode, Copilot, Cline, Roo Code, Cursor, Kilo Code, and Amp Code
23
23
  - Self-hosted ready — works with custom GitLab instances, proxy settings, and dynamic API URL routing
24
24
 
25
- Quick start: choose either Personal Access Token or OAuth2 setup below and use `@zereight/mcp-gitlab` in your MCP client configuration.
25
+ Quick start: choose either Personal Access Token or OAuth2 setup below, install `@zereight/mcp-gitlab`, and use `zereight-mcp-gitlab` in your MCP client configuration.
26
26
 
27
27
  ### Client Setup Guides
28
28
 
@@ -67,6 +67,16 @@ The server supports four authentication methods:
67
67
 
68
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.
69
69
 
70
+ Install the server globally once:
71
+
72
+ ```shell
73
+ npm install -g @zereight/mcp-gitlab
74
+ ```
75
+
76
+ The examples use `zereight-mcp-gitlab`, a less collision-prone alias for the legacy `mcp-gitlab` binary. If your MCP client cannot find it, use the absolute path from `which zereight-mcp-gitlab`.
77
+
78
+ No global install? Pin `npx` to a known version, for example `npx -y @zereight/mcp-gitlab@2.1.27`.
79
+
70
80
  #### Using CLI Arguments (for clients with env var issues)
71
81
 
72
82
  Some MCP clients (like GitHub Copilot CLI) have issues with environment variables. Use CLI arguments instead:
@@ -75,13 +85,8 @@ Some MCP clients (like GitHub Copilot CLI) have issues with environment variable
75
85
  {
76
86
  "mcpServers": {
77
87
  "gitlab": {
78
- "command": "npx",
79
- "args": [
80
- "-y",
81
- "@zereight/mcp-gitlab",
82
- "--token=YOUR_GITLAB_TOKEN",
83
- "--api-url=https://gitlab.com/api/v4"
84
- ],
88
+ "command": "zereight-mcp-gitlab",
89
+ "args": ["--token=YOUR_GITLAB_TOKEN", "--api-url=https://gitlab.com/api/v4"],
85
90
  "tools": ["*"]
86
91
  }
87
92
  }
@@ -111,6 +116,7 @@ docker run -i --rm \
111
116
  -e USE_MILESTONE=true \
112
117
  -e USE_PIPELINE=true \
113
118
  -e SSE=true \
119
+ -e SSE_AUTH_TOKEN=your_mcp_sse_token \
114
120
  -p 3333:3002 \
115
121
  zereight050/gitlab-mcp
116
122
  ```
@@ -120,7 +126,10 @@ docker run -i --rm \
120
126
  "mcpServers": {
121
127
  "gitlab": {
122
128
  "type": "sse",
123
- "url": "http://localhost:3333/sse"
129
+ "url": "http://localhost:3333/sse",
130
+ "headers": {
131
+ "Authorization": "Bearer your_mcp_sse_token"
132
+ }
124
133
  }
125
134
  }
126
135
  }
@@ -182,10 +191,10 @@ Remote MCP OAuth is different. In `GITLAB_MCP_OAUTH=true` mode, the MCP client
182
191
  provides its own callback URL during `/authorize`. `GITLAB_OAUTH_REDIRECT_URI`
183
192
  does not replace that client-provided URL.
184
193
 
185
- | Mode | Enable with | Callback variable | GitLab redirect URI |
186
- | --- | --- | --- | --- |
187
- | Local OAuth | `GITLAB_USE_OAUTH=true` | `GITLAB_OAUTH_REDIRECT_URI` | `http://127.0.0.1:8888/callback` or your local callback |
188
- | Remote MCP OAuth | `GITLAB_MCP_OAUTH=true` | `GITLAB_OAUTH_CALLBACK_PROXY=true` | `{MCP_SERVER_URL}/callback` |
194
+ | Mode | Enable with | Callback variable | GitLab redirect URI |
195
+ | ---------------- | ----------------------- | ---------------------------------- | ------------------------------------------------------- |
196
+ | Local OAuth | `GITLAB_USE_OAUTH=true` | `GITLAB_OAUTH_REDIRECT_URI` | `http://127.0.0.1:8888/callback` or your local callback |
197
+ | Remote MCP OAuth | `GITLAB_MCP_OAUTH=true` | `GITLAB_OAUTH_CALLBACK_PROXY=true` | `{MCP_SERVER_URL}/callback` |
189
198
 
190
199
  Use `GITLAB_OAUTH_REDIRECT_URI` only when the MCP server itself owns the local
191
200
  browser callback. Use `GITLAB_OAUTH_CALLBACK_PROXY=true` when a remote MCP client
@@ -201,15 +210,15 @@ exchanging credentials with GitLab on behalf of the client.
201
210
  2. A pre-registered GitLab OAuth application with `api` (or `read_api`) scopes
202
211
  — Go to `Admin area` → `Applications`, set Redirect URI to `{MCP_SERVER_URL}/callback`
203
212
 
204
- | Environment Variable | Required | Description |
205
- | --------------------- | -------- | ---------------------------------------------------------- |
206
- | `GITLAB_MCP_OAUTH` | ✅ | Set to `true` to enable |
207
- | `GITLAB_API_URL` | ✅ | GitLab API base URL |
208
- | `GITLAB_OAUTH_APP_ID` | ✅ | GitLab OAuth Application ID |
209
- | `MCP_SERVER_URL` | ✅ | Public HTTPS URL of this MCP server |
210
- | `STREAMABLE_HTTP` | ✅ | Must be `true` |
211
- | `GITLAB_OAUTH_CALLBACK_PROXY` | optional | Set to `true` to use the MCP server's fixed `/callback` URL |
212
- | `GITLAB_OAUTH_SCOPES` | optional | Comma-separated scopes (default: `api,read_api,read_user`) |
213
+ | Environment Variable | Required | Description |
214
+ | ----------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------- |
215
+ | `GITLAB_MCP_OAUTH` | ✅ | Set to `true` to enable |
216
+ | `GITLAB_API_URL` | ✅ | GitLab API base URL |
217
+ | `GITLAB_OAUTH_APP_ID` | ✅ | GitLab OAuth Application ID |
218
+ | `MCP_SERVER_URL` | ✅ | Public HTTPS URL of this MCP server |
219
+ | `STREAMABLE_HTTP` | ✅ | Must be `true` |
220
+ | `GITLAB_OAUTH_CALLBACK_PROXY` | optional | Set to `true` to use the MCP server's fixed `/callback` URL |
221
+ | `GITLAB_OAUTH_SCOPES` | optional | Comma-separated scopes (default: `api,read_api,read_user`) |
213
222
  | `GITLAB_OAUTH_ALLOWED_GROUPS` | optional | Comma-separated group full paths — only members (and subgroup members) may obtain a token (replaces deprecated `GITLAB_ALLOWED_GROUPS`) |
214
223
 
215
224
  When `STREAMABLE_HTTP=true`, server-side `GITLAB_PERSONAL_ACCESS_TOKEN` or `GITLAB_JOB_TOKEN` require `REMOTE_AUTHORIZATION=true` or `GITLAB_MCP_OAUTH=true`.
@@ -262,13 +271,14 @@ the token to GitLab on behalf of the caller.
262
271
 
263
272
  **Header priority**: `Private-Token` > `JOB-TOKEN` > `Authorization: Bearer`
264
273
 
265
- | Environment Variable | Required | Description |
266
- | ------------------------ | -------- | ---------------------------------------------------------- |
267
- | `REMOTE_AUTHORIZATION` | ✅ | Set to `true` to enable |
268
- | `STREAMABLE_HTTP` | ✅ | Must be `true` |
269
- | `ENABLE_DYNAMIC_API_URL` | optional | Allow per-request GitLab URL via `X-GitLab-API-URL` header |
270
- | `GITLAB_ALLOW_UNAUTHENTICATED_TOOL_DISCOVERY` | optional | Allow unauthenticated `initialize`, `notifications/initialized`, and `tools/list` only (tool calls still require auth) |
271
- | `MCP_TRUST_PROXY` | optional | Trust `Forwarded` / `X-Forwarded-*` headers behind a reverse proxy (download URLs, Express `req.ip`, OAuth rate limits) |
274
+ | Environment Variable | Required | Description |
275
+ | --------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------- |
276
+ | `REMOTE_AUTHORIZATION` | ✅ | Set to `true` to enable |
277
+ | `STREAMABLE_HTTP` | ✅ | Must be `true` |
278
+ | `ENABLE_DYNAMIC_API_URL` | optional | Allow per-request GitLab URL via `X-GitLab-API-URL` header |
279
+ | `GITLAB_ALLOWED_HOSTS` | optional | Comma-separated allowed `X-GitLab-API-URL` hosts; `GITLAB_API_URL` hosts are always allowed |
280
+ | `GITLAB_ALLOW_UNAUTHENTICATED_TOOL_DISCOVERY` | optional | Allow unauthenticated `initialize`, `notifications/initialized`, and `tools/list` only (tool calls still require auth) |
281
+ | `MCP_TRUST_PROXY` | optional | Trust `Forwarded` / `X-Forwarded-*` headers behind a reverse proxy (download URLs, Express `req.ip`, OAuth rate limits) |
272
282
 
273
283
  `GITLAB_ALLOW_UNAUTHENTICATED_TOOL_DISCOVERY=true` is intended for MCP gateways
274
284
  or admin UIs that need to inspect tool metadata before a user provides a GitLab
package/README.zh-CN.md CHANGED
@@ -20,7 +20,7 @@
20
20
  - 客户端设置友好:提供 Claude Code、Codex、Antigravity、OpenCode、Copilot、Cline、Roo Code、Cursor、Kilo Code 和 Amp Code 示例
21
21
  - 适合自托管:支持自定义 GitLab 实例、代理设置和动态 API URL 路由
22
22
 
23
- 快速开始:在下面选择 Personal Access Token 或 OAuth2 设置,并在 MCP 客户端配置中使用 `@zereight/mcp-gitlab`。
23
+ 快速开始:在下面选择 Personal Access Token 或 OAuth2 设置,安装 `@zereight/mcp-gitlab`,并在 MCP 客户端配置中使用 `zereight-mcp-gitlab`。
24
24
 
25
25
  ### 客户端设置指南
26
26
 
@@ -63,6 +63,16 @@
63
63
 
64
64
  最简单的本地设置可以从 Personal Access Token 开始。基于浏览器的本地认证使用 OAuth2。远程或多用户部署请继续查看下面的 MCP OAuth 和远程授权部分。
65
65
 
66
+ 先全局安装一次服务器:
67
+
68
+ ```shell
69
+ npm install -g @zereight/mcp-gitlab
70
+ ```
71
+
72
+ 示例使用 `zereight-mcp-gitlab`,这是比旧的 `mcp-gitlab` 更不容易冲突的别名。如果 MCP 客户端找不到它,请使用 `which zereight-mcp-gitlab` 输出的绝对路径。
73
+
74
+ 如果不想全局安装,请固定 `npx` 版本,例如 `npx -y @zereight/mcp-gitlab@2.1.27`。
75
+
66
76
  #### 使用 CLI 参数(适用于环境变量有问题的客户端)
67
77
 
68
78
  部分 MCP 客户端(例如 GitHub Copilot CLI)可能难以处理环境变量。可以改用 CLI 参数。
@@ -71,13 +81,8 @@
71
81
  {
72
82
  "mcpServers": {
73
83
  "gitlab": {
74
- "command": "npx",
75
- "args": [
76
- "-y",
77
- "@zereight/mcp-gitlab",
78
- "--token=YOUR_GITLAB_TOKEN",
79
- "--api-url=https://gitlab.com/api/v4"
80
- ],
84
+ "command": "zereight-mcp-gitlab",
85
+ "args": ["--token=YOUR_GITLAB_TOKEN", "--api-url=https://gitlab.com/api/v4"],
81
86
  "tools": ["*"]
82
87
  }
83
88
  }
@@ -178,16 +183,16 @@ OpenCode、MCPJam、Claude.ai 等远程 MCP 客户端可能会在授权时发送
178
183
  2. 预先注册的 GitLab OAuth 应用,包含 `api` 或 `read_api` scopes
179
184
  — 前往 `Admin area` → `Applications`,将 Redirect URI 设置为 `{MCP_SERVER_URL}/callback`
180
185
 
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` | 可选 | 设置为 `true` 时使用 MCP 服务器固定的 `/callback` URL |
189
- | `GITLAB_OAUTH_SCOPES` | 可选 | 逗号分隔的 scope(默认:`api,read_api,read_user`) |
190
- | `GITLAB_OAUTH_ALLOWED_GROUPS` | 可选 | 逗号分隔的 GitLab 群组完整路径 — 仅该群组及其子群组的成员可获取令牌(替代已废弃的 `GITLAB_ALLOWED_GROUPS`)|
186
+ | 环境变量 | 必需 | 说明 |
187
+ | ----------------------------- | ---- | ----------------------------------------------------------------------------------------------------------- |
188
+ | `GITLAB_MCP_OAUTH` | 是 | 设置为 `true` 以启用 |
189
+ | `GITLAB_API_URL` | 是 | GitLab API base URL |
190
+ | `GITLAB_OAUTH_APP_ID` | 是 | GitLab OAuth Application ID |
191
+ | `MCP_SERVER_URL` | 是 | 此 MCP 服务器的公开 HTTPS URL |
192
+ | `STREAMABLE_HTTP` | 是 | 必须为 `true` |
193
+ | `GITLAB_OAUTH_CALLBACK_PROXY` | 可选 | 设置为 `true` 时使用 MCP 服务器固定的 `/callback` URL |
194
+ | `GITLAB_OAUTH_SCOPES` | 可选 | 逗号分隔的 scope(默认:`api,read_api,read_user`) |
195
+ | `GITLAB_OAUTH_ALLOWED_GROUPS` | 可选 | 逗号分隔的 GitLab 群组完整路径 — 仅该群组及其子群组的成员可获取令牌(替代已废弃的 `GITLAB_ALLOWED_GROUPS`) |
191
196
 
192
197
  > **排查 `Unregistered redirect_uri`**
193
198
  >
@@ -238,6 +243,7 @@ MCP 客户端配置:
238
243
  | `REMOTE_AUTHORIZATION` | 是 | 设置为 `true` 以启用 |
239
244
  | `STREAMABLE_HTTP` | 是 | 必须为 `true` |
240
245
  | `ENABLE_DYNAMIC_API_URL` | 可选 | 允许按请求通过 `X-GitLab-API-URL` 请求头指定 GitLab URL |
246
+ | `GITLAB_ALLOWED_HOSTS` | 可选 | 逗号分隔的允许主机;`GITLAB_API_URL` 中的主机始终允许 |
241
247
 
242
248
  **示例请求头:**
243
249
 
package/build/index.js CHANGED
@@ -467,6 +467,14 @@ function createServer() {
467
467
  /**
468
468
  * Validate configuration at startup
469
469
  */
470
+ function isLoopbackBindHost(host) {
471
+ const normalized = host.trim().toLowerCase().replace(/^\[|\]$/g, "");
472
+ const isIpv4Loopback = /^127(?:\.\d{1,3}){3}$/.test(normalized);
473
+ return (normalized === "localhost" ||
474
+ isIpv4Loopback ||
475
+ normalized === "::1" ||
476
+ normalized === "0:0:0:0:0:0:0:1");
477
+ }
470
478
  function validateConfiguration() {
471
479
  const errors = [];
472
480
  // Validate SESSION_TIMEOUT_SECONDS
@@ -517,6 +525,12 @@ function validateConfiguration() {
517
525
  }
518
526
  }
519
527
  }
528
+ const allowedHosts = getConfig("allowed-hosts", "GITLAB_ALLOWED_HOSTS")?.split(",") || [];
529
+ for (const host of allowedHosts) {
530
+ if (host.trim() && !toAllowedGitLabApiUrl(host)) {
531
+ errors.push(`GITLAB_ALLOWED_HOSTS contains an invalid host or URL: ${host.trim()}`);
532
+ }
533
+ }
520
534
  // Validate auth configuration
521
535
  const remoteAuth = getConfig("remote-auth", "REMOTE_AUTHORIZATION") === "true";
522
536
  const useOAuth = getConfig("use-oauth", "GITLAB_USE_OAUTH") === "true";
@@ -526,12 +540,19 @@ function validateConfiguration() {
526
540
  const mcpOAuth = getConfig("mcp-oauth", "GITLAB_MCP_OAUTH") === "true";
527
541
  const mcpServerUrl = getConfig("mcp-server-url", "MCP_SERVER_URL");
528
542
  const streamableHttp = getConfig("streamable-http", "STREAMABLE_HTTP") === "true";
543
+ const sse = getConfig("sse", "SSE") === "true";
544
+ const bindHost = getConfig("host", "HOST") || "127.0.0.1";
545
+ const sseAuthToken = getConfig("sse-auth-token", "SSE_AUTH_TOKEN");
546
+ const allowUnauthenticatedRemoteSse = getConfig("sse-dangerously-allow-unauthenticated-remote", "SSE_DANGEROUSLY_ALLOW_UNAUTHENTICATED_REMOTE") === "true";
529
547
  if (!remoteAuth && !useOAuth && !hasToken && !hasJobToken && !hasCookie && !mcpOAuth) {
530
548
  errors.push("Either --token, --job-token, --cookie-path, --use-oauth=true, --remote-auth=true, or --mcp-oauth=true must be set (or use environment variables)");
531
549
  }
532
550
  if (streamableHttp && (hasToken || hasJobToken) && !remoteAuth && !mcpOAuth) {
533
551
  errors.push("STREAMABLE_HTTP=true/--streamable-http with GITLAB_PERSONAL_ACCESS_TOKEN/--token or GITLAB_JOB_TOKEN/--job-token requires REMOTE_AUTHORIZATION=true/--remote-auth=true or GITLAB_MCP_OAUTH=true/--mcp-oauth=true");
534
552
  }
553
+ if (sse && !isLoopbackBindHost(bindHost) && !sseAuthToken && !allowUnauthenticatedRemoteSse) {
554
+ errors.push("SSE=true on a non-loopback HOST requires SSE_AUTH_TOKEN (or explicitly set SSE_DANGEROUSLY_ALLOW_UNAUTHENTICATED_REMOTE=true)");
555
+ }
535
556
  if (mcpOAuth) {
536
557
  if (!mcpServerUrl) {
537
558
  errors.push("MCP_SERVER_URL is required when GITLAB_MCP_OAUTH=true (e.g. https://mcp.example.com)");
@@ -986,11 +1007,57 @@ if (GITLAB_TOOLSETS_RAW && (USE_PIPELINE || USE_MILESTONE || USE_GITLAB_WIKI)) {
986
1007
  "Legacy flags add tools additively on top of the toolset selection and may produce unexpected results.");
987
1008
  }
988
1009
  const MERGE_REQUEST_DEPLOYMENT_SUMMARY_LIMIT = 10;
1010
+ function toAllowedGitLabApiUrl(value) {
1011
+ const trimmed = value.trim();
1012
+ if (!trimmed)
1013
+ return null;
1014
+ try {
1015
+ const url = new URL(trimmed.includes("://") ? trimmed : `https://${trimmed}`);
1016
+ if (url.protocol !== "http:" && url.protocol !== "https:")
1017
+ return null;
1018
+ return { host: url.host, apiUrl: normalizeGitLabApiUrl(url.toString()) };
1019
+ }
1020
+ catch {
1021
+ return null;
1022
+ }
1023
+ }
1024
+ function parseAllowedGitLabApiUrls(value) {
1025
+ return value
1026
+ .split(",")
1027
+ .map(toAllowedGitLabApiUrl)
1028
+ .filter((entry) => Boolean(entry));
1029
+ }
1030
+ function encodeGitLabPathSegment(value) {
1031
+ return encodeURIComponent(decodeURIComponent(value));
1032
+ }
1033
+ function encodeGitLabPath(value) {
1034
+ return value.split("/").map(encodeGitLabPathSegment).join("/");
1035
+ }
1036
+ function resolveTrustedGitLabApiUrl(value) {
1037
+ const parsed = new URL(normalizeGitLabApiUrl(value));
1038
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
1039
+ throw new Error("GitLab API URL must use HTTP or HTTPS");
1040
+ }
1041
+ const allowedApiUrl = GITLAB_ALLOWED_API_URLS_BY_HOST.get(parsed.host);
1042
+ if (!allowedApiUrl) {
1043
+ throw new Error(`GitLab API URL host is not allowed: ${parsed.host}`);
1044
+ }
1045
+ return allowedApiUrl;
1046
+ }
989
1047
  // Use the normalizeGitLabApiUrl function to handle various URL formats
990
1048
  const GITLAB_API_URLS = (getConfig("api-url", "GITLAB_API_URL") || "https://gitlab.com")
991
1049
  .split(",")
992
1050
  .map(normalizeGitLabApiUrl);
993
1051
  const GITLAB_API_URL = GITLAB_API_URLS[0];
1052
+ const GITLAB_ALLOWED_API_URLS_BY_HOST = new Map();
1053
+ for (const { host, apiUrl } of [
1054
+ ...GITLAB_API_URLS.map(toAllowedGitLabApiUrl).filter((entry) => Boolean(entry)),
1055
+ ...parseAllowedGitLabApiUrls(getConfig("allowed-hosts", "GITLAB_ALLOWED_HOSTS") || ""),
1056
+ ]) {
1057
+ if (!GITLAB_ALLOWED_API_URLS_BY_HOST.has(host)) {
1058
+ GITLAB_ALLOWED_API_URLS_BY_HOST.set(host, apiUrl);
1059
+ }
1060
+ }
994
1061
  const GITLAB_PROJECT_ID = process.env.GITLAB_PROJECT_ID;
995
1062
  const GITLAB_ALLOWED_PROJECT_IDS = process.env.GITLAB_ALLOWED_PROJECT_IDS?.split(",")
996
1063
  .map(id => id.trim())
@@ -8844,18 +8911,15 @@ function registerDownloadProxy(app, maxRequestsPerMinute = Number.parseInt(proce
8844
8911
  return;
8845
8912
  }
8846
8913
  // API URL: prefer token-embedded URL, then X-GitLab-API-URL header, then default
8847
- let apiUrl = tokenApiUrl || GITLAB_API_URL;
8848
- if (!tokenApiUrl) {
8849
- const dynamicApiUrl = req.headers["x-gitlab-api-url"]?.trim();
8850
- if (ENABLE_DYNAMIC_API_URL && dynamicApiUrl) {
8851
- try {
8852
- new URL(dynamicApiUrl);
8853
- apiUrl = normalizeGitLabApiUrl(dynamicApiUrl);
8854
- }
8855
- catch {
8856
- res.status(400).json({ error: "Invalid X-GitLab-API-URL" });
8857
- return;
8858
- }
8914
+ let apiUrl = GITLAB_API_URL;
8915
+ const requestedApiUrl = tokenApiUrl || req.headers["x-gitlab-api-url"]?.trim();
8916
+ if (ENABLE_DYNAMIC_API_URL && requestedApiUrl) {
8917
+ try {
8918
+ apiUrl = resolveTrustedGitLabApiUrl(requestedApiUrl);
8919
+ }
8920
+ catch {
8921
+ res.status(400).json({ error: "Invalid X-GitLab-API-URL" });
8922
+ return;
8859
8923
  }
8860
8924
  }
8861
8925
  const { type } = req.params;
@@ -8869,7 +8933,7 @@ function registerDownloadProxy(app, maxRequestsPerMinute = Number.parseInt(proce
8869
8933
  return;
8870
8934
  }
8871
8935
  const effectiveProjectId = getEffectiveProjectId(decodeURIComponent(project_id));
8872
- gitlabUrl = `${apiUrl}/projects/${encodeURIComponent(effectiveProjectId)}/jobs/${job_id}/artifacts`;
8936
+ gitlabUrl = `${apiUrl}/projects/${encodeURIComponent(effectiveProjectId)}/jobs/${encodeGitLabPathSegment(job_id)}/artifacts`;
8873
8937
  break;
8874
8938
  }
8875
8939
  case "attachment": {
@@ -8879,7 +8943,7 @@ function registerDownloadProxy(app, maxRequestsPerMinute = Number.parseInt(proce
8879
8943
  return;
8880
8944
  }
8881
8945
  const effectiveProjectId = getEffectiveProjectId(decodeURIComponent(project_id));
8882
- gitlabUrl = `${apiUrl}/projects/${encodeURIComponent(effectiveProjectId)}/uploads/${secret}/${filename}`;
8946
+ gitlabUrl = `${apiUrl}/projects/${encodeURIComponent(effectiveProjectId)}/uploads/${encodeGitLabPathSegment(secret)}/${encodeGitLabPath(filename)}`;
8883
8947
  break;
8884
8948
  }
8885
8949
  case "release-asset": {
@@ -8891,7 +8955,7 @@ function registerDownloadProxy(app, maxRequestsPerMinute = Number.parseInt(proce
8891
8955
  return;
8892
8956
  }
8893
8957
  const effectiveProjectId = getEffectiveProjectId(decodeURIComponent(project_id));
8894
- gitlabUrl = `${apiUrl}/projects/${encodeURIComponent(effectiveProjectId)}/releases/${encodeURIComponent(tag_name)}/downloads/${direct_asset_path}`;
8958
+ gitlabUrl = `${apiUrl}/projects/${encodeURIComponent(effectiveProjectId)}/releases/${encodeURIComponent(tag_name)}/downloads/${encodeGitLabPath(direct_asset_path)}`;
8895
8959
  break;
8896
8960
  }
8897
8961
  default:
@@ -8943,12 +9007,21 @@ function registerDownloadProxy(app, maxRequestsPerMinute = Number.parseInt(proce
8943
9007
  */
8944
9008
  async function startSSEServer() {
8945
9009
  const app = express();
9010
+ const sseAuthToken = getConfig("sse-auth-token", "SSE_AUTH_TOKEN");
8946
9011
  if (MCP_TRUST_PROXY) {
8947
9012
  app.set("trust proxy", 1);
8948
9013
  }
9014
+ const requireSseAuth = (req, res, next) => {
9015
+ if (!sseAuthToken)
9016
+ return next();
9017
+ const match = /^Bearer\s+(\S+)$/i.exec(req.headers.authorization || "");
9018
+ if (match?.[1] === sseAuthToken)
9019
+ return next();
9020
+ res.status(401).json({ error: "SSE authentication required" });
9021
+ };
8949
9022
  const transports = {};
8950
9023
  let shuttingDown = false;
8951
- app.get("/sse", async (_, res) => {
9024
+ app.get("/sse", requireSseAuth, async (_, res) => {
8952
9025
  const serverInstance = createServer();
8953
9026
  const transport = new SSEServerTransport("/messages", res);
8954
9027
  transports[transport.sessionId] = transport;
@@ -8957,7 +9030,7 @@ async function startSSEServer() {
8957
9030
  });
8958
9031
  await serverInstance.connect(transport);
8959
9032
  });
8960
- app.post("/messages", async (req, res) => {
9033
+ app.post("/messages", requireSseAuth, async (req, res) => {
8961
9034
  const sessionId = req.query.sessionId;
8962
9035
  const transport = transports[sessionId];
8963
9036
  if (transport) {
@@ -9081,12 +9154,11 @@ async function startStreamableHTTPServer() {
9081
9154
  // Only process dynamic URL if the feature is enabled
9082
9155
  if (ENABLE_DYNAMIC_API_URL && dynamicApiUrl) {
9083
9156
  try {
9084
- new URL(dynamicApiUrl); // Ensure it's a valid URL format
9085
- apiUrl = normalizeGitLabApiUrl(dynamicApiUrl);
9157
+ apiUrl = resolveTrustedGitLabApiUrl(dynamicApiUrl);
9086
9158
  }
9087
9159
  catch {
9088
9160
  logger.warn(`Invalid X-GitLab-API-URL provided: ${dynamicApiUrl}. Auth will fail.`);
9089
- return null; // Reject if URL is malformed
9161
+ return null; // Reject if URL is malformed or not allowed
9090
9162
  }
9091
9163
  }
9092
9164
  // Extract token — priority: Private-Token > JOB-TOKEN > Authorization Bearer
@@ -0,0 +1,104 @@
1
+ import { after, before, describe, test } from "node:test";
2
+ import assert from "node:assert";
3
+ import { createServer } from "node:http";
4
+ import { cleanupServers, findAvailablePort, HOST, launchServer, TransportMode, } from "./utils/server-launcher.js";
5
+ import { MockGitLabServer, findMockServerPort } from "./utils/mock-gitlab-server.js";
6
+ import { CustomHeaderClient } from "./clients/custom-header-client.js";
7
+ const MOCK_TOKEN = "glpat-dynamic-url-token";
8
+ async function startAttackerServer(port) {
9
+ let hits = 0;
10
+ const server = createServer((_req, res) => {
11
+ hits++;
12
+ res.writeHead(200, { "content-type": "application/json" });
13
+ res.end("[]");
14
+ });
15
+ await new Promise((resolve, reject) => {
16
+ server.once("error", reject);
17
+ server.listen(port, HOST, () => {
18
+ server.off("error", reject);
19
+ resolve();
20
+ });
21
+ });
22
+ return { server, getHits: () => hits };
23
+ }
24
+ describe("Dynamic API URL allowlist", () => {
25
+ let primaryGitLab;
26
+ let secondaryGitLab;
27
+ let attackerServer;
28
+ let mcpServer;
29
+ let mcpUrl;
30
+ let secondaryHit = false;
31
+ let getAttackerHits = () => 0;
32
+ before(async () => {
33
+ const primaryPort = await findMockServerPort(9100);
34
+ primaryGitLab = new MockGitLabServer({ port: primaryPort, validTokens: [MOCK_TOKEN] });
35
+ await primaryGitLab.start();
36
+ const secondaryPort = await findMockServerPort(9200);
37
+ secondaryGitLab = new MockGitLabServer({ port: secondaryPort, validTokens: [MOCK_TOKEN] });
38
+ secondaryGitLab.addMockHandler("get", "/projects/1/issues", (_req, res) => {
39
+ secondaryHit = true;
40
+ res.json([]);
41
+ });
42
+ await secondaryGitLab.start();
43
+ const attackerPort = await findAvailablePort(9300);
44
+ const attacker = await startAttackerServer(attackerPort);
45
+ attackerServer = attacker.server;
46
+ getAttackerHits = attacker.getHits;
47
+ const mcpPort = await findAvailablePort(3100);
48
+ mcpServer = await launchServer({
49
+ mode: TransportMode.STREAMABLE_HTTP,
50
+ port: mcpPort,
51
+ timeout: 5000,
52
+ env: {
53
+ STREAMABLE_HTTP: "true",
54
+ REMOTE_AUTHORIZATION: "true",
55
+ ENABLE_DYNAMIC_API_URL: "true",
56
+ GITLAB_API_URL: `${primaryGitLab.getUrl()}/api/v4`,
57
+ GITLAB_ALLOWED_HOSTS: `${secondaryGitLab.getUrl()}/api/v4`,
58
+ },
59
+ });
60
+ mcpUrl = `http://${HOST}:${mcpPort}/mcp`;
61
+ });
62
+ after(async () => {
63
+ if (mcpServer)
64
+ cleanupServers([mcpServer]);
65
+ await primaryGitLab?.stop();
66
+ await secondaryGitLab?.stop();
67
+ const server = attackerServer;
68
+ if (server)
69
+ await new Promise(resolve => server.close(() => resolve()));
70
+ });
71
+ test("allows dynamic API URLs on configured hosts", async () => {
72
+ const client = new CustomHeaderClient({
73
+ authorization: `Bearer ${MOCK_TOKEN}`,
74
+ "x-gitlab-api-url": `${secondaryGitLab.getUrl()}/api/v4`,
75
+ });
76
+ await client.connect(mcpUrl);
77
+ await client.callTool("list_issues", { project_id: "1" });
78
+ await client.disconnect();
79
+ assert.strictEqual(secondaryHit, true, "allowed dynamic host should receive GitLab calls");
80
+ });
81
+ test("rejects dynamic API URLs on unconfigured hosts before forwarding tokens", async () => {
82
+ const server = attackerServer;
83
+ assert.ok(server, "attacker server should be running");
84
+ const attackerUrl = `http://${HOST}:${server.address().port}/api/v4`;
85
+ const client = new CustomHeaderClient({
86
+ authorization: `Bearer ${MOCK_TOKEN}`,
87
+ "x-gitlab-api-url": attackerUrl,
88
+ });
89
+ let connected = false;
90
+ try {
91
+ await client.connect(mcpUrl);
92
+ connected = true;
93
+ await client.callTool("list_issues", { project_id: "1" });
94
+ }
95
+ catch {
96
+ // Expected: the session is rejected before any GitLab API request is made.
97
+ }
98
+ finally {
99
+ await client.disconnect();
100
+ }
101
+ assert.strictEqual(connected, false, "untrusted dynamic host should not initialize a session");
102
+ assert.strictEqual(getAttackerHits(), 0, "token-bearing requests must not reach untrusted hosts");
103
+ });
104
+ });
@@ -0,0 +1,96 @@
1
+ import assert from "node:assert/strict";
2
+ import { spawn } from "node:child_process";
3
+ import { once } from "node:events";
4
+ import { afterEach, describe, test } from "node:test";
5
+ import * as path from "node:path";
6
+ import { findAvailablePort } from "./utils/server-launcher.js";
7
+ const ERROR_MESSAGE = "SSE=true on a non-loopback HOST requires SSE_AUTH_TOKEN (or explicitly set SSE_DANGEROUSLY_ALLOW_UNAUTHENTICATED_REMOTE=true)";
8
+ const LOOPBACK = "127.0.0.1";
9
+ const SERVER_PATH = path.resolve(process.cwd(), "build/index.js");
10
+ const running = new Set();
11
+ function startSseServer(env, port) {
12
+ const child = spawn("node", [SERVER_PATH], {
13
+ env: {
14
+ ...process.env,
15
+ GITLAB_API_URL: "https://gitlab.example.com/api/v4",
16
+ HOST: "0.0.0.0",
17
+ PORT: String(port),
18
+ SSE: "true",
19
+ STREAMABLE_HTTP: "false",
20
+ REMOTE_AUTHORIZATION: "false",
21
+ GITLAB_MCP_OAUTH: "false",
22
+ GITLAB_USE_OAUTH: "false",
23
+ GITLAB_PERSONAL_ACCESS_TOKEN: "glpat_test",
24
+ GITLAB_JOB_TOKEN: "",
25
+ GITLAB_AUTH_COOKIE_PATH: "",
26
+ ...env,
27
+ },
28
+ stdio: ["ignore", "pipe", "pipe"],
29
+ });
30
+ running.add(child);
31
+ child.once("exit", () => running.delete(child));
32
+ return child;
33
+ }
34
+ async function waitForExit(child, timeoutMs = 5000) {
35
+ let output = "";
36
+ child.stdout?.on("data", chunk => {
37
+ output += chunk.toString();
38
+ });
39
+ child.stderr?.on("data", chunk => {
40
+ output += chunk.toString();
41
+ });
42
+ let timeoutHandle;
43
+ const timeout = new Promise((_, reject) => {
44
+ timeoutHandle = setTimeout(() => reject(new Error(`server did not exit within ${timeoutMs}ms`)), timeoutMs);
45
+ });
46
+ try {
47
+ const [code] = (await Promise.race([once(child, "exit"), timeout]));
48
+ return { code, output };
49
+ }
50
+ finally {
51
+ clearTimeout(timeoutHandle);
52
+ }
53
+ }
54
+ async function waitForHealth(port, timeoutMs = 5000) {
55
+ const deadline = Date.now() + timeoutMs;
56
+ let lastError;
57
+ while (Date.now() < deadline) {
58
+ try {
59
+ const response = await fetch(`http://${LOOPBACK}:${port}/health`);
60
+ if (response.ok)
61
+ return;
62
+ }
63
+ catch (error) {
64
+ lastError = error;
65
+ }
66
+ await new Promise(resolve => setTimeout(resolve, 100));
67
+ }
68
+ throw new Error(`server did not become healthy: ${String(lastError)}`);
69
+ }
70
+ afterEach(() => {
71
+ for (const child of running) {
72
+ if (!child.killed)
73
+ child.kill("SIGTERM");
74
+ }
75
+ running.clear();
76
+ });
77
+ describe("SSE remote auth guard", () => {
78
+ test("refuses unauthenticated SSE on non-loopback hosts", async () => {
79
+ const port = await findAvailablePort(4400);
80
+ const child = startSseServer({}, port);
81
+ const { code, output } = await waitForExit(child);
82
+ assert.notEqual(code, 0);
83
+ assert.match(output, new RegExp(ERROR_MESSAGE.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
84
+ });
85
+ test("requires bearer auth on SSE endpoints when SSE_AUTH_TOKEN is configured", async () => {
86
+ const port = await findAvailablePort(4410);
87
+ startSseServer({ SSE_AUTH_TOKEN: "mcp_sse_secret" }, port);
88
+ await waitForHealth(port);
89
+ const sseResponse = await fetch(`http://${LOOPBACK}:${port}/sse`);
90
+ assert.equal(sseResponse.status, 401);
91
+ const unauthenticatedMessage = await fetch(`http://${LOOPBACK}:${port}/messages?sessionId=missing`, { method: "POST" });
92
+ assert.equal(unauthenticatedMessage.status, 401);
93
+ const authenticatedMessage = await fetch(`http://${LOOPBACK}:${port}/messages?sessionId=missing`, { method: "POST", headers: { Authorization: "Bearer mcp_sse_secret" } });
94
+ assert.equal(authenticatedMessage.status, 400);
95
+ });
96
+ });
@@ -87,17 +87,48 @@ export const toJSONSchema = (schema) => {
87
87
  return obj;
88
88
  }
89
89
  const fixedSchema = fixNullableOptional(jsonSchema, true);
90
- if (!fixedSchema.properties && Array.isArray(fixedSchema.anyOf)) {
91
- const variants = fixedSchema.anyOf.filter((item) => item?.type === "object" && item.properties);
92
- if (variants.length === fixedSchema.anyOf.length) {
93
- fixedSchema.type = "object";
94
- fixedSchema.properties = variants.reduce((properties, item) => {
95
- Object.entries(item.properties).forEach(([key, value]) => {
96
- if (!properties[key])
97
- properties[key] = value;
98
- });
99
- return properties;
100
- }, {});
90
+ // Flatten top-level anyOf/oneOf into a single object schema for Anthropic API compatibility.
91
+ // The Anthropic API rejects tool input_schema with top-level oneOf/allOf/anyOf.
92
+ for (const combiner of ["anyOf", "oneOf", "allOf"]) {
93
+ if (Array.isArray(fixedSchema[combiner])) {
94
+ const variants = fixedSchema[combiner].filter((item) => item?.type === "object" && item.properties);
95
+ if (variants.length > 0 && variants.length === fixedSchema[combiner].length) {
96
+ fixedSchema.type = "object";
97
+ fixedSchema.properties = fixedSchema.properties || {};
98
+ for (const variant of variants) {
99
+ for (const [key, value] of Object.entries(variant.properties)) {
100
+ if (!fixedSchema.properties[key]) {
101
+ fixedSchema.properties[key] = value;
102
+ }
103
+ }
104
+ }
105
+ // Compute required fields based on combiner semantics:
106
+ // - allOf: union (all schemas apply, so all requirements apply)
107
+ // - anyOf/oneOf: intersection (only shared requirements are universal)
108
+ const requiredSets = variants.map((v) => new Set(Array.isArray(v.required) ? v.required : []));
109
+ let mergedRequired;
110
+ if (combiner === "allOf") {
111
+ // Union: any field required in any variant is required
112
+ const all = new Set();
113
+ for (const s of requiredSets) {
114
+ for (const field of s)
115
+ all.add(field);
116
+ }
117
+ mergedRequired = [...all];
118
+ }
119
+ else {
120
+ // Intersection: only fields required in ALL variants
121
+ mergedRequired = [...requiredSets[0]].filter(field => requiredSets.every((s) => s.has(field)));
122
+ }
123
+ if (mergedRequired.length > 0) {
124
+ const existing = new Set(Array.isArray(fixedSchema.required) ? fixedSchema.required : []);
125
+ for (const field of mergedRequired) {
126
+ existing.add(field);
127
+ }
128
+ fixedSchema.required = [...existing];
129
+ }
130
+ delete fixedSchema[combiner];
131
+ }
101
132
  }
102
133
  }
103
134
  return fixedSchema;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zereight/mcp-gitlab",
3
- "version": "2.1.26",
3
+ "version": "2.1.28",
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": [
@@ -26,7 +26,8 @@
26
26
  "author": "zereight",
27
27
  "type": "module",
28
28
  "bin": {
29
- "mcp-gitlab": "build/index.js"
29
+ "mcp-gitlab": "build/index.js",
30
+ "zereight-mcp-gitlab": "build/index.js"
30
31
  },
31
32
  "files": [
32
33
  "build"
@@ -51,7 +52,7 @@
51
52
  "changelog": "auto-changelog -p",
52
53
  "test": "npm run test:all",
53
54
  "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/test-oauth-proxy-rate-limit.ts && node --import tsx/esm --test test/streamable-http-static-token-auth.test.ts && node --import tsx/esm --test test/streamable-http-concurrent-session.test.ts && node --import tsx/esm --test test/streamable-http-unauthenticated-discovery.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-create-repository.ts && node --import tsx/esm --test test/test-update-project.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-ci-catalog.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/forwarded-public-base-url.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
+ "test:mock": "node --import tsx/esm --test test/remote-auth-simple-test.ts && node --import tsx/esm --test test/dynamic-api-url-allowlist.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 && node --import tsx/esm --test test/sse-auth-guard.test.ts && node --import tsx/esm --test test/streamable-http-concurrent-session.test.ts && node --import tsx/esm --test test/streamable-http-unauthenticated-discovery.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-create-repository.ts && node --import tsx/esm --test test/test-update-project.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-ci-catalog.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/forwarded-public-base-url.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
56
  "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
57
  "test:mcp-oauth": "npm run build && node --import tsx/esm --test test/mcp-oauth-tests.ts",
57
58
  "test:live": "node test/validate-api.js",
@@ -70,7 +71,7 @@
70
71
  "format:check": "prettier --check \"**/*.{js,ts,json,md}\""
71
72
  },
72
73
  "dependencies": {
73
- "@modelcontextprotocol/sdk": "^1.24.2",
74
+ "@modelcontextprotocol/sdk": "^1.29.0",
74
75
  "@types/node-fetch": "^2.6.12",
75
76
  "diff": "^9.0.0",
76
77
  "express": "^5.1.0",