@zereight/mcp-gitlab 2.0.20 → 2.0.22
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 +5 -1
- package/build/index.js +10 -6
- package/build/schemas.js +1 -1
- package/build/test/test-list-merge-requests.js +106 -0
- package/build/test/utils/mock-gitlab-server.js +65 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -247,6 +247,7 @@ stdio_gitlab_mcp_client = MCPClient(
|
|
|
247
247
|
|
|
248
248
|
```shell
|
|
249
249
|
docker run -i --rm \
|
|
250
|
+
-e HOST=0.0.0.0 \
|
|
250
251
|
-e GITLAB_PERSONAL_ACCESS_TOKEN=your_gitlab_token \
|
|
251
252
|
-e GITLAB_API_URL="https://gitlab.com/api/v4" \
|
|
252
253
|
-e GITLAB_READ_ONLY_MODE=true \
|
|
@@ -273,6 +274,7 @@ docker run -i --rm \
|
|
|
273
274
|
|
|
274
275
|
```shell
|
|
275
276
|
docker run -i --rm \
|
|
277
|
+
-e HOST=0.0.0.0 \
|
|
276
278
|
-e GITLAB_PERSONAL_ACCESS_TOKEN=your_gitlab_token \
|
|
277
279
|
-e GITLAB_API_URL="https://gitlab.com/api/v4" \
|
|
278
280
|
-e GITLAB_READ_ONLY_MODE=true \
|
|
@@ -333,6 +335,7 @@ docker run -i --rm \
|
|
|
333
335
|
|
|
334
336
|
#### Performance & Security Configuration
|
|
335
337
|
|
|
338
|
+
- `HOST`: Server host address. Default: `127.0.0.1` (localhost only). Set to `0.0.0.0` to allow external connections (required for Docker with port forwarding).
|
|
336
339
|
- `MAX_SESSIONS`: Maximum number of concurrent sessions allowed. Default: `1000`. Valid range: 1-10000. When limit is reached, new connections are rejected with HTTP 503.
|
|
337
340
|
- `MAX_REQUESTS_PER_MINUTE`: Rate limit per session in requests per minute. Default: `60`. Valid range: 1-1000. Exceeded requests return HTTP 429.
|
|
338
341
|
- `PORT`: Server port. Default: `3002`. Valid range: 1-65535.
|
|
@@ -361,6 +364,7 @@ When using `REMOTE_AUTHORIZATION=true`, the MCP server can support multiple user
|
|
|
361
364
|
```bash
|
|
362
365
|
# Start server with remote authorization
|
|
363
366
|
docker run -d \
|
|
367
|
+
-e HOST=0.0.0.0 \
|
|
364
368
|
-e STREAMABLE_HTTP=true \
|
|
365
369
|
-e REMOTE_AUTHORIZATION=true \
|
|
366
370
|
-e GITLAB_API_URL="https://gitlab.com/api/v4" \
|
|
@@ -487,7 +491,7 @@ The token is stored per session (identified by `mcp-session-id` header) and reus
|
|
|
487
491
|
67. `play_pipeline_job` - Run a manual pipeline job
|
|
488
492
|
68. `retry_pipeline_job` - Retry a failed or canceled pipeline job
|
|
489
493
|
69. `cancel_pipeline_job` - Cancel a running pipeline job
|
|
490
|
-
70. `list_merge_requests` - List merge requests in a GitLab project with filtering options
|
|
494
|
+
70. `list_merge_requests` - List merge requests globally or in a specific GitLab project with filtering options (project_id is now optional)
|
|
491
495
|
71. `list_milestones` - List milestones in a GitLab project with filtering options
|
|
492
496
|
72. `get_milestone` - Get details of a specific milestone
|
|
493
497
|
73. `create_milestone` - Create a new milestone in a GitLab project
|
package/build/index.js
CHANGED
|
@@ -165,7 +165,7 @@ const STREAMABLE_HTTP = process.env.STREAMABLE_HTTP === "true";
|
|
|
165
165
|
const REMOTE_AUTHORIZATION = process.env.REMOTE_AUTHORIZATION === "true";
|
|
166
166
|
const ENABLE_DYNAMIC_API_URL = process.env.ENABLE_DYNAMIC_API_URL === "true";
|
|
167
167
|
const SESSION_TIMEOUT_SECONDS = process.env.SESSION_TIMEOUT_SECONDS ? parseInt(process.env.SESSION_TIMEOUT_SECONDS) : 3600;
|
|
168
|
-
const HOST = process.env.HOST || "
|
|
168
|
+
const HOST = process.env.HOST || "127.0.0.1";
|
|
169
169
|
const PORT = process.env.PORT || 3002;
|
|
170
170
|
// Add proxy configuration
|
|
171
171
|
const HTTP_PROXY = process.env.HTTP_PROXY;
|
|
@@ -739,7 +739,7 @@ const allTools = [
|
|
|
739
739
|
},
|
|
740
740
|
{
|
|
741
741
|
name: "list_merge_requests",
|
|
742
|
-
description: "List merge requests
|
|
742
|
+
description: "List merge requests. Without project_id, lists MRs assigned to the authenticated user by default (use scope='all' for all accessible MRs). With project_id, lists MRs for that specific project.",
|
|
743
743
|
inputSchema: toJSONSchema(ListMergeRequestsSchema),
|
|
744
744
|
},
|
|
745
745
|
{
|
|
@@ -1229,15 +1229,19 @@ async function listIssues(projectId, options = {}) {
|
|
|
1229
1229
|
return z.array(GitLabIssueSchema).parse(data);
|
|
1230
1230
|
}
|
|
1231
1231
|
/**
|
|
1232
|
-
* List merge requests
|
|
1232
|
+
* List merge requests globally or for a specific GitLab project
|
|
1233
1233
|
*
|
|
1234
|
-
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
1234
|
+
* @param {string} [projectId] - The ID or URL-encoded path of the project.
|
|
1235
|
+
* If omitted, lists MRs assigned to the authenticated user by default.
|
|
1235
1236
|
* @param {Object} options - Optional filtering parameters
|
|
1236
1237
|
* @returns {Promise<GitLabMergeRequest[]>} List of merge requests
|
|
1237
1238
|
*/
|
|
1238
1239
|
async function listMergeRequests(projectId, options = {}) {
|
|
1239
|
-
|
|
1240
|
-
const
|
|
1240
|
+
const decodedProjectId = projectId ? decodeURIComponent(projectId) : undefined;
|
|
1241
|
+
const endpoint = decodedProjectId
|
|
1242
|
+
? `${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(decodedProjectId))}/merge_requests`
|
|
1243
|
+
: `${getEffectiveApiUrl()}/merge_requests`;
|
|
1244
|
+
const url = new URL(endpoint);
|
|
1241
1245
|
// Add all query parameters
|
|
1242
1246
|
Object.entries(options).forEach(([key, value]) => {
|
|
1243
1247
|
if (value !== undefined) {
|
package/build/schemas.js
CHANGED
|
@@ -1105,7 +1105,7 @@ export const ListIssuesSchema = z
|
|
|
1105
1105
|
// Merge Requests API operation schemas
|
|
1106
1106
|
export const ListMergeRequestsSchema = z
|
|
1107
1107
|
.object({
|
|
1108
|
-
project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
|
|
1108
|
+
project_id: z.coerce.string().optional().describe("Project ID or URL-encoded path (optional - if not provided, lists all merge requests the user has access to)"),
|
|
1109
1109
|
assignee_id: z.coerce
|
|
1110
1110
|
.string()
|
|
1111
1111
|
.optional()
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { describe, test, before, after } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { spawn } from 'child_process';
|
|
4
|
+
import { MockGitLabServer, findMockServerPort } from './utils/mock-gitlab-server.js';
|
|
5
|
+
const MOCK_TOKEN = 'glpat-mock-token-12345';
|
|
6
|
+
const TEST_PROJECT_ID = '123';
|
|
7
|
+
// Helper to run the MCP tool
|
|
8
|
+
async function callListMergeRequests(args = {}, env) {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const proc = spawn('node', ['build/index.js'], {
|
|
11
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
12
|
+
env: {
|
|
13
|
+
...process.env,
|
|
14
|
+
...env,
|
|
15
|
+
GITLAB_READ_ONLY_MODE: 'true'
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
let output = '';
|
|
19
|
+
let errorOutput = '';
|
|
20
|
+
proc.stdout?.on('data', d => output += d);
|
|
21
|
+
proc.stderr?.on('data', d => errorOutput += d);
|
|
22
|
+
proc.on('close', (code) => {
|
|
23
|
+
if (code !== 0)
|
|
24
|
+
return reject(new Error(`Process exited with code ${code}: ${errorOutput}`));
|
|
25
|
+
// Find the JSON line in stdout
|
|
26
|
+
const line = output.split('\n').find(l => l.startsWith('{'));
|
|
27
|
+
if (!line)
|
|
28
|
+
return reject(new Error('No JSON output found'));
|
|
29
|
+
try {
|
|
30
|
+
const response = JSON.parse(line);
|
|
31
|
+
if (response.error) {
|
|
32
|
+
reject(response.error);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
// Parse the tool result content
|
|
36
|
+
const content = response.result?.content?.[0]?.text;
|
|
37
|
+
if (content) {
|
|
38
|
+
try {
|
|
39
|
+
resolve(JSON.parse(content));
|
|
40
|
+
}
|
|
41
|
+
catch (e) {
|
|
42
|
+
reject(new Error(`Failed to parse tool output JSON: ${content}`));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
// Fallback for direct result (if changed in future) or empty
|
|
47
|
+
resolve(response.result);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch (e) {
|
|
52
|
+
reject(e);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
proc.stdin?.end(JSON.stringify({
|
|
56
|
+
jsonrpc: "2.0", id: 1, method: "tools/call",
|
|
57
|
+
params: { name: "list_merge_requests", arguments: args }
|
|
58
|
+
}) + '\n');
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
describe('list_merge_requests', () => {
|
|
62
|
+
let mockGitLab;
|
|
63
|
+
let mockGitLabUrl;
|
|
64
|
+
before(async () => {
|
|
65
|
+
const mockPort = await findMockServerPort(9000);
|
|
66
|
+
mockGitLab = new MockGitLabServer({
|
|
67
|
+
port: mockPort,
|
|
68
|
+
validTokens: [MOCK_TOKEN]
|
|
69
|
+
});
|
|
70
|
+
await mockGitLab.start();
|
|
71
|
+
mockGitLabUrl = mockGitLab.getUrl();
|
|
72
|
+
});
|
|
73
|
+
after(async () => {
|
|
74
|
+
await mockGitLab.stop();
|
|
75
|
+
});
|
|
76
|
+
test('lists global merge requests (no project_id)', async () => {
|
|
77
|
+
const mrs = await callListMergeRequests({}, {
|
|
78
|
+
GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
|
|
79
|
+
GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN
|
|
80
|
+
});
|
|
81
|
+
assert.ok(Array.isArray(mrs), 'Response should be an array');
|
|
82
|
+
assert.strictEqual(mrs.length, 2, 'Should return 2 mock MRs');
|
|
83
|
+
// Schema coerces project_id to string
|
|
84
|
+
assert.strictEqual(String(mrs[0].project_id), '123', 'MR should have correct project_id');
|
|
85
|
+
});
|
|
86
|
+
test('lists project-specific merge requests', async () => {
|
|
87
|
+
const mrs = await callListMergeRequests({ project_id: TEST_PROJECT_ID }, {
|
|
88
|
+
GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
|
|
89
|
+
GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN
|
|
90
|
+
});
|
|
91
|
+
assert.ok(Array.isArray(mrs), 'Response should be an array');
|
|
92
|
+
assert.strictEqual(mrs.length, 2, 'Should return 2 mock MRs');
|
|
93
|
+
assert.strictEqual(mrs[0].title, 'Test MR 1');
|
|
94
|
+
});
|
|
95
|
+
test('filters global merge requests', async () => {
|
|
96
|
+
// Note: The mock server returns static data, so filtering won't actually filter the results
|
|
97
|
+
// unless we implement filtering logic in the mock.
|
|
98
|
+
// But we can verify the call succeeds.
|
|
99
|
+
const mrs = await callListMergeRequests({ state: 'opened' }, {
|
|
100
|
+
GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
|
|
101
|
+
GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN
|
|
102
|
+
});
|
|
103
|
+
assert.ok(Array.isArray(mrs), 'Response should be an array');
|
|
104
|
+
assert.strictEqual(mrs.length, 2, 'Should return 2 mock MRs');
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -139,15 +139,71 @@ export class MockGitLabServer {
|
|
|
139
139
|
}
|
|
140
140
|
});
|
|
141
141
|
});
|
|
142
|
+
// GET /api/v4/merge_requests - List all merge requests (global)
|
|
143
|
+
this.app.get('/api/v4/merge_requests', (req, res) => {
|
|
144
|
+
res.json([
|
|
145
|
+
{
|
|
146
|
+
id: 1,
|
|
147
|
+
iid: 1,
|
|
148
|
+
project_id: 123,
|
|
149
|
+
title: 'Test MR 1',
|
|
150
|
+
description: 'Description for MR 1',
|
|
151
|
+
state: 'opened',
|
|
152
|
+
created_at: '2024-01-01T00:00:00Z',
|
|
153
|
+
updated_at: '2024-01-01T00:00:00Z',
|
|
154
|
+
merged_at: null,
|
|
155
|
+
closed_at: null,
|
|
156
|
+
target_branch: 'main',
|
|
157
|
+
source_branch: 'feature-1',
|
|
158
|
+
web_url: 'https://gitlab.mock/project/123/merge_requests/1',
|
|
159
|
+
merge_commit_sha: null,
|
|
160
|
+
author: {
|
|
161
|
+
id: 1,
|
|
162
|
+
username: 'test-user',
|
|
163
|
+
name: 'Test User'
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
id: 2,
|
|
168
|
+
iid: 2,
|
|
169
|
+
project_id: 123,
|
|
170
|
+
title: 'Test MR 2',
|
|
171
|
+
description: 'Description for MR 2',
|
|
172
|
+
state: 'merged',
|
|
173
|
+
created_at: '2024-01-02T00:00:00Z',
|
|
174
|
+
updated_at: '2024-01-03T00:00:00Z',
|
|
175
|
+
merged_at: '2024-01-03T00:00:00Z',
|
|
176
|
+
closed_at: null,
|
|
177
|
+
target_branch: 'main',
|
|
178
|
+
source_branch: 'feature-2',
|
|
179
|
+
web_url: 'https://gitlab.mock/project/123/merge_requests/2',
|
|
180
|
+
merge_commit_sha: 'abcdef1234567890',
|
|
181
|
+
author: {
|
|
182
|
+
id: 1,
|
|
183
|
+
username: 'test-user',
|
|
184
|
+
name: 'Test User'
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
]);
|
|
188
|
+
});
|
|
142
189
|
// GET /api/v4/projects/:projectId/merge_requests - List merge requests
|
|
143
190
|
this.app.get('/api/v4/projects/:projectId/merge_requests', (req, res) => {
|
|
144
191
|
res.json([
|
|
145
192
|
{
|
|
146
193
|
id: 1,
|
|
147
194
|
iid: 1,
|
|
195
|
+
project_id: 123,
|
|
148
196
|
title: 'Test MR 1',
|
|
197
|
+
description: 'Description for MR 1',
|
|
149
198
|
state: 'opened',
|
|
150
199
|
created_at: '2024-01-01T00:00:00Z',
|
|
200
|
+
updated_at: '2024-01-01T00:00:00Z',
|
|
201
|
+
merged_at: null,
|
|
202
|
+
closed_at: null,
|
|
203
|
+
target_branch: 'main',
|
|
204
|
+
source_branch: 'feature-1',
|
|
205
|
+
web_url: 'https://gitlab.mock/project/123/merge_requests/1',
|
|
206
|
+
merge_commit_sha: null,
|
|
151
207
|
author: {
|
|
152
208
|
id: 1,
|
|
153
209
|
username: 'test-user',
|
|
@@ -157,9 +213,18 @@ export class MockGitLabServer {
|
|
|
157
213
|
{
|
|
158
214
|
id: 2,
|
|
159
215
|
iid: 2,
|
|
216
|
+
project_id: 123,
|
|
160
217
|
title: 'Test MR 2',
|
|
218
|
+
description: 'Description for MR 2',
|
|
161
219
|
state: 'merged',
|
|
162
220
|
created_at: '2024-01-02T00:00:00Z',
|
|
221
|
+
updated_at: '2024-01-03T00:00:00Z',
|
|
222
|
+
merged_at: '2024-01-03T00:00:00Z',
|
|
223
|
+
closed_at: null,
|
|
224
|
+
target_branch: 'main',
|
|
225
|
+
source_branch: 'feature-2',
|
|
226
|
+
web_url: 'https://gitlab.mock/project/123/merge_requests/2',
|
|
227
|
+
merge_commit_sha: 'abcdef1234567890',
|
|
163
228
|
author: {
|
|
164
229
|
id: 1,
|
|
165
230
|
username: 'test-user',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zereight/mcp-gitlab",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.22",
|
|
4
4
|
"description": "MCP server for using the GitLab API",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "zereight",
|
|
@@ -33,7 +33,8 @@
|
|
|
33
33
|
"test:server": "npm run build && node build/test/test-all-transport-server.js",
|
|
34
34
|
"test:mcp:readonly": "tsx test/readonly-mcp-tests.ts",
|
|
35
35
|
"test:oauth": "tsx test/oauth-tests.ts",
|
|
36
|
-
"test:
|
|
36
|
+
"test:list-merge-requests": "npm run build && tsx test/test-list-merge-requests.ts",
|
|
37
|
+
"test:all": "npm run test && npm run test:mcp:readonly && npm run test:oauth && npm run test:list-merge-requests",
|
|
37
38
|
"lint": "eslint . --ext .ts",
|
|
38
39
|
"lint:fix": "eslint . --ext .ts --fix",
|
|
39
40
|
"format": "prettier --write \"**/*.{js,ts,json,md}\"",
|