gitlab-mcp 1.0.0 → 1.1.0
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/package.json +9 -1
- package/src/http.ts +17 -2
- package/src/lib/session-capacity.ts +14 -0
- package/src/tools/gitlab.ts +8 -1
- package/tests/gitlab-client.test.ts +1 -3
- package/tests/integration/agent-loop.integration.test.ts +12 -6
- package/tests/request-runtime.test.ts +2 -2
- package/tests/session-capacity.test.ts +49 -0
- package/tests/upload-reference.test.ts +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gitlab-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "A MCP server for GitLab",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@10.28.1",
|
|
@@ -77,6 +77,14 @@
|
|
|
77
77
|
"path": "cz-conventional-changelog"
|
|
78
78
|
}
|
|
79
79
|
},
|
|
80
|
+
"keywords": [
|
|
81
|
+
"gitlab",
|
|
82
|
+
"mcp",
|
|
83
|
+
"ai",
|
|
84
|
+
"agent",
|
|
85
|
+
"tool",
|
|
86
|
+
"automation"
|
|
87
|
+
],
|
|
80
88
|
"author": "unadlib",
|
|
81
89
|
"license": "MIT"
|
|
82
90
|
}
|
package/src/http.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { configureNetworkRuntime } from "./lib/network.js";
|
|
|
15
15
|
import { OutputFormatter } from "./lib/output.js";
|
|
16
16
|
import { ToolPolicyEngine } from "./lib/policy.js";
|
|
17
17
|
import { GitLabRequestRuntime } from "./lib/request-runtime.js";
|
|
18
|
+
import { hasReachedSessionCapacity } from "./lib/session-capacity.js";
|
|
18
19
|
import { createMcpServer } from "./server/build-server.js";
|
|
19
20
|
import type { AppContext } from "./types/context.js";
|
|
20
21
|
|
|
@@ -99,7 +100,14 @@ if (env.SSE) {
|
|
|
99
100
|
const parsedAuth = parseRequestAuth(req);
|
|
100
101
|
const fallbackToken = env.REMOTE_AUTHORIZATION ? undefined : env.GITLAB_PERSONAL_ACCESS_TOKEN;
|
|
101
102
|
|
|
102
|
-
if (
|
|
103
|
+
if (
|
|
104
|
+
hasReachedSessionCapacity({
|
|
105
|
+
streamableSessions: sessions.size,
|
|
106
|
+
pendingSessions: pendingSessions.size,
|
|
107
|
+
sseSessions: sseSessions.size,
|
|
108
|
+
maxSessions: env.MAX_SESSIONS
|
|
109
|
+
})
|
|
110
|
+
) {
|
|
103
111
|
res.status(503).send(`Maximum ${env.MAX_SESSIONS} concurrent sessions reached`);
|
|
104
112
|
return;
|
|
105
113
|
}
|
|
@@ -243,7 +251,14 @@ app.all("/mcp", async (req, res) => {
|
|
|
243
251
|
return;
|
|
244
252
|
}
|
|
245
253
|
|
|
246
|
-
if (
|
|
254
|
+
if (
|
|
255
|
+
hasReachedSessionCapacity({
|
|
256
|
+
streamableSessions: sessions.size,
|
|
257
|
+
pendingSessions: pendingSessions.size,
|
|
258
|
+
sseSessions: sseSessions.size,
|
|
259
|
+
maxSessions: env.MAX_SESSIONS
|
|
260
|
+
})
|
|
261
|
+
) {
|
|
247
262
|
res.status(503).json({
|
|
248
263
|
jsonrpc: "2.0",
|
|
249
264
|
error: {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface SessionCapacityInput {
|
|
2
|
+
streamableSessions: number;
|
|
3
|
+
pendingSessions: number;
|
|
4
|
+
sseSessions: number;
|
|
5
|
+
maxSessions: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function getTotalSessions(input: SessionCapacityInput): number {
|
|
9
|
+
return input.streamableSessions + input.pendingSessions + input.sseSessions;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function hasReachedSessionCapacity(input: SessionCapacityInput): boolean {
|
|
13
|
+
return getTotalSessions(input) >= input.maxSessions;
|
|
14
|
+
}
|
package/src/tools/gitlab.ts
CHANGED
|
@@ -2801,7 +2801,14 @@ export function parseProjectUploadReference(
|
|
|
2801
2801
|
return undefined;
|
|
2802
2802
|
}
|
|
2803
2803
|
|
|
2804
|
-
|
|
2804
|
+
let filename: string;
|
|
2805
|
+
|
|
2806
|
+
try {
|
|
2807
|
+
filename = decodeURIComponent(filenameParts.join("/"));
|
|
2808
|
+
} catch {
|
|
2809
|
+
return undefined;
|
|
2810
|
+
}
|
|
2811
|
+
|
|
2805
2812
|
if (!filename) {
|
|
2806
2813
|
return undefined;
|
|
2807
2814
|
}
|
|
@@ -421,9 +421,7 @@ describe("GitLabClient", () => {
|
|
|
421
421
|
await client.listProjects();
|
|
422
422
|
await client.listProjects();
|
|
423
423
|
|
|
424
|
-
const urls = fetchMock.mock.calls.map(
|
|
425
|
-
(call: [URL | string, RequestInit]) => new URL(String(call[0])).origin
|
|
426
|
-
);
|
|
424
|
+
const urls = fetchMock.mock.calls.map((call) => new URL(String(call[0])).origin);
|
|
427
425
|
|
|
428
426
|
expect(urls[0]).toBe("https://a.example.com");
|
|
429
427
|
expect(urls[1]).toBe("https://b.example.com");
|
|
@@ -53,7 +53,13 @@ class ScriptedLLM implements LLM {
|
|
|
53
53
|
if (this.callIndex >= this.script.length) {
|
|
54
54
|
throw new Error(`ScriptedLLM: script exhausted after ${this.callIndex} calls`);
|
|
55
55
|
}
|
|
56
|
-
|
|
56
|
+
const response = this.script[this.callIndex];
|
|
57
|
+
if (!response) {
|
|
58
|
+
throw new Error(`ScriptedLLM: missing response at index ${this.callIndex}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
this.callIndex += 1;
|
|
62
|
+
return response;
|
|
57
63
|
}
|
|
58
64
|
}
|
|
59
65
|
|
|
@@ -293,7 +299,7 @@ describe("Agent Loop Integration (ScriptedLLM + MCP server)", () => {
|
|
|
293
299
|
|
|
294
300
|
// Verify tool was called
|
|
295
301
|
expect(result.toolCalls).toHaveLength(1);
|
|
296
|
-
expect(result.toolCalls[0]
|
|
302
|
+
expect(result.toolCalls[0]!.name).toBe("health_check");
|
|
297
303
|
|
|
298
304
|
// Verify tool result was successful
|
|
299
305
|
const toolResult = result.toolResults[0] as { isError?: boolean };
|
|
@@ -327,7 +333,7 @@ describe("Agent Loop Integration (ScriptedLLM + MCP server)", () => {
|
|
|
327
333
|
});
|
|
328
334
|
|
|
329
335
|
expect(result.toolCalls).toHaveLength(1);
|
|
330
|
-
expect(result.toolCalls[0]
|
|
336
|
+
expect(result.toolCalls[0]!.name).toBe("gitlab_list_projects");
|
|
331
337
|
|
|
332
338
|
// Verify the mocked gitlab client was called
|
|
333
339
|
expect(context.gitlab.listProjects).toHaveBeenCalled();
|
|
@@ -390,9 +396,9 @@ describe("Agent Loop Integration (ScriptedLLM + MCP server)", () => {
|
|
|
390
396
|
});
|
|
391
397
|
|
|
392
398
|
expect(result.toolCalls).toHaveLength(2);
|
|
393
|
-
expect(result.toolCalls[0]
|
|
394
|
-
expect(result.toolCalls[1]
|
|
395
|
-
expect(result.toolCalls[1]
|
|
399
|
+
expect(result.toolCalls[0]!.name).toBe("gitlab_list_projects");
|
|
400
|
+
expect(result.toolCalls[1]!.name).toBe("gitlab_get_project");
|
|
401
|
+
expect(result.toolCalls[1]!.arguments).toEqual({ project_id: "group/project-alpha" });
|
|
396
402
|
|
|
397
403
|
expect(context.gitlab.listProjects).toHaveBeenCalled();
|
|
398
404
|
expect(context.gitlab.getProject).toHaveBeenCalledWith("group/project-alpha");
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* For deeper testing we extract testable logic patterns.
|
|
7
7
|
*/
|
|
8
8
|
import { describe, expect, it } from "vitest";
|
|
9
|
-
import os from "node:os";
|
|
10
|
-
import path from "node:path";
|
|
9
|
+
import * as os from "node:os";
|
|
10
|
+
import * as path from "node:path";
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Replicate the parseTokenOutput logic for testing.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { getTotalSessions, hasReachedSessionCapacity } from "../src/lib/session-capacity.js";
|
|
4
|
+
|
|
5
|
+
describe("session capacity helpers", () => {
|
|
6
|
+
it("counts streamable, pending, and SSE sessions together", () => {
|
|
7
|
+
expect(
|
|
8
|
+
getTotalSessions({
|
|
9
|
+
streamableSessions: 2,
|
|
10
|
+
pendingSessions: 1,
|
|
11
|
+
sseSessions: 3,
|
|
12
|
+
maxSessions: 10
|
|
13
|
+
})
|
|
14
|
+
).toBe(6);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("treats capacity as reached when total equals max", () => {
|
|
18
|
+
expect(
|
|
19
|
+
hasReachedSessionCapacity({
|
|
20
|
+
streamableSessions: 1,
|
|
21
|
+
pendingSessions: 1,
|
|
22
|
+
sseSessions: 1,
|
|
23
|
+
maxSessions: 3
|
|
24
|
+
})
|
|
25
|
+
).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("treats capacity as reached when total exceeds max", () => {
|
|
29
|
+
expect(
|
|
30
|
+
hasReachedSessionCapacity({
|
|
31
|
+
streamableSessions: 2,
|
|
32
|
+
pendingSessions: 1,
|
|
33
|
+
sseSessions: 2,
|
|
34
|
+
maxSessions: 4
|
|
35
|
+
})
|
|
36
|
+
).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("does not reach capacity when total is below max", () => {
|
|
40
|
+
expect(
|
|
41
|
+
hasReachedSessionCapacity({
|
|
42
|
+
streamableSessions: 1,
|
|
43
|
+
pendingSessions: 0,
|
|
44
|
+
sseSessions: 1,
|
|
45
|
+
maxSessions: 3
|
|
46
|
+
})
|
|
47
|
+
).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -72,6 +72,10 @@ describe("parseProjectUploadReference", () => {
|
|
|
72
72
|
expect(parseProjectUploadReference("not a url at all")).toBeUndefined();
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
+
it("returns undefined for malformed percent-encoding", () => {
|
|
76
|
+
expect(parseProjectUploadReference("/uploads/abc123/%E0%A4%A")).toBeUndefined();
|
|
77
|
+
});
|
|
78
|
+
|
|
75
79
|
it("handles upload path without leading slash", () => {
|
|
76
80
|
// This depends on implementation, but should handle gracefully
|
|
77
81
|
const result = parseProjectUploadReference("uploads/abc123/file.txt");
|