@zereight/mcp-gitlab 2.1.43 → 2.1.45

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.ko.md CHANGED
@@ -80,7 +80,7 @@ npm install -g @zereight/mcp-gitlab
80
80
 
81
81
  예시는 기존 `mcp-gitlab`보다 충돌 가능성이 낮은 `zereight-mcp-gitlab` 별칭을 사용합니다. MCP 클라이언트가 찾지 못하면 `which zereight-mcp-gitlab`의 절대 경로를 사용하세요.
82
82
 
83
- 전역 설치를 쓰지 않으려면 `npx -y @zereight/mcp-gitlab@2.1.42`처럼 직전 안정 버전(문서가 권장하는 버전)으로 고정하세요. 항상 최신 버전을 원하면 `npx -y @zereight/mcp-gitlab@latest`를 사용하세요. 새 버전이 나오면 서버가 시작 시 stderr로 알려줍니다(`GITLAB_DISABLE_VERSION_CHECK=true`로 비활성화 가능).
83
+ 전역 설치를 쓰지 않으려면 `npx -y @zereight/mcp-gitlab@2.1.44`처럼 직전 안정 버전(문서가 권장하는 버전)으로 고정하세요. 항상 최신 버전을 원하면 `npx -y @zereight/mcp-gitlab@latest`를 사용하세요. 새 버전이 나오면 서버가 시작 시 stderr로 알려줍니다(`GITLAB_DISABLE_VERSION_CHECK=true`로 비활성화 가능).
84
84
 
85
85
  #### CLI 인자 사용하기(환경 변수 문제가 있는 클라이언트용)
86
86
 
package/README.md CHANGED
@@ -82,7 +82,7 @@ npm install -g @zereight/mcp-gitlab
82
82
 
83
83
  The examples use `zereight-mcp-gitlab`, a less collision-prone alias for the legacy `mcp-gitlab` binary. If your MCP client cannot find it, use the absolute path from `which zereight-mcp-gitlab`.
84
84
 
85
- No global install? Pin `npx` to the previous stable release (the version these docs recommend), for example `npx -y @zereight/mcp-gitlab@2.1.42`. If you always want the newest release, use `npx -y @zereight/mcp-gitlab@latest` instead. The server prints a notice to stderr on startup when a newer version is available (disable with `GITLAB_DISABLE_VERSION_CHECK=true`).
85
+ No global install? Pin `npx` to the previous stable release (the version these docs recommend), for example `npx -y @zereight/mcp-gitlab@2.1.44`. If you always want the newest release, use `npx -y @zereight/mcp-gitlab@latest` instead. The server prints a notice to stderr on startup when a newer version is available (disable with `GITLAB_DISABLE_VERSION_CHECK=true`).
86
86
 
87
87
  #### Using CLI Arguments (for clients with env var issues)
88
88
 
@@ -700,7 +700,7 @@ Register the skill directory in your AI client to get optimal tool usage guidanc
700
700
  156. `list_group_iterations` - List group iterations with filtering options
701
701
  157. `upload_markdown` - Upload a file to a GitLab project for use in markdown content
702
702
  158. `download_attachment` - Download an uploaded file from a GitLab project by secret and filename
703
- 159. `health_check` - Verify server status and authentication
703
+ 159. `health_check` - Verify server status and authentication; when authenticated, reports GitLab instance version from `/api/v4/version` (`version`, `revision`, `enterprise`)
704
704
  160. `list_events` - List all events for the currently authenticated user
705
705
  161. `get_project_events` - List all visible events for a specified project
706
706
  162. `list_releases` - List all releases for a project
package/README.zh-CN.md CHANGED
@@ -80,7 +80,7 @@ npm install -g @zereight/mcp-gitlab
80
80
 
81
81
  示例使用 `zereight-mcp-gitlab`,这是比旧的 `mcp-gitlab` 更不容易冲突的别名。如果 MCP 客户端找不到它,请使用 `which zereight-mcp-gitlab` 输出的绝对路径。
82
82
 
83
- 如果不想全局安装,请将 `npx` 固定到上一个稳定版本(即文档推荐的版本),例如 `npx -y @zereight/mcp-gitlab@2.1.42`。如果始终想使用最新版本,请改用 `npx -y @zereight/mcp-gitlab@latest`。有新版本发布时,服务器会在启动时通过 stderr 提示(可用 `GITLAB_DISABLE_VERSION_CHECK=true` 关闭)。
83
+ 如果不想全局安装,请将 `npx` 固定到上一个稳定版本(即文档推荐的版本),例如 `npx -y @zereight/mcp-gitlab@2.1.44`。如果始终想使用最新版本,请改用 `npx -y @zereight/mcp-gitlab@latest`。有新版本发布时,服务器会在启动时通过 stderr 提示(可用 `GITLAB_DISABLE_VERSION_CHECK=true` 关闭)。
84
84
 
85
85
  #### 使用 CLI 参数(适用于环境变量有问题的客户端)
86
86
 
package/build/index.js CHANGED
@@ -91,6 +91,7 @@ import { resolveNestedWikiUpdateTitle } from "./utils/wiki-title.js";
91
91
  import { redactSensitiveGitLabFields } from "./utils/redact-sensitive.js";
92
92
  import { checkForNewVersion } from "./utils/version-check.js";
93
93
  import { assertGitLabVersionAtLeast } from "./utils/gitlab-version-gate.js";
94
+ import { parseGitLabVersionApiResponse, } from "./utils/gitlab-instance-version.js";
94
95
  import { buildBulkPublishDraftNotesBody, needsGitLab19_2BulkPublish, } from "./utils/bulk-publish-options.js";
95
96
  import { cleanMutuallyExclusiveIdUsernameOptions, LIST_MERGE_REQUESTS_ID_USERNAME_PAIRS, sanitizeToolArguments, } from "./utils/tool-args.js";
96
97
  import { parseSearchReplaceBlocks, applySearchReplace, applyUnifiedDiff, } from "./utils/patch-helper.js";
@@ -537,6 +538,14 @@ function validateConfiguration() {
537
538
  errors.push(`MAX_REQUESTS_PER_MINUTE must be between 1 and 1000, got: ${maxReqStr}`);
538
539
  }
539
540
  }
541
+ // Validate OAUTH_REGISTER_RATE_LIMIT_PER_HOUR
542
+ const registerRateLimitStr = process.env.OAUTH_REGISTER_RATE_LIMIT_PER_HOUR;
543
+ if (registerRateLimitStr) {
544
+ const limit = Number(registerRateLimitStr);
545
+ if (!Number.isInteger(limit) || limit < 1 || limit > 1000) {
546
+ errors.push(`OAUTH_REGISTER_RATE_LIMIT_PER_HOUR must be an integer between 1 and 1000, got: ${registerRateLimitStr}`);
547
+ }
548
+ }
540
549
  // Validate PORT
541
550
  const portStr = getConfig("port", "PORT");
542
551
  if (portStr) {
@@ -4391,7 +4400,7 @@ async function publishDraftNote(projectId, mergeRequestIid, draftNoteId) {
4391
4400
  };
4392
4401
  }
4393
4402
  }
4394
- async function fetchGitLabInstanceVersion() {
4403
+ async function fetchGitLabVersionMetadata() {
4395
4404
  try {
4396
4405
  const response = await fetch(`${getEffectiveApiUrl()}/version`, {
4397
4406
  ...getFetchConfig(),
@@ -4399,18 +4408,16 @@ async function fetchGitLabInstanceVersion() {
4399
4408
  if (!response.ok)
4400
4409
  return null;
4401
4410
  const data = await response.json();
4402
- if (typeof data === "object" &&
4403
- data !== null &&
4404
- "version" in data &&
4405
- typeof data.version === "string") {
4406
- return data.version;
4407
- }
4408
- return null;
4411
+ return parseGitLabVersionApiResponse(data);
4409
4412
  }
4410
4413
  catch {
4411
4414
  return null;
4412
4415
  }
4413
4416
  }
4417
+ async function fetchGitLabInstanceVersion() {
4418
+ const metadata = await fetchGitLabVersionMetadata();
4419
+ return metadata?.version ?? null;
4420
+ }
4414
4421
  /**
4415
4422
  * Publish all draft notes for a merge request
4416
4423
  * @param {string} projectId - The ID or URL-encoded path of the project
@@ -9339,6 +9346,9 @@ async function handleToolCall(params) {
9339
9346
  const jobResponse = await fetch(jobUrl.toString(), getFetchConfig());
9340
9347
  authenticated = jobResponse.ok;
9341
9348
  }
9349
+ const versionMetadata = authenticated
9350
+ ? await fetchGitLabVersionMetadata()
9351
+ : null;
9342
9352
  return {
9343
9353
  content: [
9344
9354
  {
@@ -9347,6 +9357,7 @@ async function handleToolCall(params) {
9347
9357
  status: authenticated ? "ok" : "error",
9348
9358
  authenticated,
9349
9359
  gitlab_url: getEffectiveApiUrl(),
9360
+ ...(versionMetadata ?? {}),
9350
9361
  }),
9351
9362
  },
9352
9363
  ],
@@ -10163,6 +10174,15 @@ async function startStreamableHTTPServer() {
10163
10174
  // ERR_ERL_INVALID_IP_ADDRESS. Strip the port first, then delegate to
10164
10175
  // ipKeyGenerator for correct IPv6 subnet handling.
10165
10176
  const rateLimitOptions = { keyGenerator: mcpRateLimitKeyGenerator };
10177
+ const parsedRegisterLimit = Number(process.env.OAUTH_REGISTER_RATE_LIMIT_PER_HOUR);
10178
+ const OAUTH_REGISTER_RATE_LIMIT_PER_HOUR = Number.isInteger(parsedRegisterLimit) && parsedRegisterLimit >= 1
10179
+ ? parsedRegisterLimit
10180
+ : 20;
10181
+ const clientRegistrationRateLimitOptions = {
10182
+ ...rateLimitOptions,
10183
+ windowMs: 60 * 60 * 1000, // 1 hour
10184
+ max: OAUTH_REGISTER_RATE_LIMIT_PER_HOUR,
10185
+ };
10166
10186
  app.use(mcpAuthRouter({
10167
10187
  provider: oauthProvider,
10168
10188
  issuerUrl,
@@ -10172,7 +10192,7 @@ async function startStreamableHTTPServer() {
10172
10192
  authorizationOptions: { rateLimit: rateLimitOptions },
10173
10193
  tokenOptions: { rateLimit: rateLimitOptions },
10174
10194
  revocationOptions: { rateLimit: rateLimitOptions },
10175
- clientRegistrationOptions: { rateLimit: rateLimitOptions },
10195
+ clientRegistrationOptions: { rateLimit: clientRegistrationRateLimitOptions },
10176
10196
  }));
10177
10197
  // Expose provider so the /mcp route middleware can reference it
10178
10198
  app._mcpOAuthProvider = oauthProvider;
@@ -0,0 +1,106 @@
1
+ import { describe, test } 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
+ function createMockGitLabServer(port) {
7
+ return new MockGitLabServer({
8
+ port,
9
+ validTokens: [MOCK_TOKEN],
10
+ });
11
+ }
12
+ function baseEnv(mockGitLabUrl) {
13
+ return {
14
+ GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
15
+ GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN,
16
+ GITLAB_TOOLSETS: "projects",
17
+ };
18
+ }
19
+ async function callHealthCheckAsync(env) {
20
+ return new Promise((resolve, reject) => {
21
+ const proc = spawn("node", ["build/index.js"], {
22
+ stdio: ["pipe", "pipe", "pipe"],
23
+ env: {
24
+ ...process.env,
25
+ ...env,
26
+ USE_PIPELINE: "true",
27
+ },
28
+ });
29
+ let output = "";
30
+ let errorOutput = "";
31
+ proc.stdout?.on("data", (chunk) => (output += chunk));
32
+ proc.stderr?.on("data", (chunk) => (errorOutput += chunk));
33
+ proc.on("close", code => {
34
+ if (code !== 0) {
35
+ return reject(new Error(`Process exited with code ${code}: ${errorOutput}`));
36
+ }
37
+ const line = output.split("\n").find(entry => entry.startsWith("{"));
38
+ if (!line) {
39
+ return reject(new Error("No JSON output found"));
40
+ }
41
+ try {
42
+ const response = JSON.parse(line);
43
+ if (response.error) {
44
+ reject(response.error);
45
+ return;
46
+ }
47
+ const content = response.result?.content?.[0]?.text;
48
+ if (!content) {
49
+ reject(new Error("No tool result content"));
50
+ return;
51
+ }
52
+ resolve(JSON.parse(content));
53
+ }
54
+ catch (error) {
55
+ reject(error);
56
+ }
57
+ });
58
+ proc.stdin?.end(JSON.stringify({
59
+ jsonrpc: "2.0",
60
+ id: 1,
61
+ method: "tools/call",
62
+ params: { name: "health_check", arguments: {} },
63
+ }) + "\n");
64
+ });
65
+ }
66
+ describe("When health_check runs", () => {
67
+ describe("with authenticated mock GitLab", () => {
68
+ test("should include GitLab instance version metadata", async () => {
69
+ const mockPort = await findMockServerPort();
70
+ const mockGitLab = createMockGitLabServer(mockPort);
71
+ await mockGitLab.start();
72
+ try {
73
+ const result = await callHealthCheckAsync(baseEnv(mockGitLab.getUrl()));
74
+ assert.equal(result.status, "ok");
75
+ assert.equal(result.authenticated, true);
76
+ assert.equal(result.version, "18.3.1-ee");
77
+ assert.equal(result.revision, "abc1234");
78
+ assert.equal(result.enterprise, true);
79
+ }
80
+ finally {
81
+ await mockGitLab.stop();
82
+ }
83
+ });
84
+ });
85
+ describe("when GitLab /version lookup fails", () => {
86
+ test("should keep health check ok without version fields", async () => {
87
+ const mockPort = await findMockServerPort();
88
+ const mockGitLab = createMockGitLabServer(mockPort);
89
+ mockGitLab.addMockHandler("get", "/version", (_req, res) => {
90
+ res.status(500).json({ message: "version unavailable" });
91
+ });
92
+ await mockGitLab.start();
93
+ try {
94
+ const result = await callHealthCheckAsync(baseEnv(mockGitLab.getUrl()));
95
+ assert.equal(result.status, "ok");
96
+ assert.equal(result.authenticated, true);
97
+ assert.equal("version" in result, false);
98
+ assert.equal("revision" in result, false);
99
+ assert.equal("enterprise" in result, false);
100
+ }
101
+ finally {
102
+ await mockGitLab.stop();
103
+ }
104
+ });
105
+ });
106
+ });
@@ -0,0 +1,32 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, test } from "node:test";
3
+ import { parseGitLabVersionApiResponse } from "../../utils/gitlab-instance-version.js";
4
+ describe("When parseGitLabVersionApiResponse runs", () => {
5
+ describe("with a full GitLab /version payload", () => {
6
+ test("should return version, revision, and enterprise", () => {
7
+ assert.deepEqual(parseGitLabVersionApiResponse({
8
+ version: "18.3.1-ee",
9
+ revision: "abc1234",
10
+ enterprise: true,
11
+ }), {
12
+ version: "18.3.1-ee",
13
+ revision: "abc1234",
14
+ enterprise: true,
15
+ });
16
+ });
17
+ });
18
+ describe("with only a version field", () => {
19
+ test("should return version without optional fields", () => {
20
+ assert.deepEqual(parseGitLabVersionApiResponse({ version: "17.5.0" }), {
21
+ version: "17.5.0",
22
+ });
23
+ });
24
+ });
25
+ describe("with invalid payloads", () => {
26
+ test("should return null", () => {
27
+ assert.equal(parseGitLabVersionApiResponse(null), null);
28
+ assert.equal(parseGitLabVersionApiResponse({}), null);
29
+ assert.equal(parseGitLabVersionApiResponse({ version: 18 }), null);
30
+ });
31
+ });
32
+ });
@@ -150,6 +150,14 @@ export class MockGitLabServer {
150
150
  state: "active",
151
151
  });
152
152
  });
153
+ // GET /api/v4/version - GitLab instance version
154
+ this.app.get("/api/v4/version", (_req, res) => {
155
+ res.json({
156
+ version: "18.3.1-ee",
157
+ revision: "abc1234",
158
+ enterprise: true,
159
+ });
160
+ });
153
161
  // GET /api/v4/projects/:projectId - Get project
154
162
  this.app.get("/api/v4/projects/:projectId", (req, res) => {
155
163
  const projectId = req.params.projectId;
@@ -813,7 +813,7 @@ export const allTools = [
813
813
  },
814
814
  {
815
815
  name: "health_check",
816
- description: "Verify server status and authentication",
816
+ description: "Verify server status and authentication. When authenticated, also reports the GitLab instance version from GET /api/v4/version (version, revision, enterprise). Version lookup failures do not fail the health check — those fields are omitted.",
817
817
  inputSchema: toJSONSchema(HealthCheckSchema),
818
818
  },
819
819
  {
@@ -0,0 +1,16 @@
1
+ export function parseGitLabVersionApiResponse(data) {
2
+ if (typeof data !== "object" || data === null || !("version" in data)) {
3
+ return null;
4
+ }
5
+ if (typeof data.version !== "string") {
6
+ return null;
7
+ }
8
+ const metadata = { version: data.version };
9
+ if ("revision" in data && typeof data.revision === "string") {
10
+ metadata.revision = data.revision;
11
+ }
12
+ if ("enterprise" in data && typeof data.enterprise === "boolean") {
13
+ metadata.enterprise = data.enterprise;
14
+ }
15
+ return metadata;
16
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zereight/mcp-gitlab",
3
- "version": "2.1.43",
3
+ "version": "2.1.45",
4
4
  "mcpName": "io.github.zereight/gitlab-mcp",
5
5
  "description": "GitLab MCP server for projects, merge requests, issues, pipelines, wiki, releases, and more",
6
6
  "keywords": [