@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 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 || "0.0.0.0";
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 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.20",
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}\"",