@zereight/mcp-gitlab 2.1.27 → 2.1.29

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.29`처럼 버전을 고정하세요.
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
  }
@@ -89,12 +94,19 @@ PAT, OAuth, 읽기 전용 모드, 동적 API URL, 원격 인증을 지원하며
89
94
  - `--token` - GitLab Personal Access Token (`GITLAB_PERSONAL_ACCESS_TOKEN` 대체)
90
95
  - `--api-url` - GitLab API URL (`GITLAB_API_URL` 대체)
91
96
  - `--read-only=true` - 읽기 전용 모드 활성화 (`GITLAB_READ_ONLY_MODE` 대체)
92
- - `--use-wiki=true` - 위키 API 활성화 (`USE_GITLAB_WIKI` 대체)
93
- - `--use-milestone=true` - 마일스톤 API 활성화 (`USE_MILESTONE` 대체)
94
- - `--use-pipeline=true` - 파이프라인 API 활성화 (`USE_PIPELINE` 대체)
97
+ - `--use-wiki=true` - 위키 API 활성화 (`USE_GITLAB_WIKI` 대체, 레거시 — `GITLAB_TOOLSETS=wiki` 권장)
98
+ - `--use-milestone=true` - 마일스톤 API 활성화 (`USE_MILESTONE` 대체, 레거시 — `GITLAB_TOOLSETS=milestones` 권장)
99
+ - `--use-pipeline=true` - 파이프라인 API 활성화 (`USE_PIPELINE` 대체, 레거시 — `GITLAB_TOOLSETS=pipelines` 권장)
95
100
 
96
101
  CLI 인자는 환경 변수보다 우선합니다.
97
102
 
103
+ > **세밀한 도구 필터링:** 전체 on/off 방식인 `GITLAB_READ_ONLY_MODE` 외에도,
104
+ > `GITLAB_TOOLSETS=<group,…>`로 도구 그룹을 활성화하고, `GITLAB_TOOLS=<tool,…>`로 개별 도구만
105
+ > 허용하며(예: 읽기 도구 + 특정 쓰기 도구 몇 개), `GITLAB_DENIED_TOOLS_REGEX`로 패턴 차단할 수
106
+ > 있습니다. 레거시 `USE_GITLAB_WIKI` / `USE_MILESTONE` / `USE_PIPELINE` 플래그는 하위 호환용으로만
107
+ > 유지됩니다. [Tools Reference](./docs/tools/index.md#feature-toggles)와
108
+ > [Environment Variables](./docs/configuration/environment-variables.md)를 참고하세요.
109
+
98
110
  #### SSE
99
111
 
100
112
  ```shell
@@ -103,9 +115,7 @@ docker run -i --rm \
103
115
  -e GITLAB_PERSONAL_ACCESS_TOKEN=your_gitlab_token \
104
116
  -e GITLAB_API_URL="https://gitlab.com/api/v4" \
105
117
  -e GITLAB_READ_ONLY_MODE=true \
106
- -e USE_GITLAB_WIKI=true \
107
- -e USE_MILESTONE=true \
108
- -e USE_PIPELINE=true \
118
+ -e GITLAB_TOOLSETS=wiki,milestones,pipelines \
109
119
  -e SSE=true \
110
120
  -p 3333:3002 \
111
121
  zereight050/gitlab-mcp
@@ -130,9 +140,7 @@ docker run -i --rm \
130
140
  -e GITLAB_PERSONAL_ACCESS_TOKEN=your_gitlab_token \
131
141
  -e GITLAB_API_URL="https://gitlab.com/api/v4" \
132
142
  -e GITLAB_READ_ONLY_MODE=true \
133
- -e USE_GITLAB_WIKI=true \
134
- -e USE_MILESTONE=true \
135
- -e USE_PIPELINE=true \
143
+ -e GITLAB_TOOLSETS=wiki,milestones,pipelines \
136
144
  -e STREAMABLE_HTTP=true \
137
145
  -p 3333:3002 \
138
146
  zereight050/gitlab-mcp
@@ -178,15 +186,15 @@ MCP 서버가 직접 로컬 브라우저 callback을 받을 때만 `GITLAB_OAUTH
178
186
  2. `api` 또는 `read_api` scope가 있는 사전 등록 GitLab OAuth 애플리케이션
179
187
  — `Admin area` → `Applications`에서 Redirect URI를 `{MCP_SERVER_URL}/callback`으로 설정하세요.
180
188
 
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`) |
189
+ | 환경 변수 | 필수 | 설명 |
190
+ | ----------------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------- |
191
+ | `GITLAB_MCP_OAUTH` | 예 | 활성화하려면 `true` |
192
+ | `GITLAB_API_URL` | 예 | GitLab API base URL |
193
+ | `GITLAB_OAUTH_APP_ID` | 예 | GitLab OAuth Application ID |
194
+ | `MCP_SERVER_URL` | 예 | 이 MCP 서버의 공개 HTTPS URL |
195
+ | `STREAMABLE_HTTP` | 예 | 반드시 `true` |
196
+ | `GITLAB_OAUTH_CALLBACK_PROXY` | 선택 | MCP 서버의 고정 `/callback` URL을 사용하려면 `true` |
197
+ | `GITLAB_OAUTH_SCOPES` | 선택 | 쉼표로 구분된 scope 목록(기본값: `api,read_api,read_user`) |
190
198
  | `GITLAB_OAUTH_ALLOWED_GROUPS` | 선택 | 쉼표로 구분된 GitLab 그룹 전체 경로 — 해당 그룹 및 하위 그룹 멤버만 토큰을 발급받을 수 있음 (기존 `GITLAB_ALLOWED_GROUPS` 대체) |
191
199
 
192
200
  > **`Unregistered redirect_uri` 문제 해결**
@@ -233,12 +241,12 @@ MCP 클라이언트 설정:
233
241
 
234
242
  **헤더 우선순위**: `Private-Token` > `JOB-TOKEN` > `Authorization: Bearer`
235
243
 
236
- | 환경 변수 | 필수 | 설명 |
237
- | ------------------------ | ---- | ----------------------------------- |
238
- | `REMOTE_AUTHORIZATION` | 예 | 활성화하려면 `true` |
239
- | `STREAMABLE_HTTP` | 예 | 반드시 `true` |
240
- | `ENABLE_DYNAMIC_API_URL` | 선택 | 요청별 `X-GitLab-API-URL` 헤더 허용 |
241
- | `GITLAB_ALLOWED_HOSTS` | 선택 | 허용할 호스트의 쉼표 구분 목록; `GITLAB_API_URL` 호스트는 항상 허용 |
244
+ | 환경 변수 | 필수 | 설명 |
245
+ | ------------------------ | ---- | ------------------------------------------------------------------- |
246
+ | `REMOTE_AUTHORIZATION` | 예 | 활성화하려면 `true` |
247
+ | `STREAMABLE_HTTP` | 예 | 반드시 `true` |
248
+ | `ENABLE_DYNAMIC_API_URL` | 선택 | 요청별 `X-GitLab-API-URL` 헤더 허용 |
249
+ | `GITLAB_ALLOWED_HOSTS` | 선택 | 허용할 호스트의 쉼표 구분 목록; `GITLAB_API_URL` 호스트는 항상 허용 |
242
250
 
243
251
  **예시 요청 헤더:**
244
252
 
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.29`.
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
  }
@@ -93,12 +98,20 @@ Some MCP clients (like GitHub Copilot CLI) have issues with environment variable
93
98
  - `--token` - GitLab Personal Access Token (replaces `GITLAB_PERSONAL_ACCESS_TOKEN`)
94
99
  - `--api-url` - GitLab API URL (replaces `GITLAB_API_URL`)
95
100
  - `--read-only=true` - Enable read-only mode (replaces `GITLAB_READ_ONLY_MODE`)
96
- - `--use-wiki=true` - Enable wiki API (replaces `USE_GITLAB_WIKI`)
97
- - `--use-milestone=true` - Enable milestone API (replaces `USE_MILESTONE`)
98
- - `--use-pipeline=true` - Enable pipeline API (replaces `USE_PIPELINE`)
101
+ - `--use-wiki=true` - Enable wiki API (replaces `USE_GITLAB_WIKI`, legacy — prefer `GITLAB_TOOLSETS=wiki`)
102
+ - `--use-milestone=true` - Enable milestone API (replaces `USE_MILESTONE`, legacy — prefer `GITLAB_TOOLSETS=milestones`)
103
+ - `--use-pipeline=true` - Enable pipeline API (replaces `USE_PIPELINE`, legacy — prefer `GITLAB_TOOLSETS=pipelines`)
99
104
 
100
105
  CLI arguments take precedence over environment variables.
101
106
 
107
+ > **Fine-grained tool filtering:** beyond the all-or-nothing `GITLAB_READ_ONLY_MODE`, you can
108
+ > enable toolset groups with `GITLAB_TOOLSETS=<group,…>`, allow-list individual tools with
109
+ > `GITLAB_TOOLS=<tool,…>` (e.g. read-only groups plus a few specific write tools), and
110
+ > deny-list by pattern with `GITLAB_DENIED_TOOLS_REGEX`. The legacy `USE_GITLAB_WIKI` /
111
+ > `USE_MILESTONE` / `USE_PIPELINE` flags are kept for backward compatibility only.
112
+ > See [Tools Reference](./docs/tools/index.md#feature-toggles) and
113
+ > [Environment Variables](./docs/configuration/environment-variables.md).
114
+
102
115
  - sse
103
116
 
104
117
  ```shell
@@ -107,9 +120,7 @@ docker run -i --rm \
107
120
  -e GITLAB_PERSONAL_ACCESS_TOKEN=your_gitlab_token \
108
121
  -e GITLAB_API_URL="https://gitlab.com/api/v4" \
109
122
  -e GITLAB_READ_ONLY_MODE=true \
110
- -e USE_GITLAB_WIKI=true \
111
- -e USE_MILESTONE=true \
112
- -e USE_PIPELINE=true \
123
+ -e GITLAB_TOOLSETS=wiki,milestones,pipelines \
113
124
  -e SSE=true \
114
125
  -e SSE_AUTH_TOKEN=your_mcp_sse_token \
115
126
  -p 3333:3002 \
@@ -138,9 +149,7 @@ docker run -i --rm \
138
149
  -e REMOTE_AUTHORIZATION=true \
139
150
  -e GITLAB_API_URL="https://gitlab.com/api/v4" \
140
151
  -e GITLAB_READ_ONLY_MODE=true \
141
- -e USE_GITLAB_WIKI=true \
142
- -e USE_MILESTONE=true \
143
- -e USE_PIPELINE=true \
152
+ -e GITLAB_TOOLSETS=wiki,milestones,pipelines \
144
153
  -e STREAMABLE_HTTP=true \
145
154
  -p 3333:3002 \
146
155
  zereight050/gitlab-mcp
@@ -186,10 +195,10 @@ Remote MCP OAuth is different. In `GITLAB_MCP_OAUTH=true` mode, the MCP client
186
195
  provides its own callback URL during `/authorize`. `GITLAB_OAUTH_REDIRECT_URI`
187
196
  does not replace that client-provided URL.
188
197
 
189
- | Mode | Enable with | Callback variable | GitLab redirect URI |
190
- | --- | --- | --- | --- |
191
- | Local OAuth | `GITLAB_USE_OAUTH=true` | `GITLAB_OAUTH_REDIRECT_URI` | `http://127.0.0.1:8888/callback` or your local callback |
192
- | Remote MCP OAuth | `GITLAB_MCP_OAUTH=true` | `GITLAB_OAUTH_CALLBACK_PROXY=true` | `{MCP_SERVER_URL}/callback` |
198
+ | Mode | Enable with | Callback variable | GitLab redirect URI |
199
+ | ---------------- | ----------------------- | ---------------------------------- | ------------------------------------------------------- |
200
+ | Local OAuth | `GITLAB_USE_OAUTH=true` | `GITLAB_OAUTH_REDIRECT_URI` | `http://127.0.0.1:8888/callback` or your local callback |
201
+ | Remote MCP OAuth | `GITLAB_MCP_OAUTH=true` | `GITLAB_OAUTH_CALLBACK_PROXY=true` | `{MCP_SERVER_URL}/callback` |
193
202
 
194
203
  Use `GITLAB_OAUTH_REDIRECT_URI` only when the MCP server itself owns the local
195
204
  browser callback. Use `GITLAB_OAUTH_CALLBACK_PROXY=true` when a remote MCP client
@@ -205,15 +214,15 @@ exchanging credentials with GitLab on behalf of the client.
205
214
  2. A pre-registered GitLab OAuth application with `api` (or `read_api`) scopes
206
215
  — Go to `Admin area` → `Applications`, set Redirect URI to `{MCP_SERVER_URL}/callback`
207
216
 
208
- | Environment Variable | Required | Description |
209
- | --------------------- | -------- | ---------------------------------------------------------- |
210
- | `GITLAB_MCP_OAUTH` | ✅ | Set to `true` to enable |
211
- | `GITLAB_API_URL` | ✅ | GitLab API base URL |
212
- | `GITLAB_OAUTH_APP_ID` | ✅ | GitLab OAuth Application ID |
213
- | `MCP_SERVER_URL` | ✅ | Public HTTPS URL of this MCP server |
214
- | `STREAMABLE_HTTP` | ✅ | Must be `true` |
215
- | `GITLAB_OAUTH_CALLBACK_PROXY` | optional | Set to `true` to use the MCP server's fixed `/callback` URL |
216
- | `GITLAB_OAUTH_SCOPES` | optional | Comma-separated scopes (default: `api,read_api,read_user`) |
217
+ | Environment Variable | Required | Description |
218
+ | ----------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------- |
219
+ | `GITLAB_MCP_OAUTH` | ✅ | Set to `true` to enable |
220
+ | `GITLAB_API_URL` | ✅ | GitLab API base URL |
221
+ | `GITLAB_OAUTH_APP_ID` | ✅ | GitLab OAuth Application ID |
222
+ | `MCP_SERVER_URL` | ✅ | Public HTTPS URL of this MCP server |
223
+ | `STREAMABLE_HTTP` | ✅ | Must be `true` |
224
+ | `GITLAB_OAUTH_CALLBACK_PROXY` | optional | Set to `true` to use the MCP server's fixed `/callback` URL |
225
+ | `GITLAB_OAUTH_SCOPES` | optional | Comma-separated scopes (default: `api,read_api,read_user`) |
217
226
  | `GITLAB_OAUTH_ALLOWED_GROUPS` | optional | Comma-separated group full paths — only members (and subgroup members) may obtain a token (replaces deprecated `GITLAB_ALLOWED_GROUPS`) |
218
227
 
219
228
  When `STREAMABLE_HTTP=true`, server-side `GITLAB_PERSONAL_ACCESS_TOKEN` or `GITLAB_JOB_TOKEN` require `REMOTE_AUTHORIZATION=true` or `GITLAB_MCP_OAUTH=true`.
@@ -266,14 +275,14 @@ the token to GitLab on behalf of the caller.
266
275
 
267
276
  **Header priority**: `Private-Token` > `JOB-TOKEN` > `Authorization: Bearer`
268
277
 
269
- | Environment Variable | Required | Description |
270
- | ------------------------ | -------- | ---------------------------------------------------------- |
271
- | `REMOTE_AUTHORIZATION` | ✅ | Set to `true` to enable |
272
- | `STREAMABLE_HTTP` | ✅ | Must be `true` |
273
- | `ENABLE_DYNAMIC_API_URL` | optional | Allow per-request GitLab URL via `X-GitLab-API-URL` header |
274
- | `GITLAB_ALLOWED_HOSTS` | optional | Comma-separated allowed `X-GitLab-API-URL` hosts; `GITLAB_API_URL` hosts are always allowed |
275
- | `GITLAB_ALLOW_UNAUTHENTICATED_TOOL_DISCOVERY` | optional | Allow unauthenticated `initialize`, `notifications/initialized`, and `tools/list` only (tool calls still require auth) |
276
- | `MCP_TRUST_PROXY` | optional | Trust `Forwarded` / `X-Forwarded-*` headers behind a reverse proxy (download URLs, Express `req.ip`, OAuth rate limits) |
278
+ | Environment Variable | Required | Description |
279
+ | --------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------- |
280
+ | `REMOTE_AUTHORIZATION` | ✅ | Set to `true` to enable |
281
+ | `STREAMABLE_HTTP` | ✅ | Must be `true` |
282
+ | `ENABLE_DYNAMIC_API_URL` | optional | Allow per-request GitLab URL via `X-GitLab-API-URL` header |
283
+ | `GITLAB_ALLOWED_HOSTS` | optional | Comma-separated allowed `X-GitLab-API-URL` hosts; `GITLAB_API_URL` hosts are always allowed |
284
+ | `GITLAB_ALLOW_UNAUTHENTICATED_TOOL_DISCOVERY` | optional | Allow unauthenticated `initialize`, `notifications/initialized`, and `tools/list` only (tool calls still require auth) |
285
+ | `MCP_TRUST_PROXY` | optional | Trust `Forwarded` / `X-Forwarded-*` headers behind a reverse proxy (download URLs, Express `req.ip`, OAuth rate limits) |
277
286
 
278
287
  `GITLAB_ALLOW_UNAUTHENTICATED_TOOL_DISCOVERY=true` is intended for MCP gateways
279
288
  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.29`。
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
  }
@@ -89,12 +94,19 @@
89
94
  - `--token` - GitLab Personal Access Token(替代 `GITLAB_PERSONAL_ACCESS_TOKEN`)
90
95
  - `--api-url` - GitLab API URL(替代 `GITLAB_API_URL`)
91
96
  - `--read-only=true` - 启用只读模式(替代 `GITLAB_READ_ONLY_MODE`)
92
- - `--use-wiki=true` - 启用 Wiki API(替代 `USE_GITLAB_WIKI`)
93
- - `--use-milestone=true` - 启用里程碑 API(替代 `USE_MILESTONE`)
94
- - `--use-pipeline=true` - 启用流水线 API(替代 `USE_PIPELINE`)
97
+ - `--use-wiki=true` - 启用 Wiki API(替代 `USE_GITLAB_WIKI`,旧版 — 推荐 `GITLAB_TOOLSETS=wiki`)
98
+ - `--use-milestone=true` - 启用里程碑 API(替代 `USE_MILESTONE`,旧版 — 推荐 `GITLAB_TOOLSETS=milestones`)
99
+ - `--use-pipeline=true` - 启用流水线 API(替代 `USE_PIPELINE`,旧版 — 推荐 `GITLAB_TOOLSETS=pipelines`)
95
100
 
96
101
  CLI 参数优先于环境变量。
97
102
 
103
+ > **细粒度工具过滤:**除了全开/全关的 `GITLAB_READ_ONLY_MODE`,还可以用
104
+ > `GITLAB_TOOLSETS=<group,…>` 启用工具分组,用 `GITLAB_TOOLS=<tool,…>` 白名单启用单个工具
105
+ > (例如:只读分组 + 少数几个写工具),用 `GITLAB_DENIED_TOOLS_REGEX` 按正则屏蔽工具。
106
+ > 旧版 `USE_GITLAB_WIKI` / `USE_MILESTONE` / `USE_PIPELINE` 标志仅为向后兼容保留。
107
+ > 参见 [Tools Reference](./docs/tools/index.md#feature-toggles) 和
108
+ > [Environment Variables](./docs/configuration/environment-variables.md)。
109
+
98
110
  #### SSE
99
111
 
100
112
  ```shell
@@ -103,9 +115,7 @@ docker run -i --rm \
103
115
  -e GITLAB_PERSONAL_ACCESS_TOKEN=your_gitlab_token \
104
116
  -e GITLAB_API_URL="https://gitlab.com/api/v4" \
105
117
  -e GITLAB_READ_ONLY_MODE=true \
106
- -e USE_GITLAB_WIKI=true \
107
- -e USE_MILESTONE=true \
108
- -e USE_PIPELINE=true \
118
+ -e GITLAB_TOOLSETS=wiki,milestones,pipelines \
109
119
  -e SSE=true \
110
120
  -p 3333:3002 \
111
121
  zereight050/gitlab-mcp
@@ -130,9 +140,7 @@ docker run -i --rm \
130
140
  -e GITLAB_PERSONAL_ACCESS_TOKEN=your_gitlab_token \
131
141
  -e GITLAB_API_URL="https://gitlab.com/api/v4" \
132
142
  -e GITLAB_READ_ONLY_MODE=true \
133
- -e USE_GITLAB_WIKI=true \
134
- -e USE_MILESTONE=true \
135
- -e USE_PIPELINE=true \
143
+ -e GITLAB_TOOLSETS=wiki,milestones,pipelines \
136
144
  -e STREAMABLE_HTTP=true \
137
145
  -p 3333:3002 \
138
146
  zereight050/gitlab-mcp
@@ -178,16 +186,16 @@ OpenCode、MCPJam、Claude.ai 等远程 MCP 客户端可能会在授权时发送
178
186
  2. 预先注册的 GitLab OAuth 应用,包含 `api` 或 `read_api` scopes
179
187
  — 前往 `Admin area` → `Applications`,将 Redirect URI 设置为 `{MCP_SERVER_URL}/callback`
180
188
 
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`)|
189
+ | 环境变量 | 必需 | 说明 |
190
+ | ----------------------------- | ---- | ----------------------------------------------------------------------------------------------------------- |
191
+ | `GITLAB_MCP_OAUTH` | 是 | 设置为 `true` 以启用 |
192
+ | `GITLAB_API_URL` | 是 | GitLab API base URL |
193
+ | `GITLAB_OAUTH_APP_ID` | 是 | GitLab OAuth Application ID |
194
+ | `MCP_SERVER_URL` | 是 | 此 MCP 服务器的公开 HTTPS URL |
195
+ | `STREAMABLE_HTTP` | 是 | 必须为 `true` |
196
+ | `GITLAB_OAUTH_CALLBACK_PROXY` | 可选 | 设置为 `true` 时使用 MCP 服务器固定的 `/callback` URL |
197
+ | `GITLAB_OAUTH_SCOPES` | 可选 | 逗号分隔的 scope(默认:`api,read_api,read_user`) |
198
+ | `GITLAB_OAUTH_ALLOWED_GROUPS` | 可选 | 逗号分隔的 GitLab 群组完整路径 — 仅该群组及其子群组的成员可获取令牌(替代已废弃的 `GITLAB_ALLOWED_GROUPS`) |
191
199
 
192
200
  > **排查 `Unregistered redirect_uri`**
193
201
  >
@@ -238,7 +246,7 @@ MCP 客户端配置:
238
246
  | `REMOTE_AUTHORIZATION` | 是 | 设置为 `true` 以启用 |
239
247
  | `STREAMABLE_HTTP` | 是 | 必须为 `true` |
240
248
  | `ENABLE_DYNAMIC_API_URL` | 可选 | 允许按请求通过 `X-GitLab-API-URL` 请求头指定 GitLab URL |
241
- | `GITLAB_ALLOWED_HOSTS` | 可选 | 逗号分隔的允许主机;`GITLAB_API_URL` 中的主机始终允许 |
249
+ | `GITLAB_ALLOWED_HOSTS` | 可选 | 逗号分隔的允许主机;`GITLAB_API_URL` 中的主机始终允许 |
242
250
 
243
251
  **示例请求头:**
244
252
 
package/build/index.js CHANGED
@@ -134,6 +134,7 @@ import { normalizeGitLabApiUrl } from "./utils/url.js";
134
134
  import { estimateMergeCommitCount, filterDiffsByPatterns, summarizeWebhookEvents, } from "./utils/helpers.js";
135
135
  import { graphqlQueryContainsWriteOperation } from "./utils/graphql-query.js";
136
136
  import { resolveNestedWikiUpdateTitle } from "./utils/wiki-title.js";
137
+ import { redactSensitiveGitLabFields } from "./utils/redact-sensitive.js";
137
138
  import { cleanMutuallyExclusiveIdUsernameOptions, LIST_MERGE_REQUESTS_ID_USERNAME_PAIRS, sanitizeToolArguments, } from "./utils/tool-args.js";
138
139
  import { parseSearchReplaceBlocks, applySearchReplace, applyUnifiedDiff, } from "./utils/patch-helper.js";
139
140
  import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js";
@@ -1028,7 +1029,13 @@ function parseAllowedGitLabApiUrls(value) {
1028
1029
  .filter((entry) => Boolean(entry));
1029
1030
  }
1030
1031
  function encodeGitLabPathSegment(value) {
1031
- return encodeURIComponent(decodeURIComponent(value));
1032
+ const segment = String(value);
1033
+ try {
1034
+ return encodeURIComponent(decodeURIComponent(segment));
1035
+ }
1036
+ catch {
1037
+ return encodeURIComponent(segment);
1038
+ }
1032
1039
  }
1033
1040
  function encodeGitLabPath(value) {
1034
1041
  return value.split("/").map(encodeGitLabPathSegment).join("/");
@@ -1418,7 +1425,7 @@ async function listMergeRequests(projectId, options = {}) {
1418
1425
  */
1419
1426
  async function getIssue(projectId, issueIid) {
1420
1427
  projectId = decodeURIComponent(projectId); // Decode project ID
1421
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${issueIid}`);
1428
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${encodeGitLabPathSegment(issueIid)}`);
1422
1429
  const response = await fetch(url.toString(), {
1423
1430
  ...getFetchConfig(),
1424
1431
  });
@@ -1437,7 +1444,7 @@ async function getIssue(projectId, issueIid) {
1437
1444
  */
1438
1445
  async function updateIssue(projectId, issueIid, options) {
1439
1446
  projectId = decodeURIComponent(projectId); // Decode project ID
1440
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${issueIid}`);
1447
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${encodeGitLabPathSegment(issueIid)}`);
1441
1448
  // Convert labels array to comma-separated string if present
1442
1449
  const body = { ...options };
1443
1450
  if (body.labels && Array.isArray(body.labels)) {
@@ -1462,7 +1469,7 @@ async function updateIssue(projectId, issueIid, options) {
1462
1469
  */
1463
1470
  async function deleteIssue(projectId, issueIid) {
1464
1471
  projectId = decodeURIComponent(projectId); // Decode project ID
1465
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${issueIid}`);
1472
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${encodeGitLabPathSegment(issueIid)}`);
1466
1473
  const response = await fetch(url.toString(), {
1467
1474
  ...getFetchConfig(),
1468
1475
  method: "DELETE",
@@ -2806,7 +2813,7 @@ async function updateWorkItem(projectId, iid, options) {
2806
2813
  */
2807
2814
  async function listIssueLinks(projectId, issueIid) {
2808
2815
  projectId = decodeURIComponent(projectId); // Decode project ID
2809
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${issueIid}/links`);
2816
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${encodeGitLabPathSegment(issueIid)}/links`);
2810
2817
  const response = await fetch(url.toString(), {
2811
2818
  ...getFetchConfig(),
2812
2819
  });
@@ -2825,7 +2832,7 @@ async function listIssueLinks(projectId, issueIid) {
2825
2832
  */
2826
2833
  async function getIssueLink(projectId, issueIid, issueLinkId) {
2827
2834
  projectId = decodeURIComponent(projectId); // Decode project ID
2828
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${issueIid}/links/${issueLinkId}`);
2835
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${encodeGitLabPathSegment(issueIid)}/links/${encodeGitLabPathSegment(issueLinkId)}`);
2829
2836
  const response = await fetch(url.toString(), {
2830
2837
  ...getFetchConfig(),
2831
2838
  });
@@ -2847,7 +2854,7 @@ async function getIssueLink(projectId, issueIid, issueLinkId) {
2847
2854
  async function createIssueLink(projectId, issueIid, targetProjectId, targetIssueIid, linkType = "relates_to") {
2848
2855
  projectId = decodeURIComponent(projectId); // Decode project ID
2849
2856
  targetProjectId = decodeURIComponent(targetProjectId); // Decode target project ID as well
2850
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${issueIid}/links`);
2857
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${encodeGitLabPathSegment(issueIid)}/links`);
2851
2858
  const response = await fetch(url.toString(), {
2852
2859
  ...getFetchConfig(),
2853
2860
  method: "POST",
@@ -2872,7 +2879,7 @@ async function createIssueLink(projectId, issueIid, targetProjectId, targetIssue
2872
2879
  */
2873
2880
  async function deleteIssueLink(projectId, issueIid, issueLinkId) {
2874
2881
  projectId = decodeURIComponent(projectId); // Decode project ID
2875
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${issueIid}/links/${issueLinkId}`);
2882
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${encodeGitLabPathSegment(issueIid)}/links/${encodeGitLabPathSegment(issueLinkId)}`);
2876
2883
  const response = await fetch(url.toString(), {
2877
2884
  ...getFetchConfig(),
2878
2885
  method: "DELETE",
@@ -2995,7 +3002,7 @@ async function listIssueDiscussions(projectId, issueIid, options = {}) {
2995
3002
  }
2996
3003
  async function deleteMergeRequestDiscussionNote(projectId, mergeRequestIid, discussionId, noteId) {
2997
3004
  projectId = decodeURIComponent(projectId); // Decode project ID
2998
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/discussions/${discussionId}/notes/${noteId}`);
3005
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/discussions/${encodeGitLabPathSegment(discussionId)}/notes/${encodeGitLabPathSegment(noteId)}`);
2999
3006
  const response = await fetch(url.toString(), {
3000
3007
  ...getFetchConfig(),
3001
3008
  method: "DELETE",
@@ -3019,7 +3026,7 @@ async function deleteMergeRequestDiscussionNote(projectId, mergeRequestIid, disc
3019
3026
  */
3020
3027
  async function updateMergeRequestDiscussionNote(projectId, mergeRequestIid, discussionId, noteId, body, resolved) {
3021
3028
  projectId = decodeURIComponent(projectId); // Decode project ID
3022
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/discussions/${discussionId}/notes/${noteId}`);
3029
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/discussions/${encodeGitLabPathSegment(discussionId)}/notes/${encodeGitLabPathSegment(noteId)}`);
3023
3030
  // Only one of body or resolved can be sent according to GitLab API
3024
3031
  const payload = {};
3025
3032
  if (body !== undefined) {
@@ -3065,7 +3072,7 @@ async function updateMergeRequestDiscussionNote(projectId, mergeRequestIid, disc
3065
3072
  */
3066
3073
  async function updateIssueNote(projectId, issueIid, discussionId, noteId, body, resolved) {
3067
3074
  projectId = decodeURIComponent(projectId); // Decode project ID
3068
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${issueIid}/discussions/${discussionId}/notes/${noteId}`);
3075
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${encodeGitLabPathSegment(issueIid)}/discussions/${encodeGitLabPathSegment(discussionId)}/notes/${encodeGitLabPathSegment(noteId)}`);
3069
3076
  // Only one of body or resolved can be sent according to GitLab API
3070
3077
  const payload = {};
3071
3078
  if (body !== undefined) {
@@ -3094,8 +3101,8 @@ async function updateIssueNote(projectId, issueIid, discussionId, noteId, body,
3094
3101
  */
3095
3102
  async function createIssueNote(projectId, issueIid, discussionId, body, createdAt) {
3096
3103
  projectId = decodeURIComponent(projectId); // Decode project ID
3097
- const basePath = `${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${issueIid}`;
3098
- const url = new URL(discussionId ? `${basePath}/discussions/${discussionId}/notes` : `${basePath}/notes`);
3104
+ const basePath = `${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${encodeGitLabPathSegment(issueIid)}`;
3105
+ const url = new URL(discussionId ? `${basePath}/discussions/${encodeGitLabPathSegment(discussionId)}/notes` : `${basePath}/notes`);
3099
3106
  const payload = { body };
3100
3107
  if (createdAt) {
3101
3108
  payload.created_at = createdAt;
@@ -3122,7 +3129,7 @@ async function createIssueNote(projectId, issueIid, discussionId, body, createdA
3122
3129
  */
3123
3130
  async function createMergeRequestDiscussionNote(projectId, mergeRequestIid, discussionId, body, createdAt) {
3124
3131
  projectId = decodeURIComponent(projectId); // Decode project ID
3125
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/discussions/${discussionId}/notes`);
3132
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/discussions/${encodeGitLabPathSegment(discussionId)}/notes`);
3126
3133
  const payload = { body };
3127
3134
  if (createdAt) {
3128
3135
  payload.created_at = createdAt;
@@ -3138,7 +3145,7 @@ async function createMergeRequestDiscussionNote(projectId, mergeRequestIid, disc
3138
3145
  }
3139
3146
  async function createMergeRequestNote(projectId, mergeRequestIid, body) {
3140
3147
  projectId = decodeURIComponent(projectId); // Decode project ID
3141
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/notes`);
3148
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/notes`);
3142
3149
  const payload = {
3143
3150
  id: projectId,
3144
3151
  merge_request_iid: mergeRequestIid,
@@ -3155,7 +3162,7 @@ async function createMergeRequestNote(projectId, mergeRequestIid, body) {
3155
3162
  }
3156
3163
  async function deleteMergeRequestNote(projectId, mergeRequestIid, noteId) {
3157
3164
  projectId = decodeURIComponent(projectId); // Decode project ID
3158
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/notes/${noteId}`);
3165
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/notes/${encodeGitLabPathSegment(noteId)}`);
3159
3166
  const response = await fetch(url.toString(), {
3160
3167
  ...getFetchConfig(),
3161
3168
  method: "DELETE",
@@ -3203,7 +3210,7 @@ async function deleteRestAwardEmoji(path) {
3203
3210
  }
3204
3211
  async function getMergeRequestNote(projectId, mergeRequestIid, noteId) {
3205
3212
  projectId = decodeURIComponent(projectId); // Decode project ID
3206
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/notes/${noteId}`);
3213
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/notes/${encodeGitLabPathSegment(noteId)}`);
3207
3214
  const response = await fetch(url.toString(), {
3208
3215
  ...getFetchConfig(),
3209
3216
  method: "GET",
@@ -3214,7 +3221,7 @@ async function getMergeRequestNote(projectId, mergeRequestIid, noteId) {
3214
3221
  }
3215
3222
  async function getMergeRequestNotes(projectId, mergeRequestIid, sort, order_by, per_page, page) {
3216
3223
  projectId = decodeURIComponent(projectId); // Decode project ID
3217
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/notes`);
3224
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/notes`);
3218
3225
  if (sort) {
3219
3226
  url.searchParams.append("sort", sort);
3220
3227
  }
@@ -3237,7 +3244,7 @@ async function getMergeRequestNotes(projectId, mergeRequestIid, sort, order_by,
3237
3244
  }
3238
3245
  async function updateMergeRequestNote(projectId, mergeRequestIid, noteId, body) {
3239
3246
  projectId = decodeURIComponent(projectId); // Decode project ID
3240
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/notes/${noteId}`);
3247
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/notes/${encodeGitLabPathSegment(noteId)}`);
3241
3248
  const payload = {
3242
3249
  id: projectId,
3243
3250
  merge_request_iid: mergeRequestIid,
@@ -3415,8 +3422,7 @@ async function searchBlobs(params) {
3415
3422
  basePath = `${getEffectiveApiUrl()}/projects/${projectId}/search`;
3416
3423
  }
3417
3424
  else if (params.group_id) {
3418
- const groupId = encodeURIComponent(decodeURIComponent(params.group_id));
3419
- basePath = `${getEffectiveApiUrl()}/groups/${groupId}/search`;
3425
+ basePath = `${getEffectiveApiUrl()}/groups/${encodeGitLabPathSegment(params.group_id)}/search`;
3420
3426
  }
3421
3427
  else {
3422
3428
  basePath = `${getEffectiveApiUrl()}/search`;
@@ -3490,7 +3496,7 @@ async function getMergeRequest(projectId, mergeRequestIid, branchName) {
3490
3496
  projectId = decodeURIComponent(projectId); // Decode project ID
3491
3497
  let url;
3492
3498
  if (mergeRequestIid) {
3493
- url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}`);
3499
+ url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}`);
3494
3500
  url.searchParams.append("include_diverged_commits_count", "true");
3495
3501
  }
3496
3502
  else if (branchName) {
@@ -3512,7 +3518,7 @@ async function getMergeRequest(projectId, mergeRequestIid, branchName) {
3512
3518
  return GitLabMergeRequestSchema.parse(data);
3513
3519
  }
3514
3520
  async function getMergeRequestSourceCommitCount(projectId, mergeRequestIid) {
3515
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/commits`);
3521
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/commits`);
3516
3522
  url.searchParams.append("per_page", "100");
3517
3523
  let totalCount = 0;
3518
3524
  let page = 1;
@@ -3540,7 +3546,7 @@ async function getMergeRequestSourceCommitCount(projectId, mergeRequestIid) {
3540
3546
  }
3541
3547
  async function listMergeRequestPipelines(projectId, mergeRequestIid, options = {}) {
3542
3548
  projectId = decodeURIComponent(projectId);
3543
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/pipelines`);
3549
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/pipelines`);
3544
3550
  Object.entries(options).forEach(([key, value]) => {
3545
3551
  if (value !== undefined) {
3546
3552
  url.searchParams.append(key, value.toString());
@@ -3741,7 +3747,7 @@ async function getMergeRequestDiffs(projectId, mergeRequestIid, branchName, view
3741
3747
  const mergeRequest = await getMergeRequest(projectId, undefined, branchName);
3742
3748
  mergeRequestIid = mergeRequest.iid;
3743
3749
  }
3744
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/changes`);
3750
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/changes`);
3745
3751
  if (view) {
3746
3752
  url.searchParams.append("view", view);
3747
3753
  }
@@ -3771,7 +3777,7 @@ async function listMergeRequestDiffs(projectId, mergeRequestIid, branchName, pag
3771
3777
  const mergeRequest = await getMergeRequest(projectId, undefined, branchName);
3772
3778
  mergeRequestIid = mergeRequest.iid;
3773
3779
  }
3774
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/diffs`);
3780
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/diffs`);
3775
3781
  if (page) {
3776
3782
  url.searchParams.append("page", page.toString());
3777
3783
  }
@@ -3807,7 +3813,7 @@ async function listMergeRequestChangedFiles(projectId, mergeRequestIid, branchNa
3807
3813
  const mergeRequest = await getMergeRequest(projectId, undefined, branchName);
3808
3814
  mergeRequestIid = mergeRequest.iid;
3809
3815
  }
3810
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/changes`);
3816
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/changes`);
3811
3817
  const response = await fetch(url.toString(), { ...getFetchConfig() });
3812
3818
  await handleGitLabError(response);
3813
3819
  const data = (await response.json());
@@ -3848,7 +3854,7 @@ async function getMergeRequestFileDiff(projectId, filePaths, mergeRequestIid, br
3848
3854
  let page = 1;
3849
3855
  const perPage = 20;
3850
3856
  while (remaining.size > 0) {
3851
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/diffs`);
3857
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/diffs`);
3852
3858
  url.searchParams.append("page", page.toString());
3853
3859
  url.searchParams.append("per_page", perPage.toString());
3854
3860
  if (unidiff) {
@@ -3926,7 +3932,7 @@ async function updateMergeRequest(projectId, options, mergeRequestIid, branchNam
3926
3932
  const mergeRequest = await getMergeRequest(projectId, undefined, branchName);
3927
3933
  mergeRequestIid = mergeRequest.iid;
3928
3934
  }
3929
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}`);
3935
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}`);
3930
3936
  const response = await fetch(url.toString(), {
3931
3937
  ...getFetchConfig(),
3932
3938
  method: "PUT",
@@ -3946,7 +3952,7 @@ async function updateMergeRequest(projectId, options, mergeRequestIid, branchNam
3946
3952
  */
3947
3953
  async function mergeMergeRequest(projectId, options, mergeRequestIid) {
3948
3954
  projectId = decodeURIComponent(projectId); // Decode project ID
3949
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/merge`);
3955
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/merge`);
3950
3956
  const response = await fetch(url.toString(), {
3951
3957
  ...getFetchConfig(),
3952
3958
  method: "PUT",
@@ -3966,7 +3972,7 @@ async function mergeMergeRequest(projectId, options, mergeRequestIid) {
3966
3972
  */
3967
3973
  async function approveMergeRequest(projectId, mergeRequestIid, sha, approvalPassword) {
3968
3974
  projectId = decodeURIComponent(projectId);
3969
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/approve`);
3975
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/approve`);
3970
3976
  const body = {};
3971
3977
  if (sha) {
3972
3978
  body.sha = sha;
@@ -3991,7 +3997,7 @@ async function approveMergeRequest(projectId, mergeRequestIid, sha, approvalPass
3991
3997
  */
3992
3998
  async function unapproveMergeRequest(projectId, mergeRequestIid) {
3993
3999
  projectId = decodeURIComponent(projectId);
3994
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/unapprove`);
4000
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/unapprove`);
3995
4001
  const response = await fetch(url.toString(), {
3996
4002
  ...getFetchConfig(),
3997
4003
  method: "POST",
@@ -4009,7 +4015,7 @@ async function unapproveMergeRequest(projectId, mergeRequestIid) {
4009
4015
  */
4010
4016
  async function getMergeRequestApprovalState(projectId, mergeRequestIid) {
4011
4017
  projectId = decodeURIComponent(projectId);
4012
- const approvalStateUrl = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/approval_state`);
4018
+ const approvalStateUrl = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/approval_state`);
4013
4019
  const approvalStateResponse = await fetch(approvalStateUrl.toString(), {
4014
4020
  ...getFetchConfig(),
4015
4021
  method: "GET",
@@ -4037,7 +4043,7 @@ async function getMergeRequestApprovalState(projectId, mergeRequestIid) {
4037
4043
  */
4038
4044
  async function getMergeRequestConflicts(projectId, mergeRequestIid) {
4039
4045
  projectId = decodeURIComponent(projectId);
4040
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/conflicts`);
4046
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/conflicts`);
4041
4047
  const response = await fetch(url.toString(), {
4042
4048
  ...getFetchConfig(),
4043
4049
  method: "GET",
@@ -4046,7 +4052,7 @@ async function getMergeRequestConflicts(projectId, mergeRequestIid) {
4046
4052
  return (await response.json());
4047
4053
  }
4048
4054
  async function getMergeRequestApprovalsFallback(projectId, mergeRequestIid) {
4049
- const approvalsUrl = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/approvals`);
4055
+ const approvalsUrl = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/approvals`);
4050
4056
  const approvalsResponse = await fetch(approvalsUrl.toString(), {
4051
4057
  ...getFetchConfig(),
4052
4058
  method: "GET",
@@ -4136,7 +4142,7 @@ async function getDraftNote(project_id, merge_request_iid, draft_note_id) {
4136
4142
  }
4137
4143
  async function listDraftNotes(projectId, mergeRequestIid) {
4138
4144
  projectId = decodeURIComponent(projectId);
4139
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/draft_notes`);
4145
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/draft_notes`);
4140
4146
  const response = await fetch(url.toString(), {
4141
4147
  ...getFetchConfig(),
4142
4148
  method: "GET",
@@ -4160,7 +4166,7 @@ async function listDraftNotes(projectId, mergeRequestIid) {
4160
4166
  */
4161
4167
  async function createDraftNote(projectId, mergeRequestIid, body, inReplyToDiscussionId, position, resolveDiscussion) {
4162
4168
  projectId = decodeURIComponent(projectId);
4163
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/draft_notes`);
4169
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/draft_notes`);
4164
4170
  const requestBody = { note: body };
4165
4171
  if (inReplyToDiscussionId) {
4166
4172
  requestBody.in_reply_to_discussion_id = inReplyToDiscussionId;
@@ -4195,7 +4201,7 @@ async function createDraftNote(projectId, mergeRequestIid, body, inReplyToDiscus
4195
4201
  */
4196
4202
  async function updateDraftNote(projectId, mergeRequestIid, draftNoteId, body, position, resolveDiscussion) {
4197
4203
  projectId = decodeURIComponent(projectId);
4198
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/draft_notes/${draftNoteId}`);
4204
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/draft_notes/${encodeGitLabPathSegment(draftNoteId)}`);
4199
4205
  const requestBody = {};
4200
4206
  if (body !== undefined) {
4201
4207
  requestBody.note = body;
@@ -4227,7 +4233,7 @@ async function updateDraftNote(projectId, mergeRequestIid, draftNoteId, body, po
4227
4233
  */
4228
4234
  async function deleteDraftNote(projectId, mergeRequestIid, draftNoteId) {
4229
4235
  projectId = decodeURIComponent(projectId);
4230
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/draft_notes/${draftNoteId}`);
4236
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/draft_notes/${encodeGitLabPathSegment(draftNoteId)}`);
4231
4237
  const response = await fetch(url.toString(), {
4232
4238
  ...getFetchConfig(),
4233
4239
  method: "DELETE",
@@ -4246,7 +4252,7 @@ async function deleteDraftNote(projectId, mergeRequestIid, draftNoteId) {
4246
4252
  */
4247
4253
  async function publishDraftNote(projectId, mergeRequestIid, draftNoteId) {
4248
4254
  projectId = decodeURIComponent(projectId);
4249
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/draft_notes/${draftNoteId}/publish`);
4255
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/draft_notes/${encodeGitLabPathSegment(draftNoteId)}/publish`);
4250
4256
  const response = await fetch(url.toString(), {
4251
4257
  ...getFetchConfig(),
4252
4258
  method: "PUT",
@@ -4298,7 +4304,7 @@ async function publishDraftNote(projectId, mergeRequestIid, draftNoteId) {
4298
4304
  */
4299
4305
  async function bulkPublishDraftNotes(projectId, mergeRequestIid) {
4300
4306
  projectId = decodeURIComponent(projectId);
4301
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/draft_notes/bulk_publish`);
4307
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/draft_notes/bulk_publish`);
4302
4308
  const response = await fetch(url.toString(), {
4303
4309
  ...getFetchConfig(),
4304
4310
  method: "POST", // Changed from PUT to POST
@@ -4327,7 +4333,7 @@ async function bulkPublishDraftNotes(projectId, mergeRequestIid) {
4327
4333
  }
4328
4334
  async function resolveMergeRequestThread(projectId, mergeRequestIid, discussionId, resolved) {
4329
4335
  projectId = decodeURIComponent(projectId);
4330
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/discussions/${discussionId}`);
4336
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/discussions/${encodeGitLabPathSegment(discussionId)}`);
4331
4337
  if (resolved !== undefined) {
4332
4338
  url.searchParams.append("resolved", resolved ? "true" : "false");
4333
4339
  }
@@ -4359,7 +4365,7 @@ async function resolveMergeRequestThread(projectId, mergeRequestIid, discussionI
4359
4365
  */
4360
4366
  async function createMergeRequestThread(projectId, mergeRequestIid, body, position, createdAt) {
4361
4367
  projectId = decodeURIComponent(projectId); // Decode project ID
4362
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/discussions`);
4368
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/discussions`);
4363
4369
  const payload = { body };
4364
4370
  if (position) {
4365
4371
  payload.position = position;
@@ -4386,7 +4392,7 @@ async function createMergeRequestThread(projectId, mergeRequestIid, body, positi
4386
4392
  */
4387
4393
  async function listMergeRequestVersions(projectId, mergeRequestIid) {
4388
4394
  projectId = decodeURIComponent(projectId); // Decode project ID
4389
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/versions`);
4395
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/versions`);
4390
4396
  const response = await fetch(url.toString(), {
4391
4397
  ...getFetchConfig(),
4392
4398
  });
@@ -4405,7 +4411,7 @@ async function listMergeRequestVersions(projectId, mergeRequestIid) {
4405
4411
  */
4406
4412
  async function getMergeRequestVersion(projectId, mergeRequestIid, versionId, unidiff) {
4407
4413
  projectId = decodeURIComponent(projectId); // Decode project ID
4408
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/versions/${versionId}`);
4414
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/versions/${encodeGitLabPathSegment(versionId)}`);
4409
4415
  if (unidiff !== undefined) {
4410
4416
  url.searchParams.append("unidiff", String(unidiff));
4411
4417
  }
@@ -4696,6 +4702,8 @@ async function listWikiPages(projectId, options = {}) {
4696
4702
  url.searchParams.append("per_page", options.per_page.toString());
4697
4703
  if (options.with_content)
4698
4704
  url.searchParams.append("with_content", options.with_content.toString());
4705
+ if (options.render_html)
4706
+ url.searchParams.append("render_html", options.render_html.toString());
4699
4707
  const response = await fetch(url.toString(), {
4700
4708
  ...getFetchConfig(),
4701
4709
  });
@@ -4706,9 +4714,12 @@ async function listWikiPages(projectId, options = {}) {
4706
4714
  /**
4707
4715
  * Get a specific wiki page
4708
4716
  */
4709
- async function getWikiPage(projectId, slug) {
4717
+ async function getWikiPage(projectId, slug, renderHtml) {
4710
4718
  projectId = decodeURIComponent(projectId); // Decode project ID
4711
- const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/wikis/${encodeURIComponent(slug)}`, { ...getFetchConfig() });
4719
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/wikis/${encodeURIComponent(slug)}`);
4720
+ if (renderHtml)
4721
+ url.searchParams.append("render_html", renderHtml.toString());
4722
+ const response = await fetch(url.toString(), { ...getFetchConfig() });
4712
4723
  await handleGitLabError(response);
4713
4724
  const data = await response.json();
4714
4725
  return GitLabWikiPageSchema.parse(data);
@@ -4781,6 +4792,8 @@ async function listGroupWikiPages(groupId, options = {}) {
4781
4792
  url.searchParams.append("per_page", options.per_page.toString());
4782
4793
  if (options.with_content)
4783
4794
  url.searchParams.append("with_content", options.with_content.toString());
4795
+ if (options.render_html)
4796
+ url.searchParams.append("render_html", options.render_html.toString());
4784
4797
  const response = await fetch(url.toString(), {
4785
4798
  ...getFetchConfig(),
4786
4799
  });
@@ -4791,9 +4804,12 @@ async function listGroupWikiPages(groupId, options = {}) {
4791
4804
  /**
4792
4805
  * Get a specific group wiki page
4793
4806
  */
4794
- async function getGroupWikiPage(groupId, slug) {
4807
+ async function getGroupWikiPage(groupId, slug, renderHtml) {
4795
4808
  groupId = decodeURIComponent(groupId); // Decode group ID
4796
- const response = await fetch(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(groupId)}/wikis/${encodeURIComponent(slug)}`, { ...getFetchConfig() });
4809
+ const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(groupId)}/wikis/${encodeURIComponent(slug)}`);
4810
+ if (renderHtml)
4811
+ url.searchParams.append("render_html", renderHtml.toString());
4812
+ const response = await fetch(url.toString(), { ...getFetchConfig() });
4797
4813
  await handleGitLabError(response);
4798
4814
  const data = await response.json();
4799
4815
  return GitLabWikiPageSchema.parse(data);
@@ -7168,9 +7184,10 @@ async function handleToolCall(params) {
7168
7184
  });
7169
7185
  await handleGitLabError(response);
7170
7186
  const data = await response.json();
7171
- // Return raw data without parsing through our schema to avoid type mismatches in tests
7187
+ // Return raw data without parsing through our schema to avoid type mismatches in tests,
7188
+ // but strip credential fields (e.g. runners_token) so they never reach the AI context.
7172
7189
  return {
7173
- content: [{ type: "text", text: JSON.stringify(data) }],
7190
+ content: [{ type: "text", text: JSON.stringify(redactSensitiveGitLabFields(data)) }],
7174
7191
  };
7175
7192
  }
7176
7193
  case "list_projects": {
@@ -7192,7 +7209,9 @@ async function handleToolCall(params) {
7192
7209
  await handleGitLabError(response);
7193
7210
  const data = await response.json();
7194
7211
  return {
7195
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
7212
+ content: [
7213
+ { type: "text", text: JSON.stringify(redactSensitiveGitLabFields(data), null, 2) },
7214
+ ],
7196
7215
  };
7197
7216
  }
7198
7217
  case "list_project_members": {
@@ -7647,19 +7666,20 @@ async function handleToolCall(params) {
7647
7666
  };
7648
7667
  }
7649
7668
  case "list_wiki_pages": {
7650
- const { project_id, page, per_page, with_content } = ListWikiPagesSchema.parse(params.arguments);
7669
+ const { project_id, page, per_page, with_content, render_html } = ListWikiPagesSchema.parse(params.arguments);
7651
7670
  const wikiPages = await listWikiPages(project_id, {
7652
7671
  page,
7653
7672
  per_page,
7654
7673
  with_content,
7674
+ render_html,
7655
7675
  });
7656
7676
  return {
7657
7677
  content: [{ type: "text", text: JSON.stringify(wikiPages) }],
7658
7678
  };
7659
7679
  }
7660
7680
  case "get_wiki_page": {
7661
- const { project_id, slug } = GetWikiPageSchema.parse(params.arguments);
7662
- const wikiPage = await getWikiPage(project_id, slug);
7681
+ const { project_id, slug, render_html } = GetWikiPageSchema.parse(params.arguments);
7682
+ const wikiPage = await getWikiPage(project_id, slug, render_html);
7663
7683
  return {
7664
7684
  content: [{ type: "text", text: JSON.stringify(wikiPage) }],
7665
7685
  };
@@ -7694,19 +7714,20 @@ async function handleToolCall(params) {
7694
7714
  };
7695
7715
  }
7696
7716
  case "list_group_wiki_pages": {
7697
- const { group_id, page, per_page, with_content } = ListGroupWikiPagesSchema.parse(params.arguments);
7717
+ const { group_id, page, per_page, with_content, render_html } = ListGroupWikiPagesSchema.parse(params.arguments);
7698
7718
  const wikiPages = await listGroupWikiPages(group_id, {
7699
7719
  page,
7700
7720
  per_page,
7701
7721
  with_content,
7722
+ render_html,
7702
7723
  });
7703
7724
  return {
7704
7725
  content: [{ type: "text", text: JSON.stringify(wikiPages) }],
7705
7726
  };
7706
7727
  }
7707
7728
  case "get_group_wiki_page": {
7708
- const { group_id, slug } = GetGroupWikiPageSchema.parse(params.arguments);
7709
- const wikiPage = await getGroupWikiPage(group_id, slug);
7729
+ const { group_id, slug, render_html } = GetGroupWikiPageSchema.parse(params.arguments);
7730
+ const wikiPage = await getGroupWikiPage(group_id, slug, render_html);
7710
7731
  return {
7711
7732
  content: [{ type: "text", text: JSON.stringify(wikiPage) }],
7712
7733
  };
@@ -67,6 +67,12 @@ const REQUIRED_GITLAB_SCOPES_RO = ["read_api"];
67
67
  const PENDING_AUTH_MAX_SIZE = 1000;
68
68
  const PENDING_AUTH_TTL_MS = 10 * 60 * 1000; // 10 minutes
69
69
  const CLIENT_CACHE_MAX_SIZE = 1000;
70
+ function singleQueryParam(value) {
71
+ return typeof value === "string" ? value : undefined;
72
+ }
73
+ function invalidQueryParam(value) {
74
+ return value != null && typeof value !== "string";
75
+ }
70
76
  class BoundedLRUMap {
71
77
  _map = new Map();
72
78
  _maxSize;
@@ -503,11 +509,19 @@ class GitLabOAuthServerProvider {
503
509
  res.status(404).send("Callback proxy mode is not enabled");
504
510
  return;
505
511
  }
506
- const code = req.query.code;
507
- const state = req.query.state;
508
- const error = req.query.error;
512
+ if (invalidQueryParam(req.query.code) ||
513
+ invalidQueryParam(req.query.state) ||
514
+ invalidQueryParam(req.query.error) ||
515
+ invalidQueryParam(req.query.error_description)) {
516
+ res.status(400).send("Invalid query parameter");
517
+ return;
518
+ }
519
+ const code = singleQueryParam(req.query.code);
520
+ const state = singleQueryParam(req.query.state);
521
+ const error = singleQueryParam(req.query.error);
522
+ const errorDescription = singleQueryParam(req.query.error_description) ?? "(no description)";
509
523
  if (error) {
510
- logger.error(`GitLab OAuth error: ${error} — ${req.query.error_description ?? "(no description)"}`);
524
+ logger.error(`GitLab OAuth error: ${error} — ${errorDescription}`);
511
525
  res.status(400).send("Authorization failed");
512
526
  return;
513
527
  }
package/build/schemas.js CHANGED
@@ -2321,11 +2321,19 @@ export const ListWikiPagesSchema = z
2321
2321
  .object({
2322
2322
  project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
2323
2323
  with_content: z.coerce.boolean().optional().describe("Include content of the wiki pages"),
2324
+ render_html: z.coerce
2325
+ .boolean()
2326
+ .optional()
2327
+ .describe("Return rendered HTML content and include front_matter (e.g., the custom title)"),
2324
2328
  })
2325
2329
  .merge(PaginationOptionsSchema);
2326
2330
  export const GetWikiPageSchema = z.object({
2327
2331
  project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
2328
2332
  slug: z.string().describe("Slug of the wiki page (will be URL-encoded internally)"),
2333
+ render_html: z.coerce
2334
+ .boolean()
2335
+ .optional()
2336
+ .describe("Return rendered HTML content and include front_matter (e.g., the custom title)"),
2329
2337
  });
2330
2338
  export const CreateWikiPageSchema = z.object({
2331
2339
  project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
@@ -2350,6 +2358,7 @@ export const GitLabWikiPageSchema = z.object({
2350
2358
  slug: z.string(),
2351
2359
  format: z.string(),
2352
2360
  content: z.string().optional(),
2361
+ front_matter: z.record(z.unknown()).optional(),
2353
2362
  created_at: z.string().optional(),
2354
2363
  updated_at: z.string().optional(),
2355
2364
  });
@@ -2358,11 +2367,19 @@ export const ListGroupWikiPagesSchema = z
2358
2367
  .object({
2359
2368
  group_id: z.coerce.string().describe("Group ID or URL-encoded path"),
2360
2369
  with_content: z.coerce.boolean().optional().describe("Include content of the wiki pages"),
2370
+ render_html: z.coerce
2371
+ .boolean()
2372
+ .optional()
2373
+ .describe("Return rendered HTML content and include front_matter (e.g., the custom title)"),
2361
2374
  })
2362
2375
  .merge(PaginationOptionsSchema);
2363
2376
  export const GetGroupWikiPageSchema = z.object({
2364
2377
  group_id: z.coerce.string().describe("Group ID or URL-encoded path"),
2365
2378
  slug: z.string().describe("Slug of the wiki page (will be URL-encoded internally)"),
2379
+ render_html: z.coerce
2380
+ .boolean()
2381
+ .optional()
2382
+ .describe("Return rendered HTML content and include front_matter (e.g., the custom title)"),
2366
2383
  });
2367
2384
  export const CreateGroupWikiPageSchema = z.object({
2368
2385
  group_id: z.coerce.string().describe("Group ID or URL-encoded path"),
@@ -0,0 +1,27 @@
1
+ import assert from "node:assert/strict";
2
+ import { readFileSync } from "node:fs";
3
+ import { test } from "node:test";
4
+ const source = readFileSync(new URL("../index.ts", import.meta.url), "utf8");
5
+ const rawPathSegmentPatterns = [
6
+ "/issues/${issueIid}",
7
+ "/merge_requests/${mergeRequestIid}",
8
+ "/discussions/${discussionId}",
9
+ "/notes/${noteId}",
10
+ "/links/${issueLinkId}",
11
+ "/draft_notes/${draftNoteId}",
12
+ "/versions/${versionId}",
13
+ "/groups/${groupId}",
14
+ ];
15
+ test("GitLab URL path IDs are encoded before interpolation", () => {
16
+ for (const pattern of rawPathSegmentPatterns) {
17
+ assert.equal(source.includes(pattern), false, `${pattern} must use encodeGitLabPathSegment`);
18
+ }
19
+ });
20
+ test("encoding keeps path traversal payloads inside one URL segment", () => {
21
+ const payload = "1/../../../../admin/ci/variables";
22
+ const url = new URL(`https://gitlab.com/api/v4/projects/42/issues/${encodeURIComponent(payload)}/notes`);
23
+ assert.equal(url.pathname, "/api/v4/projects/42/issues/1%2F..%2F..%2F..%2F..%2Fadmin%2Fci%2Fvariables/notes");
24
+ });
25
+ test("malformed percent-encoded segments can still be encoded", () => {
26
+ assert.equal(encodeURIComponent("bad%zz/path"), "bad%25zz%2Fpath");
27
+ });
@@ -256,6 +256,14 @@ describe("callback-proxy cross-pod flow (stateless)", () => {
256
256
  const tokens = await podC.exchangeAuthorizationCode(podCClient, proxyCode, clientCodeVerifier, CLIENT_REDIRECT);
257
257
  assert.equal(tokens.access_token, "gl-access-123");
258
258
  });
259
+ test("callback rejects repeated query parameters", async () => {
260
+ const pod = makeProvider(loadMaterial(secret()));
261
+ const cbRes = makeFakeRes();
262
+ await pod.handleCallback(fakeReq({ code: "gitlab-code", state: ["one", "two"] }), cbRes);
263
+ assert.equal(cbRes.statusCode, 400);
264
+ assert.equal(cbRes.body, "Invalid query parameter");
265
+ assert.equal(stub.calls.length, 0);
266
+ });
259
267
  test("exchangeAuthorizationCode rejects wrong PKCE verifier", async () => {
260
268
  const s = secret();
261
269
  const pod = makeProvider(loadMaterial(s));
@@ -0,0 +1,32 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, test } from "node:test";
3
+ import { redactSensitiveGitLabFields } from "../../utils/redact-sensitive.js";
4
+ describe("When redactSensitiveGitLabFields runs", () => {
5
+ describe("with a project response containing credentials", () => {
6
+ test("should remove runners_token and import_url while keeping other fields", () => {
7
+ const data = {
8
+ id: 42,
9
+ name: "demo",
10
+ runners_token: "GR1348941secret",
11
+ import_url: "https://user:pass@example.com/repo.git",
12
+ };
13
+ const result = redactSensitiveGitLabFields(data);
14
+ assert.equal("runners_token" in result, false);
15
+ assert.equal("import_url" in result, false);
16
+ assert.equal(result.id, 42);
17
+ assert.equal(result.name, "demo");
18
+ });
19
+ });
20
+ describe("with a response that has no sensitive fields", () => {
21
+ test("should return the object unchanged", () => {
22
+ const data = { id: 1, name: "safe" };
23
+ assert.deepEqual(redactSensitiveGitLabFields(data), { id: 1, name: "safe" });
24
+ });
25
+ });
26
+ describe("with a non-object value", () => {
27
+ test("should return the value as-is", () => {
28
+ assert.equal(redactSensitiveGitLabFields(null), null);
29
+ assert.equal(redactSensitiveGitLabFields("raw"), "raw");
30
+ });
31
+ });
32
+ });
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Fields GitLab returns that are live credentials or carry embedded secrets.
3
+ * MCP tool results become part of the AI conversation context, so these must
4
+ * never be forwarded to the model.
5
+ */
6
+ const SENSITIVE_GITLAB_FIELDS = ["runners_token", "import_url"];
7
+ /**
8
+ * Strip known sensitive fields from a GitLab API response object before it is
9
+ * returned as an MCP tool result. Mutates and returns the same object.
10
+ *
11
+ * Used for raw passthrough responses (get_project / update_project) that do not
12
+ * go through a Zod schema, since Zod would otherwise strip unknown keys for us.
13
+ */
14
+ export function redactSensitiveGitLabFields(data) {
15
+ if (data && typeof data === "object") {
16
+ for (const key of SENSITIVE_GITLAB_FIELDS) {
17
+ delete data[key];
18
+ }
19
+ }
20
+ return data;
21
+ }
@@ -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.27",
3
+ "version": "2.1.29",
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/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
+ "test:mock": "node --import tsx/esm --test test/path-segment-encoding.test.ts && 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/utils/redact-sensitive.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",