@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 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 in a GitLab project with filtering options",
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 in a GitLab project with optional filtering
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
- projectId = decodeURIComponent(projectId); // Decode project ID
1240
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests`);
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.21",
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:all": "npm run test && npm run test:mcp:readonly && npm run test:oauth",
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}\"",