@zereight/mcp-gitlab 2.0.13 → 2.0.18
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 +2 -0
- package/build/gitlab-client-pool.js +113 -0
- package/build/index.js +450 -356
- package/build/test/client-pool-test.js +109 -0
- package/build/test/dynamic-api-url-test.js +304 -0
- package/build/test/dynamic-routing-tests.js +442 -0
- package/build/test/multi-server-test.js +182 -0
- package/build/test/remote-auth-simple-test.js +2 -0
- package/build/test/utils/mock-gitlab-server.js +73 -4
- package/build/test/utils/server-launcher.js +25 -9
- package/package.json +8 -4
|
@@ -8,22 +8,53 @@ export class MockGitLabServer {
|
|
|
8
8
|
server = null;
|
|
9
9
|
config;
|
|
10
10
|
requestCount = 0;
|
|
11
|
+
customRouter;
|
|
12
|
+
customHandlers = new Map();
|
|
11
13
|
constructor(config) {
|
|
12
14
|
this.config = config;
|
|
13
15
|
this.app = express();
|
|
16
|
+
this.customRouter = express.Router();
|
|
17
|
+
// Dynamic dispatcher for custom handlers
|
|
18
|
+
this.customRouter.use((req, res, next) => {
|
|
19
|
+
// Create a key from method and path (relative to /api/v4)
|
|
20
|
+
// req.path is already relative to the mount point
|
|
21
|
+
const key = `${req.method.toUpperCase()}:${req.path}`;
|
|
22
|
+
console.log(`[CustomRouter] Checking key: '${key}'`);
|
|
23
|
+
const handler = this.customHandlers.get(key);
|
|
24
|
+
if (handler) {
|
|
25
|
+
console.log(`[MockServer] Custom handler hit: ${key}`);
|
|
26
|
+
return handler(req, res, next);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
console.log(`[CustomRouter] No handler found for key: '${key}'. Available keys: ${Array.from(this.customHandlers.keys()).join(', ')}`);
|
|
30
|
+
}
|
|
31
|
+
next();
|
|
32
|
+
});
|
|
14
33
|
this.setupMiddleware();
|
|
34
|
+
this.app.use('/api/v4', this.customRouter); // Mount router on API path
|
|
15
35
|
this.setupRoutes();
|
|
16
36
|
}
|
|
37
|
+
addMockHandler(method, path, handler) {
|
|
38
|
+
// Note: path should be relative to /api/v4
|
|
39
|
+
const key = `${method.toUpperCase()}:${path}`;
|
|
40
|
+
console.log(`[MockServer] Adding custom handler: ${key}`);
|
|
41
|
+
this.customHandlers.set(key, handler);
|
|
42
|
+
}
|
|
43
|
+
clearCustomHandlers() {
|
|
44
|
+
console.log('[MockServer] Clearing custom handlers');
|
|
45
|
+
this.customHandlers.clear();
|
|
46
|
+
}
|
|
17
47
|
/**
|
|
18
48
|
* Setup middleware including auth validation
|
|
19
49
|
*/
|
|
20
50
|
setupMiddleware() {
|
|
21
|
-
this
|
|
22
|
-
// Request counter for rate limiting tests
|
|
51
|
+
// Request counter for rate limiting tests - Place this FIRST to log everything
|
|
23
52
|
this.app.use((req, res, next) => {
|
|
53
|
+
console.log(`[MockServer] ${req.method} ${req.originalUrl}`);
|
|
24
54
|
this.requestCount++;
|
|
25
55
|
next();
|
|
26
56
|
});
|
|
57
|
+
this.app.use(express.json());
|
|
27
58
|
// Artificial delay middleware (for timeout testing)
|
|
28
59
|
if (this.config.responseDelay) {
|
|
29
60
|
this.app.use((req, res, next) => {
|
|
@@ -158,21 +189,59 @@ export class MockGitLabServer {
|
|
|
158
189
|
});
|
|
159
190
|
// GET /api/v4/projects/:projectId/issues - List issues
|
|
160
191
|
this.app.get('/api/v4/projects/:projectId/issues', (req, res) => {
|
|
192
|
+
const projectId = req.params.projectId;
|
|
161
193
|
res.json([
|
|
162
194
|
{
|
|
163
195
|
id: 1,
|
|
164
196
|
iid: 1,
|
|
197
|
+
project_id: projectId,
|
|
165
198
|
title: 'Test Issue 1',
|
|
199
|
+
description: 'Test issue description',
|
|
166
200
|
state: 'opened',
|
|
167
201
|
created_at: '2024-01-01T00:00:00Z',
|
|
202
|
+
updated_at: '2024-01-02T00:00:00Z',
|
|
203
|
+
closed_at: null,
|
|
204
|
+
web_url: `https://gitlab.mock/project/${projectId}/issues/1`,
|
|
168
205
|
author: {
|
|
169
206
|
id: 1,
|
|
170
207
|
username: 'test-user',
|
|
171
|
-
name: 'Test User'
|
|
172
|
-
|
|
208
|
+
name: 'Test User',
|
|
209
|
+
avatar_url: null,
|
|
210
|
+
web_url: 'https://gitlab.mock/test-user'
|
|
211
|
+
},
|
|
212
|
+
assignees: [],
|
|
213
|
+
labels: [],
|
|
214
|
+
milestone: null
|
|
173
215
|
}
|
|
174
216
|
]);
|
|
175
217
|
});
|
|
218
|
+
// GET /api/v4/projects/:projectId/issues/:issue_iid - Get single issue
|
|
219
|
+
this.app.get('/api/v4/projects/:projectId/issues/:issue_iid', (req, res) => {
|
|
220
|
+
const issueIid = parseInt(req.params.issue_iid);
|
|
221
|
+
const projectId = req.params.projectId;
|
|
222
|
+
res.json({
|
|
223
|
+
id: issueIid,
|
|
224
|
+
iid: issueIid,
|
|
225
|
+
project_id: projectId,
|
|
226
|
+
title: `Test Issue ${issueIid}`,
|
|
227
|
+
description: `Description for issue ${issueIid}`,
|
|
228
|
+
state: 'opened',
|
|
229
|
+
created_at: '2024-01-01T00:00:00Z',
|
|
230
|
+
updated_at: '2024-01-02T00:00:00Z',
|
|
231
|
+
closed_at: null,
|
|
232
|
+
web_url: `https://gitlab.mock/project/${projectId}/issues/${issueIid}`,
|
|
233
|
+
author: {
|
|
234
|
+
id: 1,
|
|
235
|
+
username: 'test-user',
|
|
236
|
+
name: 'Test User',
|
|
237
|
+
avatar_url: null,
|
|
238
|
+
web_url: 'https://gitlab.mock/test-user'
|
|
239
|
+
},
|
|
240
|
+
assignees: [],
|
|
241
|
+
labels: [],
|
|
242
|
+
milestone: null
|
|
243
|
+
});
|
|
244
|
+
});
|
|
176
245
|
// GET /api/v4/projects - List projects
|
|
177
246
|
this.app.get('/api/v4/projects', (req, res) => {
|
|
178
247
|
res.json([
|
|
@@ -15,6 +15,7 @@ export var TransportMode;
|
|
|
15
15
|
* Launch a server with specified configuration
|
|
16
16
|
*/
|
|
17
17
|
export async function launchServer(config) {
|
|
18
|
+
console.log("Launcher: launchServer function entered.");
|
|
18
19
|
const { mode, port = 3002, env = {}, timeout = 3000 } = config;
|
|
19
20
|
// Prepare environment variables based on transport mode
|
|
20
21
|
// Use same configuration pattern as existing validate-api.js
|
|
@@ -27,15 +28,11 @@ export async function launchServer(config) {
|
|
|
27
28
|
if (!GITLAB_TOKEN && !isRemoteAuth) {
|
|
28
29
|
throw new Error('GITLAB_TOKEN_TEST or GITLAB_TOKEN environment variable is required for server testing');
|
|
29
30
|
}
|
|
30
|
-
if (!TEST_PROJECT_ID && !isRemoteAuth) {
|
|
31
|
+
if (!TEST_PROJECT_ID && !isRemoteAuth && env.ENABLE_DYNAMIC_API_URL !== 'true') {
|
|
31
32
|
throw new Error('TEST_PROJECT_ID environment variable is required for server testing');
|
|
32
33
|
}
|
|
33
34
|
const serverEnv = {
|
|
34
|
-
// Add all environment variables from the current process
|
|
35
35
|
...process.env,
|
|
36
|
-
GITLAB_API_URL: `${GITLAB_API_URL}/api/v4`,
|
|
37
|
-
...(TEST_PROJECT_ID ? { GITLAB_PROJECT_ID: TEST_PROJECT_ID } : {}),
|
|
38
|
-
GITLAB_READ_ONLY_MODE: 'true', // Use read-only mode for testing
|
|
39
36
|
...env,
|
|
40
37
|
};
|
|
41
38
|
// Only set GITLAB_PERSONAL_ACCESS_TOKEN if not using remote auth
|
|
@@ -57,11 +54,16 @@ export async function launchServer(config) {
|
|
|
57
54
|
throw new Error(`${TransportMode.STDIO} mode is not supported for server testing, because it uses process communication.`);
|
|
58
55
|
}
|
|
59
56
|
const serverPath = path.resolve(process.cwd(), 'build/index.js');
|
|
57
|
+
console.log("Launcher: Spawning server process with env:", serverEnv);
|
|
58
|
+
console.log("Launcher: Spawning server process with env:", serverEnv);
|
|
60
59
|
const serverProcess = spawn('node', [serverPath], {
|
|
61
60
|
env: serverEnv,
|
|
62
61
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
62
|
+
shell: false,
|
|
63
63
|
detached: false
|
|
64
64
|
});
|
|
65
|
+
console.log(`Launcher: Server process spawned with PID: ${serverProcess.pid}`);
|
|
66
|
+
console.log("Launcher: Server process spawned.");
|
|
65
67
|
// Wait for server to start
|
|
66
68
|
await waitForServerStart(serverProcess, mode, port, timeout);
|
|
67
69
|
const instance = {
|
|
@@ -92,8 +94,16 @@ async function waitForServerStart(process, mode, port, timeout) {
|
|
|
92
94
|
}, timeout);
|
|
93
95
|
let outputBuffer = '';
|
|
94
96
|
const onData = (data) => {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
+
try {
|
|
98
|
+
const output = data.toString();
|
|
99
|
+
console.log(`[Server Output]: ${output}`);
|
|
100
|
+
outputBuffer += output;
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
console.error("Error converting server output to string:", e);
|
|
104
|
+
reject(e);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
97
107
|
// Check for server start messages
|
|
98
108
|
const startMessages = [
|
|
99
109
|
'Starting GitLab MCP Server with stdio transport',
|
|
@@ -105,8 +115,8 @@ async function waitForServerStart(process, mode, port, timeout) {
|
|
|
105
115
|
const hasStartMessage = startMessages.some(msg => outputBuffer.includes(msg));
|
|
106
116
|
if (hasStartMessage) {
|
|
107
117
|
clearTimeout(timer);
|
|
108
|
-
process.stdout?.removeListener('data', onData);
|
|
109
|
-
process.stderr?.removeListener('data', onData);
|
|
118
|
+
// process.stdout?.removeListener('data', onData);
|
|
119
|
+
// process.stderr?.removeListener('data', onData);
|
|
110
120
|
// Additional wait for HTTP servers to be fully ready
|
|
111
121
|
if (mode !== TransportMode.STDIO) {
|
|
112
122
|
setTimeout(resolve, 1000);
|
|
@@ -121,10 +131,16 @@ async function waitForServerStart(process, mode, port, timeout) {
|
|
|
121
131
|
reject(new Error(`Server process error: ${error.message}`));
|
|
122
132
|
};
|
|
123
133
|
const onExit = (code) => {
|
|
134
|
+
console.log(`[Launcher DEBUG] Process ${process.pid} exited with code ${code}`);
|
|
135
|
+
// We don't reject here immediately, we wait for 'close' to ensure streams are flushed
|
|
136
|
+
};
|
|
137
|
+
const onClose = (code) => {
|
|
138
|
+
console.log(`[Launcher DEBUG] Process ${process.pid} closed with code ${code}`);
|
|
124
139
|
clearTimeout(timer);
|
|
125
140
|
reject(new Error(`Server process exited with code ${code} before starting`));
|
|
126
141
|
};
|
|
127
142
|
process.stdout?.on('data', onData);
|
|
143
|
+
process.on('close', onClose);
|
|
128
144
|
process.stderr?.on('data', onData);
|
|
129
145
|
process.on('error', onError);
|
|
130
146
|
process.on('exit', onExit);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zereight/mcp-gitlab",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.18",
|
|
4
4
|
"description": "MCP server for using the GitLab API",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "zereight",
|
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
"build"
|
|
11
11
|
],
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/zereight/gitlab-mcp.git"
|
|
15
|
+
},
|
|
12
16
|
"publishConfig": {
|
|
13
17
|
"access": "public"
|
|
14
18
|
},
|
|
@@ -35,7 +39,6 @@
|
|
|
35
39
|
"format:check": "prettier --check \"**/*.{js,ts,json,md}\""
|
|
36
40
|
},
|
|
37
41
|
"dependencies": {
|
|
38
|
-
"@modelcontextprotocol/sdk": "^1.10.0",
|
|
39
42
|
"@types/node-fetch": "^2.6.12",
|
|
40
43
|
"express": "^5.1.0",
|
|
41
44
|
"fetch-cookie": "^3.1.0",
|
|
@@ -49,6 +52,8 @@
|
|
|
49
52
|
"pkce-challenge": "^5.0.0",
|
|
50
53
|
"socks-proxy-agent": "^8.0.5",
|
|
51
54
|
"tough-cookie": "^5.1.2",
|
|
55
|
+
"zod": "^3.24.2",
|
|
56
|
+
"@modelcontextprotocol/sdk": "^1.24.2",
|
|
52
57
|
"zod-to-json-schema": "3.24.5"
|
|
53
58
|
},
|
|
54
59
|
"devDependencies": {
|
|
@@ -62,7 +67,6 @@
|
|
|
62
67
|
"prettier": "^3.4.2",
|
|
63
68
|
"ts-node": "^10.9.2",
|
|
64
69
|
"tsx": "^4.20.5",
|
|
65
|
-
"typescript": "^5.8.2"
|
|
66
|
-
"zod": "^3.24.2"
|
|
70
|
+
"typescript": "^5.8.2"
|
|
67
71
|
}
|
|
68
72
|
}
|