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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitlab-mcp",
3
- "version": "1.0.0",
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 (sessions.size + pendingSessions.size + sseSessions.size >= env.MAX_SESSIONS) {
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 (sessions.size + pendingSessions.size >= env.MAX_SESSIONS) {
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
+ }
@@ -2801,7 +2801,14 @@ export function parseProjectUploadReference(
2801
2801
  return undefined;
2802
2802
  }
2803
2803
 
2804
- const filename = decodeURIComponent(filenameParts.join("/"));
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
- return this.script[this.callIndex++];
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].name).toBe("health_check");
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].name).toBe("gitlab_list_projects");
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].name).toBe("gitlab_list_projects");
394
- expect(result.toolCalls[1].name).toBe("gitlab_get_project");
395
- expect(result.toolCalls[1].arguments).toEqual({ project_id: "group/project-alpha" });
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");