@zereight/mcp-gitlab 2.0.30 → 2.0.33
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 +777 -28
- package/build/oauth.js +16 -4
- package/build/schemas.js +310 -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,194 @@
|
|
|
1
|
+
import { describe, test, before, after } 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
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
const MOCK_TOKEN = 'glpat-mock-token-12345';
|
|
9
|
+
const TEST_PROJECT_ID = '123';
|
|
10
|
+
const TEST_JOB_ID = '456';
|
|
11
|
+
const TEST_ENCODED_ARTIFACT_PATH = 'reports/report#1.txt';
|
|
12
|
+
// Helper to run an MCP tool via the built server
|
|
13
|
+
async function callTool(toolName, args, env) {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
const proc = spawn('node', ['build/index.js'], {
|
|
16
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
17
|
+
env: {
|
|
18
|
+
...process.env,
|
|
19
|
+
...env,
|
|
20
|
+
GITLAB_READ_ONLY_MODE: 'true',
|
|
21
|
+
USE_PIPELINE: 'true',
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
let output = '';
|
|
25
|
+
let errorOutput = '';
|
|
26
|
+
proc.stdout?.on('data', (d) => (output += d));
|
|
27
|
+
proc.stderr?.on('data', (d) => (errorOutput += d));
|
|
28
|
+
proc.on('close', (code) => {
|
|
29
|
+
if (code !== 0)
|
|
30
|
+
return reject(new Error(`Process exited with code ${code}: ${errorOutput}`));
|
|
31
|
+
const line = output.split('\n').find((l) => l.startsWith('{'));
|
|
32
|
+
if (!line)
|
|
33
|
+
return reject(new Error('No JSON output found'));
|
|
34
|
+
try {
|
|
35
|
+
const response = JSON.parse(line);
|
|
36
|
+
if (response.error) {
|
|
37
|
+
reject(response.error);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
const content = response.result?.content?.[0]?.text;
|
|
41
|
+
if (content) {
|
|
42
|
+
try {
|
|
43
|
+
resolve(JSON.parse(content));
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// Not JSON (plain text response)
|
|
47
|
+
resolve(content);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
resolve(response.result);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch (e) {
|
|
56
|
+
reject(e);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
proc.stdin?.end(JSON.stringify({
|
|
60
|
+
jsonrpc: '2.0',
|
|
61
|
+
id: 1,
|
|
62
|
+
method: 'tools/call',
|
|
63
|
+
params: { name: toolName, arguments: args },
|
|
64
|
+
}) + '\n');
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
describe('job artifacts tools', () => {
|
|
68
|
+
let mockGitLab;
|
|
69
|
+
let mockGitLabUrl;
|
|
70
|
+
let tmpDir;
|
|
71
|
+
before(async () => {
|
|
72
|
+
const mockPort = await findMockServerPort(9200);
|
|
73
|
+
mockGitLab = new MockGitLabServer({
|
|
74
|
+
port: mockPort,
|
|
75
|
+
validTokens: [MOCK_TOKEN],
|
|
76
|
+
});
|
|
77
|
+
// Add mock handler for artifact tree listing
|
|
78
|
+
mockGitLab.addMockHandler('get', `/projects/${TEST_PROJECT_ID}/jobs/${TEST_JOB_ID}/artifacts/tree`, (req, res) => {
|
|
79
|
+
res.json([
|
|
80
|
+
{
|
|
81
|
+
name: 'report.xml',
|
|
82
|
+
path: 'report.xml',
|
|
83
|
+
type: 'file',
|
|
84
|
+
size: 1024,
|
|
85
|
+
mode: '100644',
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: 'logs',
|
|
89
|
+
path: 'logs',
|
|
90
|
+
type: 'directory',
|
|
91
|
+
mode: '040755',
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: 'output.log',
|
|
95
|
+
path: 'logs/output.log',
|
|
96
|
+
type: 'file',
|
|
97
|
+
size: 512,
|
|
98
|
+
mode: '100644',
|
|
99
|
+
},
|
|
100
|
+
]);
|
|
101
|
+
});
|
|
102
|
+
// Add mock handler for downloading the full artifact archive
|
|
103
|
+
mockGitLab.addMockHandler('get', `/projects/${TEST_PROJECT_ID}/jobs/${TEST_JOB_ID}/artifacts`, (req, res) => {
|
|
104
|
+
// Return a minimal zip-like binary content for testing
|
|
105
|
+
const fakeZipContent = Buffer.from('PK\x03\x04fake-zip-content-for-testing');
|
|
106
|
+
res.set('Content-Type', 'application/zip');
|
|
107
|
+
res.set('Content-Disposition', `attachment; filename="artifacts_job_${TEST_JOB_ID}.zip"`);
|
|
108
|
+
res.send(fakeZipContent);
|
|
109
|
+
});
|
|
110
|
+
// Add mock handler for getting a single artifact file
|
|
111
|
+
mockGitLab.addMockHandler('get', `/projects/${TEST_PROJECT_ID}/jobs/${TEST_JOB_ID}/artifacts/report.xml`, (req, res) => {
|
|
112
|
+
res.set('Content-Type', 'application/xml');
|
|
113
|
+
res.send('<testsuites><testsuite name="unit" tests="5" failures="1"></testsuite></testsuites>');
|
|
114
|
+
});
|
|
115
|
+
// Add mock handler for path that requires URL encoding
|
|
116
|
+
mockGitLab.addMockHandler('get', `/projects/${TEST_PROJECT_ID}/jobs/${TEST_JOB_ID}/artifacts/reports/report%231.txt`, (req, res) => {
|
|
117
|
+
res.set('Content-Type', 'text/plain');
|
|
118
|
+
res.send('encoded artifact content');
|
|
119
|
+
});
|
|
120
|
+
// Add mock handler for 404 on non-existent artifact
|
|
121
|
+
mockGitLab.addMockHandler('get', `/projects/${TEST_PROJECT_ID}/jobs/999/artifacts/tree`, (req, res) => {
|
|
122
|
+
res.status(404).json({ message: 'Not Found' });
|
|
123
|
+
});
|
|
124
|
+
await mockGitLab.start();
|
|
125
|
+
mockGitLabUrl = mockGitLab.getUrl();
|
|
126
|
+
// Create a temp directory for download tests
|
|
127
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gitlab-mcp-test-'));
|
|
128
|
+
});
|
|
129
|
+
after(async () => {
|
|
130
|
+
await mockGitLab.stop();
|
|
131
|
+
// Clean up temp directory
|
|
132
|
+
if (tmpDir && fs.existsSync(tmpDir)) {
|
|
133
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
test('list_job_artifacts returns artifact entries', async () => {
|
|
137
|
+
const result = await callTool('list_job_artifacts', { project_id: TEST_PROJECT_ID, job_id: TEST_JOB_ID }, {
|
|
138
|
+
GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
|
|
139
|
+
GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN,
|
|
140
|
+
});
|
|
141
|
+
assert.ok(Array.isArray(result), 'Response should be an array');
|
|
142
|
+
assert.strictEqual(result.length, 3, 'Should return 3 artifact entries');
|
|
143
|
+
assert.strictEqual(result[0].name, 'report.xml');
|
|
144
|
+
assert.strictEqual(result[0].type, 'file');
|
|
145
|
+
assert.strictEqual(result[0].size, 1024);
|
|
146
|
+
assert.strictEqual(result[1].name, 'logs');
|
|
147
|
+
assert.strictEqual(result[1].type, 'directory');
|
|
148
|
+
});
|
|
149
|
+
test('download_job_artifacts saves archive to disk', async () => {
|
|
150
|
+
const result = await callTool('download_job_artifacts', { project_id: TEST_PROJECT_ID, job_id: TEST_JOB_ID, local_path: tmpDir }, {
|
|
151
|
+
GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
|
|
152
|
+
GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN,
|
|
153
|
+
});
|
|
154
|
+
assert.ok(result.success, 'Download should succeed');
|
|
155
|
+
assert.ok(result.file_path.endsWith('.zip'), 'File should have .zip extension');
|
|
156
|
+
assert.ok(fs.existsSync(result.file_path), `File should exist at ${result.file_path}`);
|
|
157
|
+
const stats = fs.statSync(result.file_path);
|
|
158
|
+
assert.ok(stats.size > 0, 'Downloaded file should not be empty');
|
|
159
|
+
});
|
|
160
|
+
test('download_job_artifacts creates nested destination directories', async () => {
|
|
161
|
+
const nestedLocalPath = path.join(tmpDir, 'artifacts', 'run-42');
|
|
162
|
+
const result = await callTool('download_job_artifacts', { project_id: TEST_PROJECT_ID, job_id: TEST_JOB_ID, local_path: nestedLocalPath }, {
|
|
163
|
+
GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
|
|
164
|
+
GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN,
|
|
165
|
+
});
|
|
166
|
+
assert.ok(result.success, 'Download should succeed');
|
|
167
|
+
assert.ok(fs.existsSync(result.file_path), `File should exist at ${result.file_path}`);
|
|
168
|
+
assert.ok(fs.existsSync(nestedLocalPath), `Directory should be created at ${nestedLocalPath}`);
|
|
169
|
+
});
|
|
170
|
+
test('get_job_artifact_file returns file content', async () => {
|
|
171
|
+
const result = await callTool('get_job_artifact_file', {
|
|
172
|
+
project_id: TEST_PROJECT_ID,
|
|
173
|
+
job_id: TEST_JOB_ID,
|
|
174
|
+
artifact_path: 'report.xml',
|
|
175
|
+
}, {
|
|
176
|
+
GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
|
|
177
|
+
GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN,
|
|
178
|
+
});
|
|
179
|
+
assert.ok(typeof result === 'string', 'Response should be a string');
|
|
180
|
+
assert.ok(result.includes('<testsuites>'), 'Should contain XML content');
|
|
181
|
+
assert.ok(result.includes('failures="1"'), 'Should contain failure data');
|
|
182
|
+
});
|
|
183
|
+
test('get_job_artifact_file handles artifact paths with reserved characters', async () => {
|
|
184
|
+
const result = await callTool('get_job_artifact_file', {
|
|
185
|
+
project_id: TEST_PROJECT_ID,
|
|
186
|
+
job_id: TEST_JOB_ID,
|
|
187
|
+
artifact_path: TEST_ENCODED_ARTIFACT_PATH,
|
|
188
|
+
}, {
|
|
189
|
+
GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
|
|
190
|
+
GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN,
|
|
191
|
+
});
|
|
192
|
+
assert.strictEqual(result, 'encoded artifact content');
|
|
193
|
+
});
|
|
194
|
+
});
|
|
@@ -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.33",
|
|
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",
|