@zereight/mcp-gitlab 2.0.21 → 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 +1 -1
- package/build/index.js +9 -5
- 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
|
@@ -491,7 +491,7 @@ The token is stored per session (identified by `mcp-session-id` header) and reus
|
|
|
491
491
|
67. `play_pipeline_job` - Run a manual pipeline job
|
|
492
492
|
68. `retry_pipeline_job` - Retry a failed or canceled pipeline job
|
|
493
493
|
69. `cancel_pipeline_job` - Cancel a running pipeline job
|
|
494
|
-
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)
|
|
495
495
|
71. `list_milestones` - List milestones in a GitLab project with filtering options
|
|
496
496
|
72. `get_milestone` - Get details of a specific milestone
|
|
497
497
|
73. `create_milestone` - Create a new milestone in a GitLab project
|
package/build/index.js
CHANGED
|
@@ -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}\"",
|