@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 +40 -32
- package/README.md +47 -38
- package/README.zh-CN.md +36 -28
- package/build/index.js +79 -58
- package/build/oauth-proxy.js +18 -4
- package/build/schemas.js +17 -0
- package/build/test/path-segment-encoding.test.js +27 -0
- package/build/test/stateless/callback-proxy.test.js +8 -0
- package/build/test/utils/redact-sensitive.test.js +32 -0
- package/build/utils/redact-sensitive.js +21 -0
- package/build/utils/schema.js +42 -11
- package/package.json +5 -4
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 클라이언트 설정에서
|
|
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": "
|
|
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
|
|
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
|
|
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`
|
|
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
|
|
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": "
|
|
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
|
|
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
|
|
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
|
|
190
|
-
|
|
|
191
|
-
| Local OAuth
|
|
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
|
|
209
|
-
|
|
|
210
|
-
| `GITLAB_MCP_OAUTH`
|
|
211
|
-
| `GITLAB_API_URL`
|
|
212
|
-
| `GITLAB_OAUTH_APP_ID`
|
|
213
|
-
| `MCP_SERVER_URL`
|
|
214
|
-
| `STREAMABLE_HTTP`
|
|
215
|
-
| `GITLAB_OAUTH_CALLBACK_PROXY` | optional | Set to `true` to use the MCP server's fixed `/callback` URL
|
|
216
|
-
| `GITLAB_OAUTH_SCOPES`
|
|
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
|
|
270
|
-
|
|
|
271
|
-
| `REMOTE_AUTHORIZATION`
|
|
272
|
-
| `STREAMABLE_HTTP`
|
|
273
|
-
| `ENABLE_DYNAMIC_API_URL`
|
|
274
|
-
| `GITLAB_ALLOWED_HOSTS`
|
|
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`
|
|
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
|
|
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": "
|
|
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
|
|
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
|
|
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`
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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: [
|
|
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
|
};
|
package/build/oauth-proxy.js
CHANGED
|
@@ -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
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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} — ${
|
|
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
|
+
}
|
package/build/utils/schema.js
CHANGED
|
@@ -87,17 +87,48 @@ export const toJSONSchema = (schema) => {
|
|
|
87
87
|
return obj;
|
|
88
88
|
}
|
|
89
89
|
const fixedSchema = fixNullableOptional(jsonSchema, true);
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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.
|
|
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.
|
|
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",
|