@zereight/mcp-gitlab 2.0.30 → 2.0.32
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.md +8 -8
- package/build/index.js +614 -24
- package/build/oauth.js +16 -4
- package/build/schemas.js +244 -12
- package/build/test/schema-tests.js +311 -0
- package/build/test/test-deployment-tools.js +366 -0
- package/build/test/test-job-artifacts.js +194 -0
- package/build/test/test-merge-request-approval-state-tools.js +171 -0
- package/build/test/test-toolset-filtering.js +7 -6
- package/package.json +3 -2
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { after, before, 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-approval";
|
|
6
|
+
const TEST_PROJECT_ID = "123";
|
|
7
|
+
const TEST_MR_IID_WITH_FALLBACK = "88";
|
|
8
|
+
const TEST_MR_IID_WITH_APPROVAL_STATE = "89";
|
|
9
|
+
async function callTool(toolName, args, env) {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
const proc = spawn("node", ["build/index.js"], {
|
|
12
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
13
|
+
env: {
|
|
14
|
+
...process.env,
|
|
15
|
+
...env,
|
|
16
|
+
GITLAB_READ_ONLY_MODE: "true",
|
|
17
|
+
USE_PIPELINE: "true",
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
let output = "";
|
|
21
|
+
let errorOutput = "";
|
|
22
|
+
proc.stdout?.on("data", (d) => (output += d));
|
|
23
|
+
proc.stderr?.on("data", (d) => (errorOutput += d));
|
|
24
|
+
proc.on("close", (code) => {
|
|
25
|
+
if (code !== 0) {
|
|
26
|
+
return reject(new Error(`Process exited with code ${code}: ${errorOutput}`));
|
|
27
|
+
}
|
|
28
|
+
const line = output.split("\n").find(l => l.startsWith("{"));
|
|
29
|
+
if (!line) {
|
|
30
|
+
return reject(new Error("No JSON output found"));
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const response = JSON.parse(line);
|
|
34
|
+
if (response.error) {
|
|
35
|
+
reject(response.error);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
const content = response.result?.content?.[0]?.text;
|
|
39
|
+
if (content) {
|
|
40
|
+
try {
|
|
41
|
+
resolve(JSON.parse(content));
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
resolve(content);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
resolve(response.result);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
reject(error);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
proc.stdin?.end(JSON.stringify({
|
|
57
|
+
jsonrpc: "2.0",
|
|
58
|
+
id: 1,
|
|
59
|
+
method: "tools/call",
|
|
60
|
+
params: { name: toolName, arguments: args },
|
|
61
|
+
}) + "\n");
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
describe("merge request approval state tools", () => {
|
|
65
|
+
let mockGitLab;
|
|
66
|
+
let mockGitLabUrl;
|
|
67
|
+
before(async () => {
|
|
68
|
+
const mockPort = await findMockServerPort(9400);
|
|
69
|
+
mockGitLab = new MockGitLabServer({
|
|
70
|
+
port: mockPort,
|
|
71
|
+
validTokens: [MOCK_TOKEN],
|
|
72
|
+
});
|
|
73
|
+
mockGitLab.addMockHandler("get", `/projects/${TEST_PROJECT_ID}/merge_requests/${TEST_MR_IID_WITH_FALLBACK}/approval_state`, (_req, res) => {
|
|
74
|
+
res.status(404).json({ error: "404 Not Found" });
|
|
75
|
+
});
|
|
76
|
+
mockGitLab.addMockHandler("get", `/projects/${TEST_PROJECT_ID}/merge_requests/${TEST_MR_IID_WITH_FALLBACK}/approvals`, (_req, res) => {
|
|
77
|
+
res.json({
|
|
78
|
+
approved: true,
|
|
79
|
+
user_has_approved: false,
|
|
80
|
+
user_can_approve: true,
|
|
81
|
+
approved_by: [
|
|
82
|
+
{
|
|
83
|
+
user: {
|
|
84
|
+
id: "35",
|
|
85
|
+
username: "sergey.kravchenya",
|
|
86
|
+
name: "Sergey Kravchenya",
|
|
87
|
+
state: "active",
|
|
88
|
+
avatar_url: "https://gitlab.mock/uploads/avatar.png",
|
|
89
|
+
web_url: "https://gitlab.mock/sergey.kravchenya",
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
mockGitLab.addMockHandler("get", `/projects/${TEST_PROJECT_ID}/merge_requests/${TEST_MR_IID_WITH_APPROVAL_STATE}/approval_state`, (_req, res) => {
|
|
96
|
+
res.json({
|
|
97
|
+
approval_rules_overwritten: false,
|
|
98
|
+
rules: [
|
|
99
|
+
{
|
|
100
|
+
id: "101",
|
|
101
|
+
name: "Default rule",
|
|
102
|
+
rule_type: "regular",
|
|
103
|
+
approvals_required: 1,
|
|
104
|
+
approved: true,
|
|
105
|
+
approved_by: [
|
|
106
|
+
{
|
|
107
|
+
id: "35",
|
|
108
|
+
username: "sergey.kravchenya",
|
|
109
|
+
name: "Sergey Kravchenya",
|
|
110
|
+
state: "active",
|
|
111
|
+
avatar_url: "https://gitlab.mock/uploads/avatar.png",
|
|
112
|
+
web_url: "https://gitlab.mock/sergey.kravchenya",
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
id: "102",
|
|
118
|
+
name: "Code owners",
|
|
119
|
+
rule_type: "code_owner",
|
|
120
|
+
approvals_required: 1,
|
|
121
|
+
approved: true,
|
|
122
|
+
approved_by: [
|
|
123
|
+
{
|
|
124
|
+
id: "35",
|
|
125
|
+
username: "sergey.kravchenya",
|
|
126
|
+
name: "Sergey Kravchenya",
|
|
127
|
+
state: "active",
|
|
128
|
+
avatar_url: "https://gitlab.mock/uploads/avatar.png",
|
|
129
|
+
web_url: "https://gitlab.mock/sergey.kravchenya",
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
await mockGitLab.start();
|
|
137
|
+
mockGitLabUrl = mockGitLab.getUrl();
|
|
138
|
+
});
|
|
139
|
+
after(async () => {
|
|
140
|
+
await mockGitLab.stop();
|
|
141
|
+
});
|
|
142
|
+
test("falls back to approvals endpoint when approval_state is unavailable", async () => {
|
|
143
|
+
const result = await callTool("get_merge_request_approval_state", {
|
|
144
|
+
project_id: TEST_PROJECT_ID,
|
|
145
|
+
merge_request_iid: TEST_MR_IID_WITH_FALLBACK,
|
|
146
|
+
}, {
|
|
147
|
+
GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
|
|
148
|
+
GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN,
|
|
149
|
+
});
|
|
150
|
+
assert.strictEqual(result.source_endpoint, "approvals");
|
|
151
|
+
assert.strictEqual(result.approved, true);
|
|
152
|
+
assert.deepStrictEqual(result.approved_by_usernames, ["sergey.kravchenya"]);
|
|
153
|
+
assert.ok(Array.isArray(result.approved_by));
|
|
154
|
+
assert.strictEqual(result.approved_by[0].username, "sergey.kravchenya");
|
|
155
|
+
});
|
|
156
|
+
test("returns deduplicated approvers from approval_state rules", async () => {
|
|
157
|
+
const result = await callTool("get_merge_request_approval_state", {
|
|
158
|
+
project_id: TEST_PROJECT_ID,
|
|
159
|
+
merge_request_iid: TEST_MR_IID_WITH_APPROVAL_STATE,
|
|
160
|
+
}, {
|
|
161
|
+
GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
|
|
162
|
+
GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN,
|
|
163
|
+
});
|
|
164
|
+
assert.strictEqual(result.source_endpoint, "approval_state");
|
|
165
|
+
assert.strictEqual(result.rules.length, 2);
|
|
166
|
+
assert.deepStrictEqual(result.approved_by_usernames, ["sergey.kravchenya"]);
|
|
167
|
+
assert.ok(Array.isArray(result.approved_by));
|
|
168
|
+
assert.strictEqual(result.approved_by.length, 1);
|
|
169
|
+
assert.strictEqual(result.approved_by[0].username, "sergey.kravchenya");
|
|
170
|
+
});
|
|
171
|
+
});
|
|
@@ -35,10 +35,12 @@ const DEFAULT_TOOLSETS = [
|
|
|
35
35
|
"branches",
|
|
36
36
|
"projects",
|
|
37
37
|
"labels",
|
|
38
|
+
"pipelines",
|
|
39
|
+
"milestones",
|
|
40
|
+
"wiki",
|
|
38
41
|
"releases",
|
|
39
42
|
"users",
|
|
40
43
|
];
|
|
41
|
-
const NON_DEFAULT_TOOLSETS = ["pipelines", "milestones", "wiki"];
|
|
42
44
|
const DEFAULT_TOOL_COUNT = DEFAULT_TOOLSETS.reduce((sum, id) => sum + TOOLSET_TOOL_COUNTS[id], 0);
|
|
43
45
|
const ALL_TOOLSET_TOOL_COUNT = Object.values(TOOLSET_TOOL_COUNTS).reduce((sum, c) => sum + c, 0);
|
|
44
46
|
// Representative tools per toolset for spot-checking
|
|
@@ -128,10 +130,9 @@ describe("Toolset Filtering", () => {
|
|
|
128
130
|
assertContainsAll(tools, TOOLSET_SAMPLE_TOOLS[id], id);
|
|
129
131
|
}
|
|
130
132
|
});
|
|
131
|
-
test("
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
133
|
+
test("includes all toolsets by default (no non-default toolsets)", () => {
|
|
134
|
+
// All toolsets are now default, so default count equals all toolset count
|
|
135
|
+
assert.strictEqual(tools.length, ALL_TOOLSET_TOOL_COUNT);
|
|
135
136
|
});
|
|
136
137
|
test("excludes execute_graphql (not in any toolset)", () => {
|
|
137
138
|
assertContainsNone(tools, ["execute_graphql"], "unassigned");
|
|
@@ -175,7 +176,7 @@ describe("Toolset Filtering", () => {
|
|
|
175
176
|
assert.strictEqual(tools.length, ALL_TOOLSET_TOOL_COUNT);
|
|
176
177
|
});
|
|
177
178
|
test("includes pipelines, milestones, and wiki", () => {
|
|
178
|
-
for (const id of
|
|
179
|
+
for (const id of ["pipelines", "milestones", "wiki"]) {
|
|
179
180
|
assertContainsAll(tools, TOOLSET_SAMPLE_TOOLS[id], id);
|
|
180
181
|
}
|
|
181
182
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zereight/mcp-gitlab",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.32",
|
|
4
4
|
"description": "MCP server for using the GitLab API",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "zereight",
|
|
@@ -29,9 +29,10 @@
|
|
|
29
29
|
"changelog": "auto-changelog -p",
|
|
30
30
|
"test": "npm run test:all",
|
|
31
31
|
"test:all": "npm run build && npm run test:mock && npm run test:live",
|
|
32
|
-
"test:mock": "npx tsx --test test/remote-auth-simple-test.ts && tsx test/oauth-tests.ts && tsx test/test-list-merge-requests.ts && tsx test/test-list-project-members.ts && tsx test/test-download-attachment.ts",
|
|
32
|
+
"test:mock": "npx tsx --test test/remote-auth-simple-test.ts && tsx test/oauth-tests.ts && tsx test/test-list-merge-requests.ts && tsx test/test-list-project-members.ts && tsx test/test-download-attachment.ts && tsx --test test/test-job-artifacts.ts && tsx --test test/test-deployment-tools.ts && tsx --test test/test-merge-request-approval-state-tools.ts",
|
|
33
33
|
"test:live": "node test/validate-api.js",
|
|
34
34
|
"test:remote-auth": "npm run build && npx tsx --test test/remote-auth-simple-test.ts",
|
|
35
|
+
"test:schema": "tsx test/schema-tests.ts",
|
|
35
36
|
"test:oauth": "tsx test/oauth-tests.ts",
|
|
36
37
|
"test:list-merge-requests": "npm run build && tsx test/test-list-merge-requests.ts",
|
|
37
38
|
"test:approvals": "npm run build && tsx test/test-merge-request-approvals.ts",
|