@zereight/mcp-gitlab 2.1.43 → 2.1.44
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 +11 -8
- 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.43`처럼 직전 안정 버전(문서가 권장하는 버전)으로 고정하세요. 항상 최신 버전을 원하면 `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.43`. 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.43`。如果始终想使用最新版本,请改用 `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";
|
|
@@ -4391,7 +4392,7 @@ async function publishDraftNote(projectId, mergeRequestIid, draftNoteId) {
|
|
|
4391
4392
|
};
|
|
4392
4393
|
}
|
|
4393
4394
|
}
|
|
4394
|
-
async function
|
|
4395
|
+
async function fetchGitLabVersionMetadata() {
|
|
4395
4396
|
try {
|
|
4396
4397
|
const response = await fetch(`${getEffectiveApiUrl()}/version`, {
|
|
4397
4398
|
...getFetchConfig(),
|
|
@@ -4399,18 +4400,16 @@ async function fetchGitLabInstanceVersion() {
|
|
|
4399
4400
|
if (!response.ok)
|
|
4400
4401
|
return null;
|
|
4401
4402
|
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;
|
|
4403
|
+
return parseGitLabVersionApiResponse(data);
|
|
4409
4404
|
}
|
|
4410
4405
|
catch {
|
|
4411
4406
|
return null;
|
|
4412
4407
|
}
|
|
4413
4408
|
}
|
|
4409
|
+
async function fetchGitLabInstanceVersion() {
|
|
4410
|
+
const metadata = await fetchGitLabVersionMetadata();
|
|
4411
|
+
return metadata?.version ?? null;
|
|
4412
|
+
}
|
|
4414
4413
|
/**
|
|
4415
4414
|
* Publish all draft notes for a merge request
|
|
4416
4415
|
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
@@ -9339,6 +9338,9 @@ async function handleToolCall(params) {
|
|
|
9339
9338
|
const jobResponse = await fetch(jobUrl.toString(), getFetchConfig());
|
|
9340
9339
|
authenticated = jobResponse.ok;
|
|
9341
9340
|
}
|
|
9341
|
+
const versionMetadata = authenticated
|
|
9342
|
+
? await fetchGitLabVersionMetadata()
|
|
9343
|
+
: null;
|
|
9342
9344
|
return {
|
|
9343
9345
|
content: [
|
|
9344
9346
|
{
|
|
@@ -9347,6 +9349,7 @@ async function handleToolCall(params) {
|
|
|
9347
9349
|
status: authenticated ? "ok" : "error",
|
|
9348
9350
|
authenticated,
|
|
9349
9351
|
gitlab_url: getEffectiveApiUrl(),
|
|
9352
|
+
...(versionMetadata ?? {}),
|
|
9350
9353
|
}),
|
|
9351
9354
|
},
|
|
9352
9355
|
],
|
|
@@ -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