@zereight/mcp-gitlab 2.1.70 → 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.
Files changed (157) hide show
  1. package/README.ko.md +3 -1
  2. package/README.md +6 -2
  3. package/README.pt-BR.md +1 -1
  4. package/README.zh-CN.md +3 -1
  5. package/build/config.js +12 -4
  6. package/build/deprecated-env.js +47 -0
  7. package/build/index.js +369 -184
  8. package/build/oauth-device-flow.js +80 -0
  9. package/build/oauth.js +135 -18
  10. package/build/schemas.js +1 -1
  11. package/build/server/remote-http-stack.js +39 -0
  12. package/build/tools/discover-activation.js +38 -0
  13. package/build/tools/exposed-tools.js +35 -0
  14. package/build/tools/listed-tools.js +76 -0
  15. package/build/tools/registry.js +226 -57
  16. package/build/tools/tool-profile.js +66 -0
  17. package/build/utils/compact-tool-allowlist.js +14 -0
  18. package/build/utils/logger.js +34 -12
  19. package/package.json +3 -3
  20. package/build/scripts/check-skill-sync.js +0 -143
  21. package/build/scripts/generate-server-card.js +0 -39
  22. package/build/scripts/generate-tool-coverage.js +0 -58
  23. package/build/scripts/generate-tool-docs.js +0 -430
  24. package/build/scripts/tool-coverage/coverage.js +0 -341
  25. package/build/scripts/tool-coverage/coverage.test.js +0 -142
  26. package/build/test/callback-proxy-tests.js +0 -321
  27. package/build/test/cli/args.test.js +0 -175
  28. package/build/test/cli/boolean-flags.test.js +0 -106
  29. package/build/test/cli/compact-mcp-result.test.js +0 -93
  30. package/build/test/cli/credentials.test.js +0 -153
  31. package/build/test/cli/exposure.test.js +0 -104
  32. package/build/test/cli/load-config-snapshot.js +0 -5
  33. package/build/test/cli/output.test.js +0 -74
  34. package/build/test/cli/replay-command.test.js +0 -37
  35. package/build/test/cli/router.test.js +0 -183
  36. package/build/test/client-pool-test.js +0 -109
  37. package/build/test/clients/client.js +0 -34
  38. package/build/test/clients/custom-header-client.js +0 -122
  39. package/build/test/clients/sse-client.js +0 -95
  40. package/build/test/clients/stdio-client.js +0 -113
  41. package/build/test/clients/streamable-http-client.js +0 -95
  42. package/build/test/config-allowed-groups.test.js +0 -97
  43. package/build/test/create-commit-actions.test.js +0 -102
  44. package/build/test/draft-note-schema.test.js +0 -44
  45. package/build/test/dynamic-api-url-allowlist.test.js +0 -121
  46. package/build/test/dynamic-api-url-test.js +0 -304
  47. package/build/test/dynamic-routing-tests.js +0 -495
  48. package/build/test/get-merge-request-discussion-schema.test.js +0 -30
  49. package/build/test/gitlab-api-tool-errors.test.js +0 -112
  50. package/build/test/gitlab-artifact-entry-schema.test.js +0 -44
  51. package/build/test/gitlab-diff-schema.test.js +0 -36
  52. package/build/test/gitlab-pipeline-schedule-schema.test.js +0 -32
  53. package/build/test/gitlab-tag-schema.test.js +0 -51
  54. package/build/test/group-milestone-schema.test.js +0 -45
  55. package/build/test/mcp-oauth-tests.js +0 -905
  56. package/build/test/mcp-server-name.test.js +0 -87
  57. package/build/test/merge-merge-request-schema.test.js +0 -75
  58. package/build/test/multi-server-test.js +0 -190
  59. package/build/test/no-proxy-integration-test.js +0 -183
  60. package/build/test/no-proxy-test.js +0 -149
  61. package/build/test/nullable-gitlab-response-fields.test.js +0 -37
  62. package/build/test/nullish-tool-arguments-schema.test.js +0 -118
  63. package/build/test/oauth-device-flow-tests.js +0 -450
  64. package/build/test/oauth-download-url.test.js +0 -157
  65. package/build/test/oauth-startup.test.js +0 -204
  66. package/build/test/oauth-tests.js +0 -428
  67. package/build/test/path-segment-encoding.test.js +0 -98
  68. package/build/test/remote-auth-simple-test.js +0 -277
  69. package/build/test/remote-auth-tests.js +0 -314
  70. package/build/test/response-masking.test.js +0 -445
  71. package/build/test/schema-tests.js +0 -1679
  72. package/build/test/server/metrics.test.js +0 -47
  73. package/build/test/server/request-helpers.test.js +0 -48
  74. package/build/test/server/version.test.js +0 -13
  75. package/build/test/sse-auth-guard.test.js +0 -96
  76. package/build/test/sse-session-limits.test.js +0 -273
  77. package/build/test/stateless/callback-proxy.test.js +0 -510
  78. package/build/test/stateless/client-id.test.js +0 -177
  79. package/build/test/stateless/codec.test.js +0 -328
  80. package/build/test/stateless/config-ttl.test.js +0 -149
  81. package/build/test/stateless/consumed-proxy-code-cache.test.js +0 -150
  82. package/build/test/stateless/session-id-integration.test.js +0 -675
  83. package/build/test/stateless/session-id.test.js +0 -157
  84. package/build/test/streamable-http-concurrent-session.test.js +0 -171
  85. package/build/test/streamable-http-dns-rebinding.test.js +0 -185
  86. package/build/test/streamable-http-sse-stream.test.js +0 -350
  87. package/build/test/streamable-http-static-token-auth.test.js +0 -157
  88. package/build/test/streamable-http-unauthenticated-discovery.test.js +0 -207
  89. package/build/test/test-all-transport-server.js +0 -255
  90. package/build/test/test-auth-retry.js +0 -195
  91. package/build/test/test-blank-filters.js +0 -156
  92. package/build/test/test-ci-catalog.js +0 -177
  93. package/build/test/test-ci-lint.js +0 -191
  94. package/build/test/test-ci-variables.js +0 -309
  95. package/build/test/test-create-repository.js +0 -120
  96. package/build/test/test-dependency-proxy.js +0 -195
  97. package/build/test/test-deployment-tools.js +0 -463
  98. package/build/test/test-download-attachment.js +0 -190
  99. package/build/test/test-dynamic-project-scope.js +0 -524
  100. package/build/test/test-empty-toolsets.js +0 -535
  101. package/build/test/test-get-file-blame.js +0 -145
  102. package/build/test/test-get-file-contents.js +0 -128
  103. package/build/test/test-geteffectiveprojectid.js +0 -804
  104. package/build/test/test-health-check.js +0 -115
  105. package/build/test/test-issue-description-patch.js +0 -256
  106. package/build/test/test-job-artifacts.js +0 -275
  107. package/build/test/test-json-schema.js +0 -166
  108. package/build/test/test-list-group-members.js +0 -150
  109. package/build/test/test-list-group-merge-requests.js +0 -165
  110. package/build/test/test-list-issues.js +0 -170
  111. package/build/test/test-list-merge-requests.js +0 -201
  112. package/build/test/test-list-milestones.js +0 -145
  113. package/build/test/test-list-project-members.js +0 -132
  114. package/build/test/test-merge-request-approval-state-tools.js +0 -213
  115. package/build/test/test-merge-request-approvals.js +0 -187
  116. package/build/test/test-merge-request-pipelines.js +0 -106
  117. package/build/test/test-mr-diffs-filter.js +0 -132
  118. package/build/test/test-mr-file-diffs.js +0 -251
  119. package/build/test/test-oauth-proxy-rate-limit.js +0 -133
  120. package/build/test/test-orbit.js +0 -155
  121. package/build/test/test-permission-mode.js +0 -424
  122. package/build/test/test-pipeline-schedules.js +0 -484
  123. package/build/test/test-protected-branches.js +0 -174
  124. package/build/test/test-remote-downloads.js +0 -510
  125. package/build/test/test-search-code.js +0 -275
  126. package/build/test/test-snippets.js +0 -735
  127. package/build/test/test-tags.js +0 -220
  128. package/build/test/test-todos.js +0 -195
  129. package/build/test/test-token-optimizations.js +0 -691
  130. package/build/test/test-toolset-filtering.js +0 -499
  131. package/build/test/test-update-issue-slim.js +0 -141
  132. package/build/test/test-update-project.js +0 -112
  133. package/build/test/test-upload-markdown.js +0 -206
  134. package/build/test/test-vulnerabilities.js +0 -399
  135. package/build/test/test-webhooks.js +0 -218
  136. package/build/test/tool-description-quality.test.js +0 -50
  137. package/build/test/utils/bulk-publish-options.test.js +0 -32
  138. package/build/test/utils/download-token.test.js +0 -35
  139. package/build/test/utils/forwarded-public-base-url.test.js +0 -46
  140. package/build/test/utils/gitlab-instance-version.test.js +0 -32
  141. package/build/test/utils/gitlab-version-gate.test.js +0 -74
  142. package/build/test/utils/graphql-query.test.js +0 -168
  143. package/build/test/utils/helpers-path.test.js +0 -104
  144. package/build/test/utils/jmespath-tool-result.test.js +0 -91
  145. package/build/test/utils/masking.test.js +0 -384
  146. package/build/test/utils/merge-request-position.test.js +0 -57
  147. package/build/test/utils/mock-gitlab-server.js +0 -614
  148. package/build/test/utils/proxy-client-ip.test.js +0 -28
  149. package/build/test/utils/redact-sensitive.test.js +0 -32
  150. package/build/test/utils/safe-redirect-fetch.test.js +0 -463
  151. package/build/test/utils/server-launcher.js +0 -231
  152. package/build/test/utils/tool-args.test.js +0 -210
  153. package/build/test/utils/url.test.js +0 -141
  154. package/build/test/utils/version-check.test.js +0 -239
  155. package/build/test/utils/wiki-title.test.js +0 -21
  156. package/build/test-note.js +0 -54
  157. package/build/test-resolve-issue-note.js +0 -127
@@ -1,251 +0,0 @@
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
- const MOCK_TOKEN = 'glpat-mock-token-12345';
6
- const TEST_PROJECT_ID = '123';
7
- const TEST_MR_IID = '1';
8
- // Helper to call list_merge_request_changed_files
9
- async function callListMergeRequestChangedFiles(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
- }
18
- });
19
- let output = '';
20
- let errorOutput = '';
21
- proc.stdout?.on('data', d => output += d);
22
- proc.stderr?.on('data', d => errorOutput += d);
23
- proc.on('close', (code) => {
24
- if (code !== 0)
25
- return reject(new Error(`Process exited with code ${code}: ${errorOutput}`));
26
- // Find the JSON line in stdout
27
- const line = output.split('\n').find(l => l.startsWith('{'));
28
- if (!line)
29
- return reject(new Error('No JSON output found'));
30
- try {
31
- const response = JSON.parse(line);
32
- if (response.error) {
33
- reject(response.error);
34
- }
35
- else {
36
- // Parse the tool result content
37
- const content = response.result?.content?.[0]?.text;
38
- if (content) {
39
- try {
40
- resolve(JSON.parse(content));
41
- }
42
- catch (e) {
43
- reject(new Error(`Failed to parse tool output JSON: ${content}`));
44
- }
45
- }
46
- else {
47
- resolve(response.result);
48
- }
49
- }
50
- }
51
- catch (e) {
52
- reject(e);
53
- }
54
- });
55
- proc.stdin?.end(JSON.stringify({
56
- jsonrpc: "2.0", id: 1, method: "tools/call",
57
- params: { name: "list_merge_request_changed_files", arguments: args }
58
- }) + '\n');
59
- });
60
- }
61
- // Helper to call get_merge_request_file_diff
62
- async function callGetMergeRequestFileDiff(args = {}, env) {
63
- return new Promise((resolve, reject) => {
64
- const proc = spawn('node', ['build/index.js'], {
65
- stdio: ['pipe', 'pipe', 'pipe'],
66
- env: {
67
- ...process.env,
68
- ...env,
69
- GITLAB_READ_ONLY_MODE: 'true'
70
- }
71
- });
72
- let output = '';
73
- let errorOutput = '';
74
- proc.stdout?.on('data', d => output += d);
75
- proc.stderr?.on('data', d => errorOutput += d);
76
- proc.on('close', (code) => {
77
- if (code !== 0)
78
- return reject(new Error(`Process exited with code ${code}: ${errorOutput}`));
79
- // Find the JSON line in stdout
80
- const line = output.split('\n').find(l => l.startsWith('{'));
81
- if (!line)
82
- return reject(new Error('No JSON output found'));
83
- try {
84
- const response = JSON.parse(line);
85
- if (response.error) {
86
- reject(response.error);
87
- }
88
- else {
89
- // Parse the tool result content
90
- const content = response.result?.content?.[0]?.text;
91
- if (content) {
92
- try {
93
- resolve(JSON.parse(content));
94
- }
95
- catch (e) {
96
- reject(new Error(`Failed to parse tool output JSON: ${content}`));
97
- }
98
- }
99
- else {
100
- resolve(response.result);
101
- }
102
- }
103
- }
104
- catch (e) {
105
- reject(e);
106
- }
107
- });
108
- proc.stdin?.end(JSON.stringify({
109
- jsonrpc: "2.0", id: 1, method: "tools/call",
110
- params: { name: "get_merge_request_file_diff", arguments: args }
111
- }) + '\n');
112
- });
113
- }
114
- describe('list_merge_request_changed_files', () => {
115
- let mockGitLab;
116
- let mockGitLabUrl;
117
- before(async () => {
118
- const mockPort = await findMockServerPort();
119
- mockGitLab = new MockGitLabServer({
120
- port: mockPort,
121
- validTokens: [MOCK_TOKEN]
122
- });
123
- await mockGitLab.start();
124
- mockGitLabUrl = mockGitLab.getUrl();
125
- });
126
- after(async () => {
127
- await mockGitLab.stop();
128
- });
129
- test('returns all changed files without filtering', async () => {
130
- const files = await callListMergeRequestChangedFiles({ project_id: TEST_PROJECT_ID, merge_request_iid: TEST_MR_IID }, {
131
- GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
132
- GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN
133
- });
134
- assert.ok(Array.isArray(files), 'Response should be an array');
135
- assert.strictEqual(files.length, 4, 'Should return 4 files');
136
- // Check structure of returned files
137
- for (const file of files) {
138
- assert.ok(file.new_path !== undefined, 'Each file should have new_path');
139
- assert.ok(file.old_path !== undefined, 'Each file should have old_path');
140
- }
141
- assert.strictEqual(files[0].new_path, 'src/index.ts');
142
- assert.strictEqual(files[1].new_path, 'vendor/package/file.js');
143
- assert.strictEqual(files[2].new_path, 'README.md');
144
- assert.strictEqual(files[3].new_path, 'package-lock.json');
145
- });
146
- test('filters out vendor folder with ^vendor/ pattern', async () => {
147
- const files = await callListMergeRequestChangedFiles({
148
- project_id: TEST_PROJECT_ID,
149
- merge_request_iid: TEST_MR_IID,
150
- excluded_file_patterns: ['^vendor/']
151
- }, {
152
- GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
153
- GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN
154
- });
155
- assert.ok(Array.isArray(files), 'Response should be an array');
156
- assert.strictEqual(files.length, 3, 'Should return 3 files (vendor filtered out)');
157
- assert.strictEqual(files[0].new_path, 'src/index.ts');
158
- assert.strictEqual(files[1].new_path, 'README.md');
159
- assert.strictEqual(files[2].new_path, 'package-lock.json');
160
- });
161
- test('filters multiple patterns at once', async () => {
162
- const files = await callListMergeRequestChangedFiles({
163
- project_id: TEST_PROJECT_ID,
164
- merge_request_iid: TEST_MR_IID,
165
- excluded_file_patterns: ['^vendor/', 'package-lock\\.json']
166
- }, {
167
- GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
168
- GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN
169
- });
170
- assert.ok(Array.isArray(files), 'Response should be an array');
171
- assert.strictEqual(files.length, 2, 'Should return 2 files (vendor and package-lock filtered out)');
172
- assert.strictEqual(files[0].new_path, 'src/index.ts');
173
- assert.strictEqual(files[1].new_path, 'README.md');
174
- });
175
- });
176
- describe('get_merge_request_file_diff', () => {
177
- let mockGitLab;
178
- let mockGitLabUrl;
179
- before(async () => {
180
- const mockPort = await findMockServerPort();
181
- mockGitLab = new MockGitLabServer({
182
- port: mockPort,
183
- validTokens: [MOCK_TOKEN]
184
- });
185
- await mockGitLab.start();
186
- mockGitLabUrl = mockGitLab.getUrl();
187
- });
188
- after(async () => {
189
- await mockGitLab.stop();
190
- });
191
- test('returns diffs for existing files in single page', async () => {
192
- // Request only first few files that fit in one page (per_page=20)
193
- const fileDiff = await callGetMergeRequestFileDiff({
194
- project_id: TEST_PROJECT_ID,
195
- merge_request_iid: TEST_MR_IID,
196
- file_paths: ['src/index.ts', 'README.md']
197
- }, {
198
- GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
199
- GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN
200
- });
201
- assert.ok(Array.isArray(fileDiff), 'Response should be an array');
202
- assert.strictEqual(fileDiff.length, 2, 'Should return 2 diff results');
203
- // Check that we got the correct files
204
- const paths = fileDiff.map((f) => f.new_path || f.old_path).sort();
205
- assert.deepStrictEqual(paths, ['README.md', 'src/index.ts'].sort());
206
- });
207
- test('handles pagination when result spans multiple pages', async () => {
208
- // Request more files than fit in one page (we have 15 total, per_page defaults to 20)
209
- // but let's use a smaller per_page by testing with unidiff param
210
- const fileDiff = await callGetMergeRequestFileDiff({
211
- project_id: TEST_PROJECT_ID,
212
- merge_request_iid: TEST_MR_IID,
213
- file_paths: [
214
- 'src/index.ts',
215
- 'config/settings.yml',
216
- 'models/user.go'
217
- ]
218
- }, {
219
- GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
220
- GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN
221
- });
222
- assert.ok(Array.isArray(fileDiff), 'Response should be an array');
223
- assert.strictEqual(fileDiff.length, 3, 'Should return 3 diff results');
224
- });
225
- test('returns error objects for not-found files', async () => {
226
- // Request some existing + non-existing files
227
- const fileDiff = await callGetMergeRequestFileDiff({
228
- project_id: TEST_PROJECT_ID,
229
- merge_request_iid: TEST_MR_IID,
230
- file_paths: ['src/index.ts', 'nonexistent/file.txt', 'also_missing.py']
231
- }, {
232
- GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
233
- GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN
234
- });
235
- assert.ok(Array.isArray(fileDiff), 'Response should be an array');
236
- // Should return 3 results: 1 success + 2 errors
237
- assert.strictEqual(fileDiff.length, 3);
238
- // Find the error entries
239
- const errorEntries = fileDiff.filter((f) => f.error !== undefined);
240
- const successEntries = fileDiff.filter((f) => f.error === undefined);
241
- assert.strictEqual(errorEntries.length, 2, 'Should have 2 error entries');
242
- assert.strictEqual(successEntries.length, 1, 'Should have 1 success entry');
243
- // Verify error messages are helpful
244
- const errorMsgs = errorEntries.map((e) => e.error);
245
- assert.ok(errorMsgs.some(msg => msg.includes('nonexistent/file.txt')), 'Error message should mention nonexistent file');
246
- assert.ok(errorMsgs.some(msg => msg.includes('also_missing.py')), 'Error message should mention other missing file');
247
- // Check hint is present in at least one error
248
- const hints = errorEntries.map((e) => e.hint).filter(Boolean);
249
- assert.ok(hints.length > 0, 'Errors should include hints to check list_merge_request_changed_files');
250
- });
251
- });
@@ -1,133 +0,0 @@
1
- /**
2
- * Regression tests for OAuth endpoint rate limiting behind trusted proxies.
3
- */
4
- import { describe, test, after, before } from "node:test";
5
- import assert from "node:assert";
6
- import { launchServer, findAvailablePort, cleanupServers, TransportMode, HOST, } from "./utils/server-launcher.js";
7
- import { MockGitLabServer, findMockServerPort } from "./utils/mock-gitlab-server.js";
8
- const MOCK_GITLAB_PORT_BASE = 9210;
9
- const MCP_SERVER_PORT_BASE = 3210;
10
- const MOCK_CLIENT_ID = "mock-app-uid-from-dcr";
11
- function addOAuthEndpoints(mockGitLab, baseUrl) {
12
- mockGitLab.addRootHandler("post", "/oauth/register", (req, res) => {
13
- res.status(201).json({
14
- client_id: MOCK_CLIENT_ID,
15
- client_name: req.body?.client_name ?? "test",
16
- redirect_uris: req.body?.redirect_uris ?? [],
17
- token_endpoint_auth_method: "none",
18
- require_pkce: true,
19
- });
20
- });
21
- mockGitLab.addRootHandler("get", "/.well-known/oauth-authorization-server", (_req, res) => {
22
- res.json({
23
- issuer: baseUrl,
24
- authorization_endpoint: `${baseUrl}/oauth/authorize`,
25
- token_endpoint: `${baseUrl}/oauth/token`,
26
- registration_endpoint: `${baseUrl}/oauth/register`,
27
- revocation_endpoint: `${baseUrl}/oauth/revoke`,
28
- scopes_supported: ["api", "read_api", "read_user"],
29
- response_types_supported: ["code"],
30
- grant_types_supported: ["authorization_code", "refresh_token"],
31
- code_challenge_methods_supported: ["S256"],
32
- });
33
- });
34
- }
35
- describe("When MCP OAuth runs behind a trusted proxy", () => {
36
- describe("with X-Forwarded-For containing client ports", () => {
37
- let mcpBaseUrl;
38
- let mockGitLab;
39
- let servers = [];
40
- before(async () => {
41
- const mockPort = await findMockServerPort();
42
- mockGitLab = new MockGitLabServer({ port: mockPort, validTokens: [] });
43
- await mockGitLab.start();
44
- const mockGitLabUrl = mockGitLab.getUrl();
45
- addOAuthEndpoints(mockGitLab, mockGitLabUrl);
46
- const mcpPort = await findAvailablePort(MCP_SERVER_PORT_BASE);
47
- mcpBaseUrl = `http://${HOST}:${mcpPort}`;
48
- const server = await launchServer({
49
- mode: TransportMode.STREAMABLE_HTTP,
50
- port: mcpPort,
51
- timeout: 5000,
52
- env: {
53
- STREAMABLE_HTTP: "true",
54
- GITLAB_MCP_OAUTH: "true",
55
- MCP_TRUST_PROXY: "true",
56
- GITLAB_OAUTH_APP_ID: "test-oauth-app-id",
57
- GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
58
- MCP_SERVER_URL: mcpBaseUrl,
59
- MCP_DANGEROUSLY_ALLOW_INSECURE_ISSUER_URL: "true",
60
- },
61
- });
62
- servers.push(server);
63
- });
64
- after(async () => {
65
- cleanupServers(servers);
66
- if (mockGitLab) {
67
- await mockGitLab.stop();
68
- }
69
- });
70
- test("should not return 500 from /register", async () => {
71
- const res = await fetch(`${mcpBaseUrl}/register`, {
72
- method: "POST",
73
- headers: {
74
- "Content-Type": "application/json",
75
- "X-Forwarded-For": "160.79.106.36:38914",
76
- },
77
- body: JSON.stringify({
78
- redirect_uris: ["https://client.example/callback"],
79
- client_name: "proxy-rate-limit-test",
80
- }),
81
- });
82
- assert.notEqual(res.status, 500, "rate-limit key generation should not crash /register");
83
- assert.strictEqual(res.status, 201, "DCR should succeed behind forwarded IPv4:port");
84
- });
85
- test("should not return 500 from /authorize", async () => {
86
- const params = new URLSearchParams({
87
- response_type: "code",
88
- client_id: MOCK_CLIENT_ID,
89
- redirect_uri: "https://client.example/callback",
90
- code_challenge: "challenge",
91
- code_challenge_method: "S256",
92
- state: "proxy-rate-limit-state",
93
- scope: "api",
94
- });
95
- const res = await fetch(`${mcpBaseUrl}/authorize?${params}`, {
96
- redirect: "manual",
97
- headers: {
98
- "X-Forwarded-For": "[2001:db8::1]:5678",
99
- },
100
- });
101
- assert.notEqual(res.status, 500, "rate-limit key generation should not crash /authorize");
102
- });
103
- test("should not return 500 from /token", async () => {
104
- const res = await fetch(`${mcpBaseUrl}/token`, {
105
- method: "POST",
106
- headers: {
107
- "Content-Type": "application/x-www-form-urlencoded",
108
- "X-Forwarded-For": "160.79.106.36:38914",
109
- },
110
- body: new URLSearchParams({
111
- grant_type: "authorization_code",
112
- client_id: MOCK_CLIENT_ID,
113
- code: "invalid-code",
114
- redirect_uri: "https://client.example/callback",
115
- }),
116
- });
117
- assert.notEqual(res.status, 500, "rate-limit key generation should not crash /token");
118
- });
119
- test("should not return 500 from /revoke", async () => {
120
- const res = await fetch(`${mcpBaseUrl}/revoke`, {
121
- method: "POST",
122
- headers: {
123
- "Content-Type": "application/x-www-form-urlencoded",
124
- "X-Forwarded-For": "[2001:db8::1]",
125
- },
126
- body: new URLSearchParams({
127
- token: "invalid-token",
128
- }),
129
- });
130
- assert.notEqual(res.status, 500, "rate-limit key generation should not crash /revoke");
131
- });
132
- });
133
- });
@@ -1,155 +0,0 @@
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
- const MOCK_TOKEN = "glpat-mock-token-12345";
6
- const mockSchema = {
7
- nodes: [{ name: "Project", properties: ["id", "name"] }],
8
- edges: [{ name: "HAS_MERGE_REQUEST", from: "Project", to: "MergeRequest" }],
9
- };
10
- const mockStatus = {
11
- status: "ready",
12
- last_indexed_at: "2026-09-05T10:00:00Z",
13
- indexed_groups: 3,
14
- };
15
- const mockTools = {
16
- tools: [{ name: "orbit_query", description: "Execute a graph query" }],
17
- };
18
- const mockQueryRaw = {
19
- results: [{ project: "group/a", failures: 7 }],
20
- };
21
- const mockQueryLlm = "Top failing project: group/a (7 failures).";
22
- function baseEnv(mockGitLabUrl) {
23
- return {
24
- GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
25
- GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN,
26
- GITLAB_TOOLSETS: "orbit",
27
- };
28
- }
29
- async function callToolAsync(env, name, args) {
30
- return new Promise((resolve, reject) => {
31
- const proc = spawn("node", ["build/index.js"], {
32
- stdio: ["pipe", "pipe", "pipe"],
33
- env: { ...process.env, ...env },
34
- });
35
- let output = "";
36
- let errorOutput = "";
37
- proc.stdout?.on("data", (chunk) => (output += chunk));
38
- proc.stderr?.on("data", (chunk) => (errorOutput += chunk));
39
- proc.on("close", code => {
40
- if (code !== 0) {
41
- return reject(new Error(`Process exited with code ${code}: ${errorOutput}`));
42
- }
43
- const line = output.split("\n").find(entry => entry.startsWith("{"));
44
- if (!line) {
45
- return reject(new Error("No JSON output found"));
46
- }
47
- try {
48
- const response = JSON.parse(line);
49
- if (response.error) {
50
- reject(new Error(JSON.stringify(response.error)));
51
- return;
52
- }
53
- const content = response.result?.content?.[0]?.text;
54
- if (content === undefined) {
55
- reject(new Error("No tool result content"));
56
- return;
57
- }
58
- resolve(content);
59
- }
60
- catch (error) {
61
- reject(error);
62
- }
63
- });
64
- proc.stdin?.end(JSON.stringify({
65
- jsonrpc: "2.0",
66
- id: 1,
67
- method: "tools/call",
68
- params: { name, arguments: args },
69
- }) + "\n");
70
- });
71
- }
72
- describe("orbit tools", () => {
73
- let mockGitLab;
74
- let mockGitLabUrl;
75
- let lastQueryBody = null;
76
- before(async () => {
77
- const mockPort = await findMockServerPort();
78
- mockGitLab = new MockGitLabServer({
79
- port: mockPort,
80
- validTokens: [MOCK_TOKEN],
81
- });
82
- mockGitLab.addMockHandler("get", "/orbit/schema", (_req, res) => {
83
- res.json(mockSchema);
84
- });
85
- mockGitLab.addMockHandler("get", "/orbit/status", (_req, res) => {
86
- res.json(mockStatus);
87
- });
88
- mockGitLab.addMockHandler("get", "/orbit/tools", (_req, res) => {
89
- res.json(mockTools);
90
- });
91
- mockGitLab.addMockHandler("post", "/orbit/query", (req, res) => {
92
- lastQueryBody = req.body;
93
- if (req.body?.format === "raw") {
94
- res.json(mockQueryRaw);
95
- return;
96
- }
97
- res.type("text/plain").send(mockQueryLlm);
98
- });
99
- await mockGitLab.start();
100
- mockGitLabUrl = mockGitLab.getUrl();
101
- });
102
- after(async () => {
103
- await mockGitLab.stop();
104
- });
105
- test("orbit_get_schema returns the graph schema", async () => {
106
- const text = await callToolAsync(baseEnv(mockGitLabUrl), "orbit_get_schema", {});
107
- assert.deepEqual(JSON.parse(text), mockSchema);
108
- });
109
- test("orbit_get_status returns indexing status", async () => {
110
- const text = await callToolAsync(baseEnv(mockGitLabUrl), "orbit_get_status", {});
111
- assert.deepEqual(JSON.parse(text), mockStatus);
112
- });
113
- test("orbit_list_tools returns tool definitions", async () => {
114
- const text = await callToolAsync(baseEnv(mockGitLabUrl), "orbit_list_tools", {});
115
- assert.deepEqual(JSON.parse(text), mockTools);
116
- });
117
- test("orbit_query with format=raw returns structured JSON", async () => {
118
- const query = { node: "Project", limit: 5 };
119
- const text = await callToolAsync(baseEnv(mockGitLabUrl), "orbit_query", {
120
- query,
121
- format: "raw",
122
- });
123
- assert.deepEqual(JSON.parse(text), mockQueryRaw);
124
- assert.deepEqual(lastQueryBody, { query, format: "raw" });
125
- });
126
- test("orbit_query defaults to llm format and passes text through", async () => {
127
- const query = { node: "Project", limit: 5 };
128
- const text = await callToolAsync(baseEnv(mockGitLabUrl), "orbit_query", { query });
129
- assert.equal(text, mockQueryLlm);
130
- assert.deepEqual(lastQueryBody, { query, format: "llm" });
131
- });
132
- for (const toolName of [
133
- "orbit_query",
134
- "orbit_get_schema",
135
- "orbit_get_status",
136
- "orbit_list_tools",
137
- ]) {
138
- test(`${toolName} is refused under strict project scope`, async () => {
139
- const env = {
140
- ...baseEnv(mockGitLabUrl),
141
- ENABLE_STRICT_PROJECT_SCOPE: "true",
142
- GITLAB_ALLOWED_PROJECT_IDS: "1,2",
143
- };
144
- const args = toolName === "orbit_query" ? { query: {} } : {};
145
- let message = "";
146
- try {
147
- message = await callToolAsync(env, toolName, args);
148
- }
149
- catch (error) {
150
- message = error.message;
151
- }
152
- assert.match(message, /not allowed while a project allowlist is in effect/);
153
- });
154
- }
155
- });