@zereight/mcp-gitlab 2.0.18 → 2.0.20
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.md +77 -13
- package/build/index.js +8 -0
- package/build/oauth.js +20 -11
- package/build/test/test-all-transport-server.js +6 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -28,6 +28,7 @@ The server supports two authentication methods:
|
|
|
28
28
|
#### Using OAuth2 Authentication
|
|
29
29
|
|
|
30
30
|
OAuth2 provides a more secure authentication flow using browser-based authentication. When enabled, the server will:
|
|
31
|
+
|
|
31
32
|
1. Open your browser to GitLab's authorization page
|
|
32
33
|
2. Wait for you to approve the access
|
|
33
34
|
3. Store the token securely for future use
|
|
@@ -55,6 +56,7 @@ Then configure the MCP server with OAuth:
|
|
|
55
56
|
"env": {
|
|
56
57
|
"GITLAB_USE_OAUTH": "true",
|
|
57
58
|
"GITLAB_OAUTH_CLIENT_ID": "your_oauth_client_id",
|
|
59
|
+
"GITLAB_OAUTH_CLIENT_SECRET": "your_oauth_client_secret", // Required for Confidential apps only
|
|
58
60
|
"GITLAB_OAUTH_REDIRECT_URI": "http://127.0.0.1:8888/callback",
|
|
59
61
|
"GITLAB_API_URL": "your_gitlab_api_url",
|
|
60
62
|
"GITLAB_PROJECT_ID": "your_project_id", // Optional: default project
|
|
@@ -94,13 +96,69 @@ Then configure the MCP server with OAuth:
|
|
|
94
96
|
|
|
95
97
|
#### vscode .vscode/mcp.json
|
|
96
98
|
|
|
99
|
+
**Using OAuth2 (Non-Confidential - Recommended):**
|
|
100
|
+
|
|
101
|
+
```json
|
|
102
|
+
{
|
|
103
|
+
"servers": {
|
|
104
|
+
"GitLab-MCP": {
|
|
105
|
+
"type": "stdio",
|
|
106
|
+
"command": "npx",
|
|
107
|
+
"args": ["-y", "@zereight/mcp-gitlab"],
|
|
108
|
+
"env": {
|
|
109
|
+
"GITLAB_USE_OAUTH": "true",
|
|
110
|
+
"GITLAB_OAUTH_CLIENT_ID": "your_oauth_client_id",
|
|
111
|
+
"GITLAB_OAUTH_REDIRECT_URI": "http://127.0.0.1:8888/callback",
|
|
112
|
+
"GITLAB_API_URL": "https://gitlab.com/api/v4",
|
|
113
|
+
"GITLAB_READ_ONLY_MODE": "false",
|
|
114
|
+
"USE_GITLAB_WIKI": "false",
|
|
115
|
+
"USE_MILESTONE": "false",
|
|
116
|
+
"USE_PIPELINE": "false"
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**Using OAuth2 (Confidential):**
|
|
124
|
+
|
|
125
|
+
```json
|
|
126
|
+
{
|
|
127
|
+
"inputs": [
|
|
128
|
+
{
|
|
129
|
+
"type": "promptString",
|
|
130
|
+
"id": "gitlab-oauth-secret",
|
|
131
|
+
"description": "GitLab OAuth Client Secret",
|
|
132
|
+
"password": true
|
|
133
|
+
}
|
|
134
|
+
],
|
|
135
|
+
"servers": {
|
|
136
|
+
"GitLab-MCP": {
|
|
137
|
+
"type": "stdio",
|
|
138
|
+
"command": "npx",
|
|
139
|
+
"args": ["-y", "@zereight/mcp-gitlab"],
|
|
140
|
+
"env": {
|
|
141
|
+
"GITLAB_USE_OAUTH": "true",
|
|
142
|
+
"GITLAB_OAUTH_CLIENT_ID": "your_oauth_client_id",
|
|
143
|
+
"GITLAB_OAUTH_CLIENT_SECRET": "${input:gitlab-oauth-secret}",
|
|
144
|
+
"GITLAB_OAUTH_REDIRECT_URI": "http://127.0.0.1:8888/callback",
|
|
145
|
+
"GITLAB_API_URL": "https://gitlab.com/api/v4",
|
|
146
|
+
"GITLAB_READ_ONLY_MODE": "false"
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
**Using Personal Access Token:**
|
|
154
|
+
|
|
97
155
|
```json
|
|
98
156
|
{
|
|
99
157
|
"inputs": [
|
|
100
158
|
{
|
|
101
159
|
"type": "promptString",
|
|
102
160
|
"id": "gitlab-token",
|
|
103
|
-
"description": "
|
|
161
|
+
"description": "GitLab Personal Access Token",
|
|
104
162
|
"password": true
|
|
105
163
|
}
|
|
106
164
|
],
|
|
@@ -111,9 +169,11 @@ Then configure the MCP server with OAuth:
|
|
|
111
169
|
"args": ["-y", "@zereight/mcp-gitlab"],
|
|
112
170
|
"env": {
|
|
113
171
|
"GITLAB_PERSONAL_ACCESS_TOKEN": "${input:gitlab-token}",
|
|
114
|
-
"GITLAB_API_URL": "
|
|
115
|
-
"GITLAB_READ_ONLY_MODE": "
|
|
116
|
-
|
|
172
|
+
"GITLAB_API_URL": "https://gitlab.com/api/v4",
|
|
173
|
+
"GITLAB_READ_ONLY_MODE": "false",
|
|
174
|
+
"USE_GITLAB_WIKI": "false",
|
|
175
|
+
"USE_MILESTONE": "false",
|
|
176
|
+
"USE_PIPELINE": "false"
|
|
117
177
|
}
|
|
118
178
|
}
|
|
119
179
|
}
|
|
@@ -127,7 +187,7 @@ env_vars = {
|
|
|
127
187
|
"GITLAB_PERSONAL_ACCESS_TOKEN": gitlab_access_token,
|
|
128
188
|
"GITLAB_API_URL": gitlab_api_url,
|
|
129
189
|
"USE_GITLAB_WIKI": use_gitlab_wiki
|
|
130
|
-
# ......the rest of the optional parameters
|
|
190
|
+
# ......the rest of the optional parameters
|
|
131
191
|
}
|
|
132
192
|
|
|
133
193
|
stdio_gitlab_mcp_client = MCPClient(
|
|
@@ -141,10 +201,11 @@ stdio_gitlab_mcp_client = MCPClient(
|
|
|
141
201
|
)
|
|
142
202
|
```
|
|
143
203
|
|
|
144
|
-
|
|
145
204
|
#### Docker
|
|
146
205
|
|
|
147
|
-
-
|
|
206
|
+
> **Note**: For Docker deployments, **Personal Access Token is recommended**. OAuth requires browser-based authentication and a local callback server, which does not work properly in containerized environments.
|
|
207
|
+
|
|
208
|
+
**Using Personal Access Token (stdio) - Recommended:**
|
|
148
209
|
|
|
149
210
|
```json
|
|
150
211
|
{
|
|
@@ -167,11 +228,11 @@ stdio_gitlab_mcp_client = MCPClient(
|
|
|
167
228
|
"USE_MILESTONE",
|
|
168
229
|
"-e",
|
|
169
230
|
"USE_PIPELINE",
|
|
170
|
-
"
|
|
231
|
+
"zereight050/gitlab-mcp"
|
|
171
232
|
],
|
|
172
233
|
"env": {
|
|
173
234
|
"GITLAB_PERSONAL_ACCESS_TOKEN": "your_gitlab_token",
|
|
174
|
-
"GITLAB_API_URL": "https://gitlab.com/api/v4",
|
|
235
|
+
"GITLAB_API_URL": "https://gitlab.com/api/v4",
|
|
175
236
|
"GITLAB_READ_ONLY_MODE": "false",
|
|
176
237
|
"USE_GITLAB_WIKI": "true",
|
|
177
238
|
"USE_MILESTONE": "true",
|
|
@@ -194,7 +255,7 @@ docker run -i --rm \
|
|
|
194
255
|
-e USE_PIPELINE=true \
|
|
195
256
|
-e SSE=true \
|
|
196
257
|
-p 3333:3002 \
|
|
197
|
-
|
|
258
|
+
zereight050/gitlab-mcp
|
|
198
259
|
```
|
|
199
260
|
|
|
200
261
|
```json
|
|
@@ -220,7 +281,7 @@ docker run -i --rm \
|
|
|
220
281
|
-e USE_PIPELINE=true \
|
|
221
282
|
-e STREAMABLE_HTTP=true \
|
|
222
283
|
-p 3333:3002 \
|
|
223
|
-
|
|
284
|
+
zereight050/gitlab-mcp
|
|
224
285
|
```
|
|
225
286
|
|
|
226
287
|
```json
|
|
@@ -241,6 +302,7 @@ docker run -i --rm \
|
|
|
241
302
|
- `GITLAB_PERSONAL_ACCESS_TOKEN`: Your GitLab personal access token. **Required in standard mode**; not used when `REMOTE_AUTHORIZATION=true` or when using OAuth.
|
|
242
303
|
- `GITLAB_USE_OAUTH`: Set to `true` to enable OAuth2 authentication instead of personal access token.
|
|
243
304
|
- `GITLAB_OAUTH_CLIENT_ID`: The Client ID from your GitLab OAuth application. Required when using OAuth.
|
|
305
|
+
- `GITLAB_OAUTH_CLIENT_SECRET`: The Client Secret from your GitLab OAuth application. Required only for Confidential applications.
|
|
244
306
|
- `GITLAB_OAUTH_REDIRECT_URI`: The OAuth callback URL. Default: `http://127.0.0.1:8888/callback`
|
|
245
307
|
- `GITLAB_OAUTH_TOKEN_PATH`: Custom path to store the OAuth token. Default: `~/.gitlab-mcp-token.json`
|
|
246
308
|
- `REMOTE_AUTHORIZATION`: When set to 'true', enables remote per-session authorization via HTTP headers. In this mode:
|
|
@@ -290,6 +352,7 @@ When using Streamable HTTP transport, the following endpoints are available:
|
|
|
290
352
|
### Remote Authorization Setup (Multi-User Support)
|
|
291
353
|
|
|
292
354
|
When using `REMOTE_AUTHORIZATION=true`, the MCP server can support multiple users, each with their own GitLab token passed via HTTP headers. This is useful for:
|
|
355
|
+
|
|
293
356
|
- Shared MCP server instances where each user needs their own GitLab access
|
|
294
357
|
- IDE integrations that can inject user-specific tokens into MCP requests
|
|
295
358
|
|
|
@@ -304,7 +367,7 @@ docker run -d \
|
|
|
304
367
|
-e GITLAB_READ_ONLY_MODE=true \
|
|
305
368
|
-e SESSION_TIMEOUT_SECONDS=3600 \
|
|
306
369
|
-p 3333:3002 \
|
|
307
|
-
|
|
370
|
+
zereight050/gitlab-mcp
|
|
308
371
|
```
|
|
309
372
|
|
|
310
373
|
**Client Configuration:**
|
|
@@ -339,9 +402,10 @@ The token is stored per session (identified by `mcp-session-id` header) and reus
|
|
|
339
402
|
```
|
|
340
403
|
|
|
341
404
|
**Important Notes:**
|
|
405
|
+
|
|
342
406
|
- Remote authorization **only works with Streamable HTTP transport**
|
|
343
407
|
- Each session is isolated - tokens from one session cannot access another session's data
|
|
344
|
-
|
|
408
|
+
Tokens are automatically cleaned up when sessions close
|
|
345
409
|
- **Session timeout:** Auth tokens expire after `SESSION_TIMEOUT_SECONDS` (default 1 hour) of inactivity. After timeout, the client must send auth headers again. The transport session remains active.
|
|
346
410
|
- Each request resets the timeout timer for that session
|
|
347
411
|
- **Rate limiting:** Each session is limited to `MAX_REQUESTS_PER_MINUTE` requests per minute (default 60)
|
package/build/index.js
CHANGED
|
@@ -5166,6 +5166,14 @@ async function startStreamableHTTPServer() {
|
|
|
5166
5166
|
await handleRequest();
|
|
5167
5167
|
}
|
|
5168
5168
|
});
|
|
5169
|
+
// Reject unsupported methods on /mcp
|
|
5170
|
+
app.get("/mcp", (_req, res) => {
|
|
5171
|
+
res.setHeader("Allow", "POST, DELETE");
|
|
5172
|
+
res.status(405).json({
|
|
5173
|
+
error: "Method Not Allowed",
|
|
5174
|
+
message: "GET /mcp is not supported when STREAMABLE_HTTP is enabled. Use POST to communicate with the MCP server."
|
|
5175
|
+
});
|
|
5176
|
+
});
|
|
5169
5177
|
// Metrics endpoint
|
|
5170
5178
|
app.get("/metrics", (_req, res) => {
|
|
5171
5179
|
res.json({
|
package/build/oauth.js
CHANGED
|
@@ -16,7 +16,7 @@ const pendingAuthRequests = new Map();
|
|
|
16
16
|
* Check if a port is already in use
|
|
17
17
|
*/
|
|
18
18
|
async function isPortInUse(port) {
|
|
19
|
-
return new Promise(
|
|
19
|
+
return new Promise(resolve => {
|
|
20
20
|
const server = net.createServer();
|
|
21
21
|
server.once("error", (err) => {
|
|
22
22
|
if (err.code === "EADDRINUSE") {
|
|
@@ -44,9 +44,9 @@ async function requestAuthFromExistingServer(port, requestId) {
|
|
|
44
44
|
path: `/auth-request?requestId=${requestId}`,
|
|
45
45
|
method: "GET",
|
|
46
46
|
};
|
|
47
|
-
const req = http.request(options,
|
|
47
|
+
const req = http.request(options, res => {
|
|
48
48
|
let data = "";
|
|
49
|
-
res.on("data",
|
|
49
|
+
res.on("data", chunk => {
|
|
50
50
|
data += chunk;
|
|
51
51
|
});
|
|
52
52
|
res.on("end", () => {
|
|
@@ -64,7 +64,7 @@ async function requestAuthFromExistingServer(port, requestId) {
|
|
|
64
64
|
}
|
|
65
65
|
});
|
|
66
66
|
});
|
|
67
|
-
req.on("error",
|
|
67
|
+
req.on("error", error => {
|
|
68
68
|
reject(new Error(`Failed to connect to existing OAuth server: ${error.message}`));
|
|
69
69
|
});
|
|
70
70
|
req.setTimeout(5 * 60 * 1000, () => {
|
|
@@ -82,8 +82,7 @@ export class GitLabOAuth {
|
|
|
82
82
|
constructor(config) {
|
|
83
83
|
this.config = config;
|
|
84
84
|
this.tokenStoragePath =
|
|
85
|
-
config.tokenStoragePath ||
|
|
86
|
-
path.join(process.env.HOME || "", ".gitlab-mcp-token.json");
|
|
85
|
+
config.tokenStoragePath || path.join(process.env.HOME || "", ".gitlab-mcp-token.json");
|
|
87
86
|
}
|
|
88
87
|
/**
|
|
89
88
|
* Get the authorization URL for OAuth flow
|
|
@@ -119,6 +118,10 @@ export class GitLabOAuth {
|
|
|
119
118
|
redirect_uri: this.config.redirectUri,
|
|
120
119
|
code_verifier: this.codeVerifier,
|
|
121
120
|
});
|
|
121
|
+
// Add client_secret for Confidential applications
|
|
122
|
+
if (this.config.clientSecret) {
|
|
123
|
+
params.append("client_secret", this.config.clientSecret);
|
|
124
|
+
}
|
|
122
125
|
const response = await fetch(tokenUrl, {
|
|
123
126
|
method: "POST",
|
|
124
127
|
headers: {
|
|
@@ -130,7 +133,7 @@ export class GitLabOAuth {
|
|
|
130
133
|
const errorText = await response.text();
|
|
131
134
|
throw new Error(`Token exchange failed: ${response.status} ${errorText}`);
|
|
132
135
|
}
|
|
133
|
-
const data = await response.json();
|
|
136
|
+
const data = (await response.json());
|
|
134
137
|
return {
|
|
135
138
|
access_token: data.access_token,
|
|
136
139
|
refresh_token: data.refresh_token,
|
|
@@ -150,6 +153,10 @@ export class GitLabOAuth {
|
|
|
150
153
|
grant_type: "refresh_token",
|
|
151
154
|
redirect_uri: this.config.redirectUri,
|
|
152
155
|
});
|
|
156
|
+
// Add client_secret for Confidential applications
|
|
157
|
+
if (this.config.clientSecret) {
|
|
158
|
+
params.append("client_secret", this.config.clientSecret);
|
|
159
|
+
}
|
|
153
160
|
const response = await fetch(tokenUrl, {
|
|
154
161
|
method: "POST",
|
|
155
162
|
headers: {
|
|
@@ -161,7 +168,7 @@ export class GitLabOAuth {
|
|
|
161
168
|
const errorText = await response.text();
|
|
162
169
|
throw new Error(`Token refresh failed: ${response.status} ${errorText}`);
|
|
163
170
|
}
|
|
164
|
-
const data = await response.json();
|
|
171
|
+
const data = (await response.json());
|
|
165
172
|
return {
|
|
166
173
|
access_token: data.access_token,
|
|
167
174
|
refresh_token: data.refresh_token || refreshToken,
|
|
@@ -271,7 +278,7 @@ export class GitLabOAuth {
|
|
|
271
278
|
const authUrl = await this.getAuthorizationUrl(newState);
|
|
272
279
|
logger.info("Opening browser for new authentication request...");
|
|
273
280
|
logger.info(`If browser doesn't open, visit: ${authUrl}`);
|
|
274
|
-
open(authUrl).catch(
|
|
281
|
+
open(authUrl).catch(err => {
|
|
275
282
|
logger.error("Failed to open browser:", err);
|
|
276
283
|
logger.info(`Please manually open: ${authUrl}`);
|
|
277
284
|
});
|
|
@@ -427,12 +434,12 @@ export class GitLabOAuth {
|
|
|
427
434
|
const authUrl = await this.getAuthorizationUrl(state);
|
|
428
435
|
logger.info("Opening browser for authentication...");
|
|
429
436
|
logger.info(`If browser doesn't open, visit: ${authUrl}`);
|
|
430
|
-
open(authUrl).catch(
|
|
437
|
+
open(authUrl).catch(err => {
|
|
431
438
|
logger.error("Failed to open browser:", err);
|
|
432
439
|
logger.info(`Please manually open: ${authUrl}`);
|
|
433
440
|
});
|
|
434
441
|
});
|
|
435
|
-
server.on("error",
|
|
442
|
+
server.on("error", error => {
|
|
436
443
|
logger.error("OAuth server error:", error);
|
|
437
444
|
const pending = pendingAuthRequests.get(initialRequestId);
|
|
438
445
|
if (pending) {
|
|
@@ -502,6 +509,7 @@ export class GitLabOAuth {
|
|
|
502
509
|
*/
|
|
503
510
|
export async function initializeOAuth(gitlabUrl = "https://gitlab.com") {
|
|
504
511
|
const clientId = process.env.GITLAB_OAUTH_CLIENT_ID;
|
|
512
|
+
const clientSecret = process.env.GITLAB_OAUTH_CLIENT_SECRET;
|
|
505
513
|
const redirectUri = process.env.GITLAB_OAUTH_REDIRECT_URI || "http://127.0.0.1:8888/callback";
|
|
506
514
|
const tokenStoragePath = process.env.GITLAB_OAUTH_TOKEN_PATH;
|
|
507
515
|
if (!clientId) {
|
|
@@ -509,6 +517,7 @@ export async function initializeOAuth(gitlabUrl = "https://gitlab.com") {
|
|
|
509
517
|
}
|
|
510
518
|
const oauth = new GitLabOAuth({
|
|
511
519
|
clientId,
|
|
520
|
+
clientSecret,
|
|
512
521
|
redirectUri,
|
|
513
522
|
gitlabUrl,
|
|
514
523
|
scopes: ["api"],
|
|
@@ -216,6 +216,12 @@ describe('GitLab MCP Server - Streamable HTTP Transport', () => {
|
|
|
216
216
|
cleanup();
|
|
217
217
|
console.log('Client disconnected from Streamable HTTP server');
|
|
218
218
|
});
|
|
219
|
+
test('should return 405 for GET /mcp', async () => {
|
|
220
|
+
const response = await fetch(`http://${HOST}:${port}/mcp`);
|
|
221
|
+
assert.strictEqual(response.status, 405, 'GET /mcp should respond with 405');
|
|
222
|
+
const body = await response.json();
|
|
223
|
+
assert.strictEqual(body?.error, 'Method Not Allowed');
|
|
224
|
+
});
|
|
219
225
|
test('should list tools via Streamable HTTP', async () => {
|
|
220
226
|
const tools = await client.listTools();
|
|
221
227
|
assert.ok(tools !== null && tools !== undefined, 'Tools response should be defined');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zereight/mcp-gitlab",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.20",
|
|
4
4
|
"description": "MCP server for using the GitLab API",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "zereight",
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
"access": "public"
|
|
18
18
|
},
|
|
19
19
|
"engines": {
|
|
20
|
-
"node": ">=
|
|
20
|
+
"node": ">=18.0.0",
|
|
21
|
+
"npm": ">=9.0.0"
|
|
21
22
|
},
|
|
22
23
|
"scripts": {
|
|
23
24
|
"build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
|