@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 +1 -1
- package/README.md +2 -2
- package/README.zh-CN.md +1 -1
- package/build/index.js +29 -9
- package/build/test/test-health-check.js +106 -0
- package/build/test/utils/gitlab-instance-version.test.js +32 -0
- package/build/test/utils/mock-gitlab-server.js +8 -0
- package/build/tools/registry.js +1 -1
- package/build/utils/gitlab-instance-version.js +16 -0
- package/package.json +1 -1
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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:
|
|
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;
|
package/build/tools/registry.js
CHANGED
|
@@ -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