@zereight/mcp-gitlab 2.1.69 → 2.2.0
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 +10 -4
- package/README.md +238 -233
- package/README.pt-BR.md +4 -4
- package/README.zh-CN.md +6 -4
- package/build/config.js +12 -4
- package/build/deprecated-env.js +47 -0
- package/build/index.js +456 -237
- package/build/oauth-device-flow.js +80 -0
- package/build/oauth.js +135 -18
- package/build/schemas.js +1 -1
- package/build/server/remote-http-stack.js +39 -0
- package/build/tools/discover-activation.js +38 -0
- package/build/tools/exposed-tools.js +35 -0
- package/build/tools/listed-tools.js +76 -0
- package/build/tools/registry.js +226 -57
- package/build/tools/tool-profile.js +66 -0
- package/build/utils/compact-tool-allowlist.js +14 -0
- package/build/utils/logger.js +34 -12
- package/build/utils/url.js +15 -0
- package/package.json +3 -3
- package/build/scripts/check-skill-sync.js +0 -143
- package/build/scripts/generate-server-card.js +0 -39
- package/build/scripts/generate-tool-coverage.js +0 -58
- package/build/scripts/generate-tool-docs.js +0 -430
- package/build/scripts/tool-coverage/coverage.js +0 -341
- package/build/scripts/tool-coverage/coverage.test.js +0 -142
- package/build/test/callback-proxy-tests.js +0 -321
- package/build/test/cli/args.test.js +0 -175
- package/build/test/cli/boolean-flags.test.js +0 -106
- package/build/test/cli/compact-mcp-result.test.js +0 -93
- package/build/test/cli/exposure.test.js +0 -104
- package/build/test/cli/load-config-snapshot.js +0 -5
- package/build/test/cli/output.test.js +0 -74
- package/build/test/cli/replay-command.test.js +0 -37
- package/build/test/cli/router.test.js +0 -183
- package/build/test/client-pool-test.js +0 -109
- package/build/test/clients/client.js +0 -34
- package/build/test/clients/custom-header-client.js +0 -122
- package/build/test/clients/sse-client.js +0 -95
- package/build/test/clients/stdio-client.js +0 -113
- package/build/test/clients/streamable-http-client.js +0 -95
- package/build/test/config-allowed-groups.test.js +0 -97
- package/build/test/create-commit-actions.test.js +0 -102
- package/build/test/draft-note-schema.test.js +0 -44
- package/build/test/dynamic-api-url-allowlist.test.js +0 -121
- package/build/test/dynamic-api-url-test.js +0 -304
- package/build/test/dynamic-routing-tests.js +0 -495
- package/build/test/get-merge-request-discussion-schema.test.js +0 -30
- package/build/test/gitlab-artifact-entry-schema.test.js +0 -44
- package/build/test/gitlab-diff-schema.test.js +0 -36
- package/build/test/gitlab-pipeline-schedule-schema.test.js +0 -32
- package/build/test/gitlab-tag-schema.test.js +0 -51
- package/build/test/group-milestone-schema.test.js +0 -45
- package/build/test/mcp-oauth-tests.js +0 -905
- package/build/test/mcp-server-name.test.js +0 -87
- package/build/test/merge-merge-request-schema.test.js +0 -75
- package/build/test/multi-server-test.js +0 -190
- package/build/test/no-proxy-integration-test.js +0 -183
- package/build/test/no-proxy-test.js +0 -149
- package/build/test/nullable-gitlab-response-fields.test.js +0 -37
- package/build/test/nullish-tool-arguments-schema.test.js +0 -118
- package/build/test/oauth-device-flow-tests.js +0 -450
- package/build/test/oauth-startup.test.js +0 -204
- package/build/test/oauth-tests.js +0 -428
- package/build/test/path-segment-encoding.test.js +0 -98
- package/build/test/remote-auth-simple-test.js +0 -277
- package/build/test/remote-auth-tests.js +0 -314
- package/build/test/response-masking.test.js +0 -445
- package/build/test/schema-tests.js +0 -1679
- package/build/test/server/metrics.test.js +0 -47
- package/build/test/server/request-helpers.test.js +0 -48
- package/build/test/server/version.test.js +0 -13
- package/build/test/sse-auth-guard.test.js +0 -96
- package/build/test/sse-session-limits.test.js +0 -273
- package/build/test/stateless/callback-proxy.test.js +0 -510
- package/build/test/stateless/client-id.test.js +0 -177
- package/build/test/stateless/codec.test.js +0 -328
- package/build/test/stateless/config-ttl.test.js +0 -149
- package/build/test/stateless/consumed-proxy-code-cache.test.js +0 -150
- package/build/test/stateless/session-id-integration.test.js +0 -675
- package/build/test/stateless/session-id.test.js +0 -157
- package/build/test/streamable-http-concurrent-session.test.js +0 -171
- package/build/test/streamable-http-dns-rebinding.test.js +0 -185
- package/build/test/streamable-http-sse-stream.test.js +0 -350
- package/build/test/streamable-http-static-token-auth.test.js +0 -157
- package/build/test/streamable-http-unauthenticated-discovery.test.js +0 -207
- package/build/test/test-all-transport-server.js +0 -255
- package/build/test/test-auth-retry.js +0 -195
- package/build/test/test-blank-filters.js +0 -156
- package/build/test/test-ci-catalog.js +0 -177
- package/build/test/test-ci-lint.js +0 -191
- package/build/test/test-ci-variables.js +0 -309
- package/build/test/test-create-repository.js +0 -120
- package/build/test/test-dependency-proxy.js +0 -195
- package/build/test/test-deployment-tools.js +0 -463
- package/build/test/test-download-attachment.js +0 -190
- package/build/test/test-dynamic-project-scope.js +0 -524
- package/build/test/test-empty-toolsets.js +0 -535
- package/build/test/test-get-file-blame.js +0 -145
- package/build/test/test-get-file-contents.js +0 -128
- package/build/test/test-geteffectiveprojectid.js +0 -812
- package/build/test/test-health-check.js +0 -115
- package/build/test/test-issue-description-patch.js +0 -256
- package/build/test/test-job-artifacts.js +0 -275
- package/build/test/test-json-schema.js +0 -166
- package/build/test/test-list-group-members.js +0 -150
- package/build/test/test-list-group-merge-requests.js +0 -165
- package/build/test/test-list-issues.js +0 -170
- package/build/test/test-list-merge-requests.js +0 -201
- package/build/test/test-list-milestones.js +0 -145
- package/build/test/test-list-project-members.js +0 -132
- package/build/test/test-merge-request-approval-state-tools.js +0 -213
- package/build/test/test-merge-request-approvals.js +0 -187
- package/build/test/test-merge-request-pipelines.js +0 -106
- package/build/test/test-mr-diffs-filter.js +0 -132
- package/build/test/test-mr-file-diffs.js +0 -251
- package/build/test/test-oauth-proxy-rate-limit.js +0 -133
- package/build/test/test-orbit.js +0 -155
- package/build/test/test-permission-mode.js +0 -424
- package/build/test/test-pipeline-schedules.js +0 -482
- package/build/test/test-protected-branches.js +0 -174
- package/build/test/test-remote-downloads.js +0 -510
- package/build/test/test-search-code.js +0 -275
- package/build/test/test-snippets.js +0 -735
- package/build/test/test-tags.js +0 -220
- package/build/test/test-todos.js +0 -195
- package/build/test/test-token-optimizations.js +0 -691
- package/build/test/test-toolset-filtering.js +0 -499
- package/build/test/test-update-issue-slim.js +0 -141
- package/build/test/test-update-project.js +0 -112
- package/build/test/test-upload-markdown.js +0 -206
- package/build/test/test-vulnerabilities.js +0 -399
- package/build/test/test-webhooks.js +0 -218
- package/build/test/tool-description-quality.test.js +0 -50
- package/build/test/utils/bulk-publish-options.test.js +0 -32
- package/build/test/utils/download-token.test.js +0 -35
- package/build/test/utils/forwarded-public-base-url.test.js +0 -46
- package/build/test/utils/gitlab-instance-version.test.js +0 -32
- package/build/test/utils/gitlab-version-gate.test.js +0 -74
- package/build/test/utils/graphql-query.test.js +0 -168
- package/build/test/utils/helpers-path.test.js +0 -104
- package/build/test/utils/jmespath-tool-result.test.js +0 -91
- package/build/test/utils/masking.test.js +0 -384
- package/build/test/utils/merge-request-position.test.js +0 -57
- package/build/test/utils/mock-gitlab-server.js +0 -614
- package/build/test/utils/proxy-client-ip.test.js +0 -28
- package/build/test/utils/redact-sensitive.test.js +0 -32
- package/build/test/utils/safe-redirect-fetch.test.js +0 -463
- package/build/test/utils/server-launcher.js +0 -231
- package/build/test/utils/tool-args.test.js +0 -210
- package/build/test/utils/url.test.js +0 -128
- package/build/test/utils/version-check.test.js +0 -239
- package/build/test/utils/wiki-title.test.js +0 -21
- package/build/test-note.js +0 -54
- package/build/test-resolve-issue-note.js +0 -127
|
@@ -1,812 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Test suite for getEffectiveProjectId function
|
|
3
|
-
* Tests the behavior of project ID resolution with different environment configurations
|
|
4
|
-
*/
|
|
5
|
-
import { describe, test, before, after } from 'node:test';
|
|
6
|
-
import assert from 'node:assert';
|
|
7
|
-
import { launchServer, findAvailablePort, cleanupServers, TransportMode, HOST } from './utils/server-launcher.js';
|
|
8
|
-
import { MockGitLabServer, findMockServerPort } from './utils/mock-gitlab-server.js';
|
|
9
|
-
import { CustomHeaderClient } from './clients/custom-header-client.js';
|
|
10
|
-
// Use the same token that will be passed via GITLAB_TOKEN_TEST environment variable
|
|
11
|
-
const MOCK_TOKEN = process.env.GITLAB_TOKEN_TEST || 'glpat-mock-token-12345';
|
|
12
|
-
const DEFAULT_PROJECT_ID = '123';
|
|
13
|
-
const OTHER_PROJECT_ID = '456';
|
|
14
|
-
// Ensure GITLAB_TOKEN_TEST is set for launchServer() validation
|
|
15
|
-
if (!process.env.GITLAB_TOKEN_TEST && !process.env.GITLAB_TOKEN) {
|
|
16
|
-
process.env.GITLAB_TOKEN_TEST = MOCK_TOKEN;
|
|
17
|
-
}
|
|
18
|
-
if (!process.env.TEST_PROJECT_ID) {
|
|
19
|
-
process.env.TEST_PROJECT_ID = DEFAULT_PROJECT_ID;
|
|
20
|
-
}
|
|
21
|
-
console.log('🔍 Testing getEffectiveProjectId functionality');
|
|
22
|
-
console.log('');
|
|
23
|
-
describe('getEffectiveProjectId', { concurrency: 1 }, () => {
|
|
24
|
-
describe('getEffectiveProjectId - No GITLAB_ALLOWED_PROJECT_IDS', () => {
|
|
25
|
-
let mcpUrl;
|
|
26
|
-
let mockGitLab;
|
|
27
|
-
let servers = [];
|
|
28
|
-
let client;
|
|
29
|
-
let group123Requested = false;
|
|
30
|
-
let group789Requested = false;
|
|
31
|
-
let labelLookupRoots = [];
|
|
32
|
-
before(async () => {
|
|
33
|
-
// Start mock GitLab server
|
|
34
|
-
const mockPort = await findMockServerPort();
|
|
35
|
-
mockGitLab = new MockGitLabServer({
|
|
36
|
-
port: mockPort,
|
|
37
|
-
validTokens: [MOCK_TOKEN]
|
|
38
|
-
});
|
|
39
|
-
mockGitLab.addMockHandler('get', '/groups/123', (_req, res) => {
|
|
40
|
-
group123Requested = true;
|
|
41
|
-
res.json({
|
|
42
|
-
id: 123,
|
|
43
|
-
name: 'Test Group',
|
|
44
|
-
path: 'test-group',
|
|
45
|
-
full_path: 'test-group',
|
|
46
|
-
});
|
|
47
|
-
});
|
|
48
|
-
mockGitLab.addMockHandler('get', '/projects/789', (_req, res) => {
|
|
49
|
-
res.status(404).json({ message: '404 Project Not Found' });
|
|
50
|
-
});
|
|
51
|
-
mockGitLab.addMockHandler('get', '/groups/789', (_req, res) => {
|
|
52
|
-
group789Requested = true;
|
|
53
|
-
res.json({
|
|
54
|
-
id: 789,
|
|
55
|
-
name: 'Numeric Group',
|
|
56
|
-
path: 'numeric-group',
|
|
57
|
-
full_path: 'numeric-group',
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
mockGitLab.addRootHandler('post', '/api/graphql', (req, res) => {
|
|
61
|
-
const { query, variables } = req.body;
|
|
62
|
-
if (query.includes('workItemTypes')) {
|
|
63
|
-
res.json({
|
|
64
|
-
data: {
|
|
65
|
-
namespace: {
|
|
66
|
-
workItemTypes: {
|
|
67
|
-
nodes: [{ id: 'gid://gitlab/WorkItems::Type/1', name: 'Issue' }],
|
|
68
|
-
},
|
|
69
|
-
},
|
|
70
|
-
},
|
|
71
|
-
});
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
if (query.includes('workItems(')) {
|
|
75
|
-
assert.ok(query.includes('group(fullPath: $path)'), 'Should query the group GraphQL root');
|
|
76
|
-
assert.strictEqual(variables.path, 'test-group', 'Should use resolved group full_path');
|
|
77
|
-
res.json({
|
|
78
|
-
data: {
|
|
79
|
-
group: {
|
|
80
|
-
workItems: {
|
|
81
|
-
nodes: [
|
|
82
|
-
{
|
|
83
|
-
id: 'gid://gitlab/WorkItem/1',
|
|
84
|
-
iid: '1',
|
|
85
|
-
title: 'Group work item',
|
|
86
|
-
state: 'OPEN',
|
|
87
|
-
webUrl: 'https://gitlab.mock/groups/test-group/-/work_items/1',
|
|
88
|
-
workItemType: { name: 'Issue' },
|
|
89
|
-
widgets: [],
|
|
90
|
-
},
|
|
91
|
-
],
|
|
92
|
-
pageInfo: { hasNextPage: false, endCursor: null },
|
|
93
|
-
},
|
|
94
|
-
},
|
|
95
|
-
},
|
|
96
|
-
});
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
if (query.includes('labels(title:')) {
|
|
100
|
-
assert.ok(query.includes('group(fullPath: $path)'), 'Should resolve labels from Group');
|
|
101
|
-
assert.ok(!query.includes('namespace(fullPath: $path)'), 'Should not query labels from Namespace');
|
|
102
|
-
labelLookupRoots.push('group');
|
|
103
|
-
res.json({
|
|
104
|
-
data: {
|
|
105
|
-
group: { l0: { nodes: [{ id: 'gid://gitlab/GroupLabel/1' }] } },
|
|
106
|
-
users: { nodes: [] },
|
|
107
|
-
},
|
|
108
|
-
});
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
if (query.includes('workItem(iid: $iid)')) {
|
|
112
|
-
res.json({
|
|
113
|
-
data: { namespace: { workItem: { id: 'gid://gitlab/WorkItem/1' } } },
|
|
114
|
-
});
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
if (query.includes('workItemCreate')) {
|
|
118
|
-
assert.deepStrictEqual(variables.labelIds, ['gid://gitlab/GroupLabel/1']);
|
|
119
|
-
res.json({
|
|
120
|
-
data: {
|
|
121
|
-
workItemCreate: {
|
|
122
|
-
workItem: {
|
|
123
|
-
id: 'gid://gitlab/WorkItem/2',
|
|
124
|
-
iid: '2',
|
|
125
|
-
title: variables.title,
|
|
126
|
-
webUrl: 'https://gitlab.mock/groups/test-group/-/work_items/2',
|
|
127
|
-
workItemType: { name: 'Issue' },
|
|
128
|
-
},
|
|
129
|
-
errors: [],
|
|
130
|
-
},
|
|
131
|
-
},
|
|
132
|
-
});
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
if (query.includes('workItemUpdate')) {
|
|
136
|
-
assert.deepStrictEqual(variables.addLabelIds, ['gid://gitlab/GroupLabel/1']);
|
|
137
|
-
res.json({
|
|
138
|
-
data: {
|
|
139
|
-
workItemUpdate: {
|
|
140
|
-
workItem: {
|
|
141
|
-
id: variables.id,
|
|
142
|
-
iid: '1',
|
|
143
|
-
title: 'Updated group work item',
|
|
144
|
-
state: 'OPEN',
|
|
145
|
-
webUrl: 'https://gitlab.mock/groups/test-group/-/work_items/1',
|
|
146
|
-
workItemType: { name: 'Issue' },
|
|
147
|
-
widgets: [],
|
|
148
|
-
},
|
|
149
|
-
errors: [],
|
|
150
|
-
},
|
|
151
|
-
},
|
|
152
|
-
});
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
res.status(500).json({ message: `Unexpected GraphQL query: ${query}` });
|
|
156
|
-
});
|
|
157
|
-
await mockGitLab.start();
|
|
158
|
-
const mockGitLabUrl = mockGitLab.getUrl();
|
|
159
|
-
// Start MCP server WITHOUT GITLAB_ALLOWED_PROJECT_IDS
|
|
160
|
-
const mcpPort = await findAvailablePort(3100);
|
|
161
|
-
const server = await launchServer({
|
|
162
|
-
mode: TransportMode.STREAMABLE_HTTP,
|
|
163
|
-
port: mcpPort,
|
|
164
|
-
timeout: 5000,
|
|
165
|
-
env: {
|
|
166
|
-
STREAMABLE_HTTP: 'true',
|
|
167
|
-
REMOTE_AUTHORIZATION: 'true',
|
|
168
|
-
GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
|
|
169
|
-
GITLAB_PROJECT_ID: DEFAULT_PROJECT_ID,
|
|
170
|
-
GITLAB_READ_ONLY_MODE: 'false',
|
|
171
|
-
GITLAB_TOOLSETS: 'all',
|
|
172
|
-
}
|
|
173
|
-
});
|
|
174
|
-
servers.push(server);
|
|
175
|
-
mcpUrl = `http://${HOST}:${mcpPort}/mcp`;
|
|
176
|
-
client = new CustomHeaderClient({
|
|
177
|
-
authorization: `Bearer ${MOCK_TOKEN}`,
|
|
178
|
-
});
|
|
179
|
-
await client.connect(mcpUrl);
|
|
180
|
-
console.log(`Mock GitLab: ${mockGitLabUrl}`);
|
|
181
|
-
console.log(`MCP Server: ${mcpUrl}`);
|
|
182
|
-
console.log(`Default Project: ${DEFAULT_PROJECT_ID}`);
|
|
183
|
-
});
|
|
184
|
-
after(async () => {
|
|
185
|
-
if (client) {
|
|
186
|
-
await client.disconnect();
|
|
187
|
-
}
|
|
188
|
-
cleanupServers(servers);
|
|
189
|
-
if (mockGitLab) {
|
|
190
|
-
await mockGitLab.stop();
|
|
191
|
-
}
|
|
192
|
-
});
|
|
193
|
-
test('should use GITLAB_PROJECT_ID when no project_id is provided', async () => {
|
|
194
|
-
// Call get_project without specifying project_id
|
|
195
|
-
const result = await client.callTool('get_project', {
|
|
196
|
-
project_id: ''
|
|
197
|
-
});
|
|
198
|
-
assert.ok(result.content, 'Should have content');
|
|
199
|
-
const content = result.content[0];
|
|
200
|
-
assert.ok('text' in content, 'Content should have text');
|
|
201
|
-
const project = JSON.parse(content.text);
|
|
202
|
-
// The mock server should receive a request for the default project
|
|
203
|
-
assert.strictEqual(project.id.toString(), DEFAULT_PROJECT_ID, 'Should use GITLAB_PROJECT_ID as default');
|
|
204
|
-
console.log(` ✓ Used default project ${DEFAULT_PROJECT_ID} when no project_id provided`);
|
|
205
|
-
});
|
|
206
|
-
test('should prioritize passed project_id over GITLAB_PROJECT_ID', async () => {
|
|
207
|
-
// Call get_project with a different project_id
|
|
208
|
-
const result = await client.callTool('get_project', {
|
|
209
|
-
project_id: OTHER_PROJECT_ID
|
|
210
|
-
});
|
|
211
|
-
assert.ok(result.content, 'Should have content');
|
|
212
|
-
const content = result.content[0];
|
|
213
|
-
assert.ok('text' in content, 'Content should have text');
|
|
214
|
-
const project = JSON.parse(content.text);
|
|
215
|
-
// Should use the passed project_id, not GITLAB_PROJECT_ID
|
|
216
|
-
assert.strictEqual(project.id.toString(), OTHER_PROJECT_ID, 'Should use passed project_id');
|
|
217
|
-
console.log(` ✓ Used passed project_id ${OTHER_PROJECT_ID} instead of default ${DEFAULT_PROJECT_ID}`);
|
|
218
|
-
});
|
|
219
|
-
test('should resolve explicit numeric group IDs through the groups endpoint', async () => {
|
|
220
|
-
group123Requested = false;
|
|
221
|
-
const result = await client.callTool('list_work_items', {
|
|
222
|
-
project_id: 'group:123',
|
|
223
|
-
});
|
|
224
|
-
assert.ok(group123Requested, 'Should request /groups/123');
|
|
225
|
-
assert.ok(result.content, 'Should have content');
|
|
226
|
-
const content = result.content[0];
|
|
227
|
-
assert.ok('text' in content, 'Content should have text');
|
|
228
|
-
const parsed = JSON.parse(content.text);
|
|
229
|
-
assert.strictEqual(parsed.items.length, 1, 'Should return group work items');
|
|
230
|
-
assert.strictEqual(parsed.items[0].title, 'Group work item');
|
|
231
|
-
console.log(' ✓ Resolved group:123 via /groups/123 when no project allowlist is set');
|
|
232
|
-
});
|
|
233
|
-
test('should resolve labels from the group root when creating group work items', async () => {
|
|
234
|
-
labelLookupRoots = [];
|
|
235
|
-
const result = await client.callTool('create_work_item', {
|
|
236
|
-
project_id: 'group:123',
|
|
237
|
-
title: 'Created group work item',
|
|
238
|
-
labels: ['group-label'],
|
|
239
|
-
});
|
|
240
|
-
assert.deepStrictEqual(labelLookupRoots, ['group']);
|
|
241
|
-
assert.ok(result.content, 'Should have content');
|
|
242
|
-
const content = result.content[0];
|
|
243
|
-
assert.ok('text' in content, 'Content should have text');
|
|
244
|
-
assert.strictEqual(JSON.parse(content.text).title, 'Created group work item');
|
|
245
|
-
console.log(' ✓ Resolved create labels through group(fullPath:)');
|
|
246
|
-
});
|
|
247
|
-
test('should resolve labels from the group root when updating group work items', async () => {
|
|
248
|
-
labelLookupRoots = [];
|
|
249
|
-
const result = await client.callTool('update_work_item', {
|
|
250
|
-
project_id: 'group:123',
|
|
251
|
-
iid: 1,
|
|
252
|
-
add_labels: ['group-label'],
|
|
253
|
-
});
|
|
254
|
-
assert.deepStrictEqual(labelLookupRoots, ['group']);
|
|
255
|
-
assert.ok(result.content, 'Should have content');
|
|
256
|
-
const content = result.content[0];
|
|
257
|
-
assert.ok('text' in content, 'Content should have text');
|
|
258
|
-
assert.strictEqual(JSON.parse(content.text).title, 'Updated group work item');
|
|
259
|
-
console.log(' ✓ Resolved update labels through group(fullPath:)');
|
|
260
|
-
});
|
|
261
|
-
test('should not fall back from bare numeric project IDs to groups', async () => {
|
|
262
|
-
group789Requested = false;
|
|
263
|
-
let didThrow = false;
|
|
264
|
-
try {
|
|
265
|
-
await client.callTool('list_work_items', {
|
|
266
|
-
project_id: '789',
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
catch (error) {
|
|
270
|
-
didThrow = true;
|
|
271
|
-
assert.ok(error instanceof Error);
|
|
272
|
-
assert.ok(error.message.includes('404'), 'Should surface the project 404');
|
|
273
|
-
}
|
|
274
|
-
assert.ok(didThrow, 'Should have failed on the project 404');
|
|
275
|
-
assert.strictEqual(group789Requested, false, 'Should not request /groups/789');
|
|
276
|
-
console.log(' ✓ Bare numeric ID 789 did not fall back to /groups/789');
|
|
277
|
-
});
|
|
278
|
-
test('should not fall back from explicit numeric project IDs to groups', async () => {
|
|
279
|
-
group789Requested = false;
|
|
280
|
-
let didThrow = false;
|
|
281
|
-
try {
|
|
282
|
-
await client.callTool('list_work_items', {
|
|
283
|
-
project_id: 'project:789',
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
catch (error) {
|
|
287
|
-
didThrow = true;
|
|
288
|
-
assert.ok(error instanceof Error);
|
|
289
|
-
assert.ok(error.message.includes('404'), 'Should surface the project 404');
|
|
290
|
-
}
|
|
291
|
-
assert.ok(didThrow, 'Should have failed on the project 404');
|
|
292
|
-
assert.strictEqual(group789Requested, false, 'Should not request /groups/789');
|
|
293
|
-
console.log(' ✓ Explicit project:789 did not fall back to /groups/789');
|
|
294
|
-
});
|
|
295
|
-
});
|
|
296
|
-
describe('getEffectiveProjectId - With single GITLAB_ALLOWED_PROJECT_IDS', () => {
|
|
297
|
-
let mcpUrl;
|
|
298
|
-
let mockGitLab;
|
|
299
|
-
let servers = [];
|
|
300
|
-
let client;
|
|
301
|
-
before(async () => {
|
|
302
|
-
// Start mock GitLab server
|
|
303
|
-
const mockPort = await findMockServerPort();
|
|
304
|
-
mockGitLab = new MockGitLabServer({
|
|
305
|
-
port: mockPort,
|
|
306
|
-
validTokens: [MOCK_TOKEN]
|
|
307
|
-
});
|
|
308
|
-
await mockGitLab.start();
|
|
309
|
-
const mockGitLabUrl = mockGitLab.getUrl();
|
|
310
|
-
// Start MCP server WITH single GITLAB_ALLOWED_PROJECT_IDS
|
|
311
|
-
const mcpPort = await findAvailablePort(3200);
|
|
312
|
-
const server = await launchServer({
|
|
313
|
-
mode: TransportMode.STREAMABLE_HTTP,
|
|
314
|
-
port: mcpPort,
|
|
315
|
-
timeout: 5000,
|
|
316
|
-
env: {
|
|
317
|
-
REMOTE_AUTHORIZATION: 'true',
|
|
318
|
-
GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
|
|
319
|
-
GITLAB_PROJECT_ID: DEFAULT_PROJECT_ID,
|
|
320
|
-
GITLAB_ALLOWED_PROJECT_IDS: DEFAULT_PROJECT_ID,
|
|
321
|
-
GITLAB_READ_ONLY_MODE: 'true',
|
|
322
|
-
GITLAB_TOOLSETS: 'all',
|
|
323
|
-
}
|
|
324
|
-
});
|
|
325
|
-
servers.push(server);
|
|
326
|
-
mcpUrl = `http://${HOST}:${mcpPort}/mcp`;
|
|
327
|
-
client = new CustomHeaderClient({
|
|
328
|
-
authorization: `Bearer ${MOCK_TOKEN}`,
|
|
329
|
-
});
|
|
330
|
-
await client.connect(mcpUrl);
|
|
331
|
-
console.log(`Mock GitLab: ${mockGitLabUrl}`);
|
|
332
|
-
console.log(`MCP Server: ${mcpUrl}`);
|
|
333
|
-
console.log(`Allowed Project: ${DEFAULT_PROJECT_ID}`);
|
|
334
|
-
});
|
|
335
|
-
after(async () => {
|
|
336
|
-
if (client) {
|
|
337
|
-
await client.disconnect();
|
|
338
|
-
}
|
|
339
|
-
cleanupServers(servers);
|
|
340
|
-
if (mockGitLab) {
|
|
341
|
-
await mockGitLab.stop();
|
|
342
|
-
}
|
|
343
|
-
});
|
|
344
|
-
test('should use single allowed project as default', async () => {
|
|
345
|
-
const result = await client.callTool('get_project', {
|
|
346
|
-
project_id: ''
|
|
347
|
-
});
|
|
348
|
-
assert.ok(result.content, 'Should have content');
|
|
349
|
-
const content = result.content[0];
|
|
350
|
-
assert.ok('text' in content, 'Content should have text');
|
|
351
|
-
const project = JSON.parse(content.text);
|
|
352
|
-
assert.strictEqual(project.id.toString(), DEFAULT_PROJECT_ID, 'Should use allowed project as default');
|
|
353
|
-
console.log(` ✓ Used allowed project ${DEFAULT_PROJECT_ID} as default`);
|
|
354
|
-
});
|
|
355
|
-
test('should reject access to non-allowed project', async () => {
|
|
356
|
-
try {
|
|
357
|
-
await client.callTool('get_project', {
|
|
358
|
-
project_id: OTHER_PROJECT_ID
|
|
359
|
-
});
|
|
360
|
-
assert.fail('Should have rejected access to non-allowed project');
|
|
361
|
-
}
|
|
362
|
-
catch (error) {
|
|
363
|
-
assert.ok(error instanceof Error);
|
|
364
|
-
assert.ok(error.message.includes('Access denied'), 'Should indicate access denied');
|
|
365
|
-
console.log(' ✓ Correctly rejected access to non-allowed project');
|
|
366
|
-
}
|
|
367
|
-
});
|
|
368
|
-
test('should reject explicit group IDs when GITLAB_ALLOWED_PROJECT_IDS is set', async () => {
|
|
369
|
-
let didThrow = false;
|
|
370
|
-
try {
|
|
371
|
-
await client.callTool('list_work_items', {
|
|
372
|
-
project_id: 'group:123',
|
|
373
|
-
});
|
|
374
|
-
}
|
|
375
|
-
catch (error) {
|
|
376
|
-
didThrow = true;
|
|
377
|
-
assert.ok(error instanceof Error);
|
|
378
|
-
assert.ok(error.message.includes('GITLAB_ALLOWED_PROJECT_IDS') &&
|
|
379
|
-
error.message.includes('project allowlist does not cover groups'), 'Should explain that project allowlists do not authorize groups');
|
|
380
|
-
console.log(' ✓ Rejected group:123 when GITLAB_ALLOWED_PROJECT_IDS is set');
|
|
381
|
-
}
|
|
382
|
-
assert.ok(didThrow, 'Should have rejected explicit group access');
|
|
383
|
-
});
|
|
384
|
-
});
|
|
385
|
-
describe('getEffectiveProjectId - With multiple GITLAB_ALLOWED_PROJECT_IDS', () => {
|
|
386
|
-
let mcpUrl;
|
|
387
|
-
let mockGitLab;
|
|
388
|
-
let servers = [];
|
|
389
|
-
let client;
|
|
390
|
-
before(async () => {
|
|
391
|
-
// Start mock GitLab server
|
|
392
|
-
const mockPort = await findMockServerPort();
|
|
393
|
-
mockGitLab = new MockGitLabServer({
|
|
394
|
-
port: mockPort,
|
|
395
|
-
validTokens: [MOCK_TOKEN]
|
|
396
|
-
});
|
|
397
|
-
await mockGitLab.start();
|
|
398
|
-
const mockGitLabUrl = mockGitLab.getUrl();
|
|
399
|
-
// Start MCP server WITH multiple GITLAB_ALLOWED_PROJECT_IDS
|
|
400
|
-
const mcpPort = await findAvailablePort(3300);
|
|
401
|
-
const server = await launchServer({
|
|
402
|
-
mode: TransportMode.STREAMABLE_HTTP,
|
|
403
|
-
port: mcpPort,
|
|
404
|
-
timeout: 5000,
|
|
405
|
-
env: {
|
|
406
|
-
REMOTE_AUTHORIZATION: 'true',
|
|
407
|
-
GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
|
|
408
|
-
GITLAB_ALLOWED_PROJECT_IDS: `${DEFAULT_PROJECT_ID},${OTHER_PROJECT_ID}`,
|
|
409
|
-
GITLAB_READ_ONLY_MODE: 'true',
|
|
410
|
-
}
|
|
411
|
-
});
|
|
412
|
-
servers.push(server);
|
|
413
|
-
mcpUrl = `http://${HOST}:${mcpPort}/mcp`;
|
|
414
|
-
client = new CustomHeaderClient({
|
|
415
|
-
authorization: `Bearer ${MOCK_TOKEN}`,
|
|
416
|
-
});
|
|
417
|
-
await client.connect(mcpUrl);
|
|
418
|
-
console.log(`Mock GitLab: ${mockGitLabUrl}`);
|
|
419
|
-
console.log(`MCP Server: ${mcpUrl}`);
|
|
420
|
-
console.log(`Allowed Projects: ${DEFAULT_PROJECT_ID},${OTHER_PROJECT_ID}`);
|
|
421
|
-
});
|
|
422
|
-
after(async () => {
|
|
423
|
-
if (client) {
|
|
424
|
-
await client.disconnect();
|
|
425
|
-
}
|
|
426
|
-
cleanupServers(servers);
|
|
427
|
-
if (mockGitLab) {
|
|
428
|
-
await mockGitLab.stop();
|
|
429
|
-
}
|
|
430
|
-
});
|
|
431
|
-
test('should require explicit project_id when multiple projects allowed', async () => {
|
|
432
|
-
try {
|
|
433
|
-
await client.callTool('get_project', {
|
|
434
|
-
project_id: ''
|
|
435
|
-
});
|
|
436
|
-
assert.fail('Should have required explicit project_id');
|
|
437
|
-
}
|
|
438
|
-
catch (error) {
|
|
439
|
-
assert.ok(error instanceof Error);
|
|
440
|
-
assert.ok(error.message.includes('Please specify a project ID'), 'Should require project ID');
|
|
441
|
-
console.log(' ✓ Correctly required explicit project_id');
|
|
442
|
-
}
|
|
443
|
-
});
|
|
444
|
-
test('should allow access to first allowed project', async () => {
|
|
445
|
-
const result = await client.callTool('get_project', {
|
|
446
|
-
project_id: DEFAULT_PROJECT_ID
|
|
447
|
-
});
|
|
448
|
-
assert.ok(result.content, 'Should have content');
|
|
449
|
-
const content = result.content[0];
|
|
450
|
-
assert.ok('text' in content, 'Content should have text');
|
|
451
|
-
const project = JSON.parse(content.text);
|
|
452
|
-
assert.strictEqual(project.id.toString(), DEFAULT_PROJECT_ID, 'Should allow first project');
|
|
453
|
-
console.log(` ✓ Allowed access to first project ${DEFAULT_PROJECT_ID}`);
|
|
454
|
-
});
|
|
455
|
-
test('should allow access to second allowed project', async () => {
|
|
456
|
-
const result = await client.callTool('get_project', {
|
|
457
|
-
project_id: OTHER_PROJECT_ID
|
|
458
|
-
});
|
|
459
|
-
assert.ok(result.content, 'Should have content');
|
|
460
|
-
const content = result.content[0];
|
|
461
|
-
assert.ok('text' in content, 'Content should have text');
|
|
462
|
-
const project = JSON.parse(content.text);
|
|
463
|
-
assert.strictEqual(project.id.toString(), OTHER_PROJECT_ID, 'Should allow second project');
|
|
464
|
-
console.log(` ✓ Allowed access to second project ${OTHER_PROJECT_ID}`);
|
|
465
|
-
});
|
|
466
|
-
});
|
|
467
|
-
describe('GITLAB_PROJECT_ID guards repository and group mutators', () => {
|
|
468
|
-
let mcpUrl;
|
|
469
|
-
let mockGitLab;
|
|
470
|
-
let servers = [];
|
|
471
|
-
let client;
|
|
472
|
-
before(async () => {
|
|
473
|
-
const mockPort = await findMockServerPort();
|
|
474
|
-
mockGitLab = new MockGitLabServer({
|
|
475
|
-
port: mockPort,
|
|
476
|
-
validTokens: [MOCK_TOKEN]
|
|
477
|
-
});
|
|
478
|
-
await mockGitLab.start();
|
|
479
|
-
const mockGitLabUrl = mockGitLab.getUrl();
|
|
480
|
-
const mcpPort = await findAvailablePort(3400);
|
|
481
|
-
const server = await launchServer({
|
|
482
|
-
mode: TransportMode.STREAMABLE_HTTP,
|
|
483
|
-
port: mcpPort,
|
|
484
|
-
timeout: 5000,
|
|
485
|
-
env: {
|
|
486
|
-
REMOTE_AUTHORIZATION: 'true',
|
|
487
|
-
GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
|
|
488
|
-
GITLAB_PROJECT_ID: DEFAULT_PROJECT_ID,
|
|
489
|
-
GITLAB_TOOLSETS: 'variables,repositories,groups,projects',
|
|
490
|
-
}
|
|
491
|
-
});
|
|
492
|
-
servers.push(server);
|
|
493
|
-
mcpUrl = `http://${HOST}:${mcpPort}/mcp`;
|
|
494
|
-
client = new CustomHeaderClient({
|
|
495
|
-
authorization: `Bearer ${MOCK_TOKEN}`,
|
|
496
|
-
});
|
|
497
|
-
await client.connect(mcpUrl);
|
|
498
|
-
});
|
|
499
|
-
after(async () => {
|
|
500
|
-
if (client)
|
|
501
|
-
await client.disconnect();
|
|
502
|
-
cleanupServers(servers);
|
|
503
|
-
if (mockGitLab)
|
|
504
|
-
await mockGitLab.stop();
|
|
505
|
-
});
|
|
506
|
-
test('should reject create_repository when GITLAB_PROJECT_ID is set', async () => {
|
|
507
|
-
try {
|
|
508
|
-
await client.callTool('create_repository', { name: 'test-repo' });
|
|
509
|
-
assert.fail('Should have rejected create_repository');
|
|
510
|
-
}
|
|
511
|
-
catch (error) {
|
|
512
|
-
assert.ok(error instanceof Error);
|
|
513
|
-
assert.ok(error.message.includes('create_repository is not allowed'), 'Should mention create_repository');
|
|
514
|
-
}
|
|
515
|
-
});
|
|
516
|
-
test('should reject fork_repository when GITLAB_PROJECT_ID is set', async () => {
|
|
517
|
-
try {
|
|
518
|
-
await client.callTool('fork_repository', { project_id: '999' });
|
|
519
|
-
assert.fail('Should have rejected fork_repository');
|
|
520
|
-
}
|
|
521
|
-
catch (error) {
|
|
522
|
-
assert.ok(error instanceof Error);
|
|
523
|
-
assert.ok(error.message.includes('fork_repository is not allowed'), 'Should mention fork_repository');
|
|
524
|
-
}
|
|
525
|
-
});
|
|
526
|
-
test('should reject create_group when GITLAB_PROJECT_ID is set', async () => {
|
|
527
|
-
try {
|
|
528
|
-
await client.callTool('create_group', { name: 'test-group', path: 'test-group' });
|
|
529
|
-
assert.fail('Should have rejected create_group');
|
|
530
|
-
}
|
|
531
|
-
catch (error) {
|
|
532
|
-
assert.ok(error instanceof Error);
|
|
533
|
-
assert.ok(error.message.includes('create_group is not allowed'), 'Should mention create_group');
|
|
534
|
-
}
|
|
535
|
-
});
|
|
536
|
-
test('should reject list_group_variables when GITLAB_PROJECT_ID is set', async () => {
|
|
537
|
-
try {
|
|
538
|
-
await client.callTool('list_group_variables', { group_id: 'my-group' });
|
|
539
|
-
assert.fail('Should have rejected list_group_variables');
|
|
540
|
-
}
|
|
541
|
-
catch (error) {
|
|
542
|
-
assert.ok(error instanceof Error);
|
|
543
|
-
assert.ok(error.message.includes('list_group_variables is not allowed'), 'Should mention list_group_variables');
|
|
544
|
-
}
|
|
545
|
-
});
|
|
546
|
-
test('should reject get_group_variable when GITLAB_PROJECT_ID is set', async () => {
|
|
547
|
-
try {
|
|
548
|
-
await client.callTool('get_group_variable', { group_id: 'my-group', key: 'SHARED_SECRET' });
|
|
549
|
-
assert.fail('Should have rejected get_group_variable');
|
|
550
|
-
}
|
|
551
|
-
catch (error) {
|
|
552
|
-
assert.ok(error instanceof Error);
|
|
553
|
-
assert.ok(error.message.includes('get_group_variable is not allowed'), 'Should mention get_group_variable');
|
|
554
|
-
}
|
|
555
|
-
});
|
|
556
|
-
test('should allow get_project (non-mutator) when GITLAB_PROJECT_ID is set', async () => {
|
|
557
|
-
const result = await client.callTool('get_project', { project_id: '' });
|
|
558
|
-
assert.ok(result.content, 'Should have content');
|
|
559
|
-
const content = result.content[0];
|
|
560
|
-
assert.ok('text' in content, 'Content should have text');
|
|
561
|
-
const project = JSON.parse(content.text);
|
|
562
|
-
assert.strictEqual(project.id.toString(), DEFAULT_PROJECT_ID, 'Should use default project');
|
|
563
|
-
});
|
|
564
|
-
});
|
|
565
|
-
describe('GITLAB_ALLOWED_PROJECT_IDS guards repository and group mutators (allowlist-only, no GITLAB_PROJECT_ID)', () => {
|
|
566
|
-
let mcpUrl;
|
|
567
|
-
let mockGitLab;
|
|
568
|
-
let servers = [];
|
|
569
|
-
let client;
|
|
570
|
-
before(async () => {
|
|
571
|
-
const mockPort = await findMockServerPort();
|
|
572
|
-
mockGitLab = new MockGitLabServer({
|
|
573
|
-
port: mockPort,
|
|
574
|
-
validTokens: [MOCK_TOKEN]
|
|
575
|
-
});
|
|
576
|
-
await mockGitLab.start();
|
|
577
|
-
const mockGitLabUrl = mockGitLab.getUrl();
|
|
578
|
-
const mcpPort = await findAvailablePort(3600);
|
|
579
|
-
const server = await launchServer({
|
|
580
|
-
mode: TransportMode.STREAMABLE_HTTP,
|
|
581
|
-
port: mcpPort,
|
|
582
|
-
timeout: 5000,
|
|
583
|
-
env: {
|
|
584
|
-
REMOTE_AUTHORIZATION: 'true',
|
|
585
|
-
GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
|
|
586
|
-
GITLAB_ALLOWED_PROJECT_IDS: DEFAULT_PROJECT_ID,
|
|
587
|
-
GITLAB_TOOLSETS: 'variables,merge_requests,issues,pipelines,repositories,groups,projects',
|
|
588
|
-
}
|
|
589
|
-
});
|
|
590
|
-
servers.push(server);
|
|
591
|
-
mcpUrl = `http://${HOST}:${mcpPort}/mcp`;
|
|
592
|
-
client = new CustomHeaderClient({
|
|
593
|
-
authorization: `Bearer ${MOCK_TOKEN}`,
|
|
594
|
-
});
|
|
595
|
-
await client.connect(mcpUrl);
|
|
596
|
-
});
|
|
597
|
-
after(async () => {
|
|
598
|
-
if (client)
|
|
599
|
-
await client.disconnect();
|
|
600
|
-
cleanupServers(servers);
|
|
601
|
-
if (mockGitLab)
|
|
602
|
-
await mockGitLab.stop();
|
|
603
|
-
});
|
|
604
|
-
test('should reject create_repository with GITLAB_ALLOWED_PROJECT_IDS', async () => {
|
|
605
|
-
try {
|
|
606
|
-
await client.callTool('create_repository', { name: 'test-repo' });
|
|
607
|
-
assert.fail('Should have rejected create_repository');
|
|
608
|
-
}
|
|
609
|
-
catch (error) {
|
|
610
|
-
assert.ok(error instanceof Error);
|
|
611
|
-
assert.ok(error.message.includes('create_repository is not allowed'), 'Should mention create_repository');
|
|
612
|
-
}
|
|
613
|
-
});
|
|
614
|
-
test('should reject fork_repository with GITLAB_ALLOWED_PROJECT_IDS', async () => {
|
|
615
|
-
try {
|
|
616
|
-
await client.callTool('fork_repository', { project_id: '999' });
|
|
617
|
-
assert.fail('Should have rejected fork_repository');
|
|
618
|
-
}
|
|
619
|
-
catch (error) {
|
|
620
|
-
assert.ok(error instanceof Error);
|
|
621
|
-
assert.ok(error.message.includes('fork_repository is not allowed'), 'Should mention fork_repository');
|
|
622
|
-
}
|
|
623
|
-
});
|
|
624
|
-
test('should reject create_group with GITLAB_ALLOWED_PROJECT_IDS', async () => {
|
|
625
|
-
try {
|
|
626
|
-
await client.callTool('create_group', { name: 'test-group', path: 'test-group' });
|
|
627
|
-
assert.fail('Should have rejected create_group');
|
|
628
|
-
}
|
|
629
|
-
catch (error) {
|
|
630
|
-
assert.ok(error instanceof Error);
|
|
631
|
-
assert.ok(error.message.includes('create_group is not allowed'), 'Should mention create_group');
|
|
632
|
-
}
|
|
633
|
-
});
|
|
634
|
-
test('should reject list_group_variables with GITLAB_ALLOWED_PROJECT_IDS', async () => {
|
|
635
|
-
try {
|
|
636
|
-
await client.callTool('list_group_variables', { group_id: 'my-group' });
|
|
637
|
-
assert.fail('Should have rejected list_group_variables');
|
|
638
|
-
}
|
|
639
|
-
catch (error) {
|
|
640
|
-
assert.ok(error instanceof Error);
|
|
641
|
-
assert.ok(error.message.includes('list_group_variables is not allowed'), 'Should mention list_group_variables');
|
|
642
|
-
}
|
|
643
|
-
});
|
|
644
|
-
test('should reject get_group_variable with GITLAB_ALLOWED_PROJECT_IDS', async () => {
|
|
645
|
-
try {
|
|
646
|
-
await client.callTool('get_group_variable', { group_id: 'my-group', key: 'SHARED_SECRET' });
|
|
647
|
-
assert.fail('Should have rejected get_group_variable');
|
|
648
|
-
}
|
|
649
|
-
catch (error) {
|
|
650
|
-
assert.ok(error instanceof Error);
|
|
651
|
-
assert.ok(error.message.includes('get_group_variable is not allowed'), 'Should mention get_group_variable');
|
|
652
|
-
}
|
|
653
|
-
});
|
|
654
|
-
test('should allow get_project (non-mutator) with GITLAB_ALLOWED_PROJECT_IDS', async () => {
|
|
655
|
-
const result = await client.callTool('get_project', { project_id: '' });
|
|
656
|
-
assert.ok(result.content, 'Should have content');
|
|
657
|
-
const content = result.content[0];
|
|
658
|
-
assert.ok('text' in content, 'Content should have text');
|
|
659
|
-
const project = JSON.parse(content.text);
|
|
660
|
-
assert.strictEqual(project.id.toString(), DEFAULT_PROJECT_ID, 'Should use default project');
|
|
661
|
-
});
|
|
662
|
-
test('should enforce the project allowlist for get_draft_note', async () => {
|
|
663
|
-
await assert.rejects(client.callTool('get_draft_note', {
|
|
664
|
-
project_id: OTHER_PROJECT_ID,
|
|
665
|
-
merge_request_iid: '1',
|
|
666
|
-
draft_note_id: '1',
|
|
667
|
-
}), /Access denied/);
|
|
668
|
-
});
|
|
669
|
-
test('should reject dot path segments before making GitLab requests', async () => {
|
|
670
|
-
const calls = [
|
|
671
|
-
{
|
|
672
|
-
name: 'get_draft_note',
|
|
673
|
-
args: { project_id: DEFAULT_PROJECT_ID, merge_request_iid: '..', draft_note_id: '1' },
|
|
674
|
-
},
|
|
675
|
-
{
|
|
676
|
-
name: 'get_job_artifact_file',
|
|
677
|
-
args: { project_id: DEFAULT_PROJECT_ID, job_id: '1', artifact_path: '../file.txt' },
|
|
678
|
-
},
|
|
679
|
-
{
|
|
680
|
-
name: 'list_issue_emoji_reactions',
|
|
681
|
-
args: { project_id: DEFAULT_PROJECT_ID, issue_iid: '..' },
|
|
682
|
-
},
|
|
683
|
-
];
|
|
684
|
-
for (const { name, args } of calls) {
|
|
685
|
-
await assert.rejects(client.callTool(name, args), /Cannot use value as a GitLab URL path segment/);
|
|
686
|
-
}
|
|
687
|
-
});
|
|
688
|
-
});
|
|
689
|
-
describe('GITLAB_READ_ONLY_MODE enforces read-only for all write tools', () => {
|
|
690
|
-
let mcpUrl;
|
|
691
|
-
let mockGitLab;
|
|
692
|
-
let servers = [];
|
|
693
|
-
let client;
|
|
694
|
-
before(async () => {
|
|
695
|
-
const mockPort = await findMockServerPort();
|
|
696
|
-
mockGitLab = new MockGitLabServer({
|
|
697
|
-
port: mockPort,
|
|
698
|
-
validTokens: [MOCK_TOKEN]
|
|
699
|
-
});
|
|
700
|
-
await mockGitLab.start();
|
|
701
|
-
const mockGitLabUrl = mockGitLab.getUrl();
|
|
702
|
-
const mcpPort = await findAvailablePort(3500);
|
|
703
|
-
const server = await launchServer({
|
|
704
|
-
mode: TransportMode.STREAMABLE_HTTP,
|
|
705
|
-
port: mcpPort,
|
|
706
|
-
timeout: 5000,
|
|
707
|
-
env: {
|
|
708
|
-
REMOTE_AUTHORIZATION: 'true',
|
|
709
|
-
GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
|
|
710
|
-
GITLAB_READ_ONLY_MODE: 'true',
|
|
711
|
-
}
|
|
712
|
-
});
|
|
713
|
-
servers.push(server);
|
|
714
|
-
mcpUrl = `http://${HOST}:${mcpPort}/mcp`;
|
|
715
|
-
client = new CustomHeaderClient({
|
|
716
|
-
authorization: `Bearer ${MOCK_TOKEN}`,
|
|
717
|
-
});
|
|
718
|
-
await client.connect(mcpUrl);
|
|
719
|
-
});
|
|
720
|
-
after(async () => {
|
|
721
|
-
if (client)
|
|
722
|
-
await client.disconnect();
|
|
723
|
-
cleanupServers(servers);
|
|
724
|
-
if (mockGitLab)
|
|
725
|
-
await mockGitLab.stop();
|
|
726
|
-
});
|
|
727
|
-
test('should reject create_group in read-only mode (no project ID)', async () => {
|
|
728
|
-
try {
|
|
729
|
-
await client.callTool('create_group', { name: 'test-group', path: 'test-group' });
|
|
730
|
-
assert.fail('Should have rejected create_group in read-only mode');
|
|
731
|
-
}
|
|
732
|
-
catch (error) {
|
|
733
|
-
assert.ok(error instanceof Error);
|
|
734
|
-
assert.ok(error.message.includes('create_group is not allowed'), 'Should mention create_group');
|
|
735
|
-
}
|
|
736
|
-
});
|
|
737
|
-
test('should reject create_repository in read-only mode', async () => {
|
|
738
|
-
try {
|
|
739
|
-
await client.callTool('create_repository', { name: 'test-repo' });
|
|
740
|
-
assert.fail('Should have rejected create_repository in read-only mode');
|
|
741
|
-
}
|
|
742
|
-
catch (error) {
|
|
743
|
-
assert.ok(error instanceof Error);
|
|
744
|
-
assert.ok(error.message.includes('create_repository is not allowed'), 'Should mention create_repository');
|
|
745
|
-
}
|
|
746
|
-
});
|
|
747
|
-
test('should allow get_project (read-only) in read-only mode', async () => {
|
|
748
|
-
const result = await client.callTool('get_project', { project_id: DEFAULT_PROJECT_ID });
|
|
749
|
-
assert.ok(result.content, 'Should have content');
|
|
750
|
-
});
|
|
751
|
-
});
|
|
752
|
-
describe('GITLAB_PROJECT_ID guards dependency proxy tools', () => {
|
|
753
|
-
let mcpUrl;
|
|
754
|
-
let mockGitLab;
|
|
755
|
-
let servers = [];
|
|
756
|
-
let client;
|
|
757
|
-
before(async () => {
|
|
758
|
-
const mockPort = await findMockServerPort();
|
|
759
|
-
mockGitLab = new MockGitLabServer({
|
|
760
|
-
port: mockPort,
|
|
761
|
-
validTokens: [MOCK_TOKEN],
|
|
762
|
-
});
|
|
763
|
-
await mockGitLab.start();
|
|
764
|
-
const mockGitLabUrl = mockGitLab.getUrl();
|
|
765
|
-
const mcpPort = await findAvailablePort(3500);
|
|
766
|
-
const server = await launchServer({
|
|
767
|
-
mode: TransportMode.STREAMABLE_HTTP,
|
|
768
|
-
port: mcpPort,
|
|
769
|
-
timeout: 5000,
|
|
770
|
-
env: {
|
|
771
|
-
REMOTE_AUTHORIZATION: 'true',
|
|
772
|
-
GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
|
|
773
|
-
GITLAB_PROJECT_ID: DEFAULT_PROJECT_ID,
|
|
774
|
-
GITLAB_TOOLSETS: 'dependency_proxy',
|
|
775
|
-
},
|
|
776
|
-
});
|
|
777
|
-
servers.push(server);
|
|
778
|
-
mcpUrl = `http://${HOST}:${mcpPort}/mcp`;
|
|
779
|
-
client = new CustomHeaderClient({
|
|
780
|
-
authorization: `Bearer ${MOCK_TOKEN}`,
|
|
781
|
-
});
|
|
782
|
-
await client.connect(mcpUrl);
|
|
783
|
-
});
|
|
784
|
-
after(async () => {
|
|
785
|
-
if (client)
|
|
786
|
-
await client.disconnect();
|
|
787
|
-
cleanupServers(servers);
|
|
788
|
-
if (mockGitLab)
|
|
789
|
-
await mockGitLab.stop();
|
|
790
|
-
});
|
|
791
|
-
test('should reject get_dependency_proxy_settings when GITLAB_PROJECT_ID is set', async () => {
|
|
792
|
-
try {
|
|
793
|
-
await client.callTool('get_dependency_proxy_settings', { group_id: 'my-group' });
|
|
794
|
-
assert.fail('Should have rejected get_dependency_proxy_settings');
|
|
795
|
-
}
|
|
796
|
-
catch (error) {
|
|
797
|
-
assert.ok(error instanceof Error);
|
|
798
|
-
assert.ok(error.message.includes('get_dependency_proxy_settings is not allowed'), 'Should mention get_dependency_proxy_settings');
|
|
799
|
-
}
|
|
800
|
-
});
|
|
801
|
-
test('should reject purge_dependency_proxy_cache when GITLAB_PROJECT_ID is set', async () => {
|
|
802
|
-
try {
|
|
803
|
-
await client.callTool('purge_dependency_proxy_cache', { group_id: 'my-group' });
|
|
804
|
-
assert.fail('Should have rejected purge_dependency_proxy_cache');
|
|
805
|
-
}
|
|
806
|
-
catch (error) {
|
|
807
|
-
assert.ok(error instanceof Error);
|
|
808
|
-
assert.ok(error.message.includes('purge_dependency_proxy_cache is not allowed'), 'Should mention purge_dependency_proxy_cache');
|
|
809
|
-
}
|
|
810
|
-
});
|
|
811
|
-
});
|
|
812
|
-
}); // end wrapper describe
|