@zereight/mcp-gitlab 2.1.21 → 2.1.23
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 +45 -45
- package/README.md +36 -22
- package/README.zh-CN.md +44 -44
- package/build/config.js +8 -2
- package/build/index.js +127 -32
- package/build/oauth.js +9 -9
- package/build/schemas.js +6 -3
- package/build/scripts/generate-tool-docs.js +404 -0
- package/build/test/config-allowed-groups.test.js +97 -0
- package/build/test/test-oauth-proxy-rate-limit.js +133 -0
- package/build/test/test-remote-downloads.js +162 -1
- package/build/test/utils/proxy-client-ip.test.js +28 -0
- package/build/utils/proxy-client-ip.js +11 -0
- package/package.json +2 -2
|
@@ -17,6 +17,16 @@ const MOCK_TOKEN = 'glpat-mock-token-12345';
|
|
|
17
17
|
const TEST_PROJECT_ID = '123';
|
|
18
18
|
const TEST_JOB_ID = '456';
|
|
19
19
|
const TEST_SECRET = 'testsecret';
|
|
20
|
+
const FORWARDED_BASE_URL = 'https://gitlab.mcp.example.test/gitlab-mcp';
|
|
21
|
+
const FORWARDED_HEADERS = {
|
|
22
|
+
'X-Forwarded-Proto': 'https',
|
|
23
|
+
'X-Forwarded-Host': 'gitlab.mcp.example.test',
|
|
24
|
+
'X-Forwarded-Prefix': '/gitlab-mcp',
|
|
25
|
+
};
|
|
26
|
+
const RFC_FORWARDED_HEADERS = {
|
|
27
|
+
'Forwarded': 'for=192.0.2.43;proto=http;host=attacker.example.test, for="[2001:db8:cafe::17]";proto="https";host="gitlab.mcp.example.test"',
|
|
28
|
+
'X-Forwarded-Prefix': '/gitlab-mcp',
|
|
29
|
+
};
|
|
20
30
|
// Minimal 1x1 transparent PNG
|
|
21
31
|
const MINIMAL_PNG = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==', 'base64');
|
|
22
32
|
const LARGE_FILE_TOKEN = 'glpat-largefile-test-token';
|
|
@@ -92,6 +102,8 @@ describe('Remote Downloads - Download Proxy Endpoint', { timeout: 30_000 }, () =
|
|
|
92
102
|
env: {
|
|
93
103
|
STREAMABLE_HTTP: 'true',
|
|
94
104
|
REMOTE_AUTHORIZATION: 'true',
|
|
105
|
+
MCP_TRUST_PROXY: 'false',
|
|
106
|
+
MCP_SERVER_URL: '',
|
|
95
107
|
GITLAB_API_URL: `${mockGitLab.getUrl()}/api/v4`,
|
|
96
108
|
USE_PIPELINE: 'true',
|
|
97
109
|
MAX_REQUESTS_PER_MINUTE: '2',
|
|
@@ -148,6 +160,61 @@ describe('Remote Downloads - Download Proxy Endpoint', { timeout: 30_000 }, () =
|
|
|
148
160
|
}
|
|
149
161
|
assert.ok(got429, 'Should have received 429 within 10 requests (rate limit is 2/min)');
|
|
150
162
|
});
|
|
163
|
+
test('ignores forwarded headers unless MCP_TRUST_PROXY is enabled', async () => {
|
|
164
|
+
const initRes = await fetch(`http://${HOST}:${serverPort}/mcp`, {
|
|
165
|
+
method: 'POST',
|
|
166
|
+
headers: {
|
|
167
|
+
'Content-Type': 'application/json',
|
|
168
|
+
'Accept': 'application/json, text/event-stream',
|
|
169
|
+
'Private-Token': MOCK_TOKEN,
|
|
170
|
+
},
|
|
171
|
+
body: JSON.stringify({
|
|
172
|
+
jsonrpc: '2.0',
|
|
173
|
+
id: 20,
|
|
174
|
+
method: 'initialize',
|
|
175
|
+
params: {
|
|
176
|
+
protocolVersion: '2025-03-26',
|
|
177
|
+
capabilities: {},
|
|
178
|
+
clientInfo: { name: 'test-remote-downloads-no-trust-proxy', version: '1.0' },
|
|
179
|
+
},
|
|
180
|
+
}),
|
|
181
|
+
});
|
|
182
|
+
assert.strictEqual(initRes.status, 200, 'Initialize should succeed');
|
|
183
|
+
const sessionId = initRes.headers.get('mcp-session-id');
|
|
184
|
+
assert.ok(sessionId, 'Should receive a session ID');
|
|
185
|
+
const toolRes = await fetch(`http://${HOST}:${serverPort}/mcp`, {
|
|
186
|
+
method: 'POST',
|
|
187
|
+
headers: {
|
|
188
|
+
'Content-Type': 'application/json',
|
|
189
|
+
'Accept': 'application/json, text/event-stream',
|
|
190
|
+
...FORWARDED_HEADERS,
|
|
191
|
+
'Private-Token': MOCK_TOKEN,
|
|
192
|
+
'mcp-session-id': sessionId,
|
|
193
|
+
},
|
|
194
|
+
body: JSON.stringify({
|
|
195
|
+
jsonrpc: '2.0',
|
|
196
|
+
id: 21,
|
|
197
|
+
method: 'tools/call',
|
|
198
|
+
params: {
|
|
199
|
+
name: 'download_job_artifacts',
|
|
200
|
+
arguments: {
|
|
201
|
+
project_id: TEST_PROJECT_ID,
|
|
202
|
+
job_id: TEST_JOB_ID,
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
}),
|
|
206
|
+
});
|
|
207
|
+
assert.strictEqual(toolRes.status, 200, 'Tool call should return 200');
|
|
208
|
+
const responses = parseSSE(await toolRes.text());
|
|
209
|
+
const result = responses.find(r => r.id === 21);
|
|
210
|
+
assert.ok(result?.result, 'Should have a result');
|
|
211
|
+
const textBlock = result.result.content.find(c => c.type === 'text');
|
|
212
|
+
assert.ok(textBlock?.text, 'Should have text content');
|
|
213
|
+
const parsed = JSON.parse(textBlock.text);
|
|
214
|
+
assert.ok(parsed.download_url, 'Should contain download_url');
|
|
215
|
+
assert.ok(parsed.download_url.startsWith(`http://${HOST}:${serverPort}/downloads/job-artifacts?`), `URL should fall back to local server address when MCP_TRUST_PROXY is disabled, got ${parsed.download_url}`);
|
|
216
|
+
assert.ok(!parsed.download_url.startsWith(`${FORWARDED_BASE_URL}/downloads/job-artifacts?`), 'URL should not use forwarded public base URL when MCP_TRUST_PROXY is disabled');
|
|
217
|
+
});
|
|
151
218
|
});
|
|
152
219
|
describe('Remote Downloads - Tool Behavior via MCP Protocol', { timeout: 60_000 }, () => {
|
|
153
220
|
let mockGitLab;
|
|
@@ -189,6 +256,8 @@ describe('Remote Downloads - Tool Behavior via MCP Protocol', { timeout: 60_000
|
|
|
189
256
|
env: {
|
|
190
257
|
STREAMABLE_HTTP: 'true',
|
|
191
258
|
REMOTE_AUTHORIZATION: 'true',
|
|
259
|
+
MCP_TRUST_PROXY: 'true',
|
|
260
|
+
MCP_SERVER_URL: '',
|
|
192
261
|
GITLAB_API_URL: `${mockGitLab.getUrl()}/api/v4`,
|
|
193
262
|
USE_PIPELINE: 'true',
|
|
194
263
|
},
|
|
@@ -222,12 +291,13 @@ describe('Remote Downloads - Tool Behavior via MCP Protocol', { timeout: 60_000
|
|
|
222
291
|
if (mockGitLab)
|
|
223
292
|
await mockGitLab.stop();
|
|
224
293
|
});
|
|
225
|
-
async function callTool(id, name, args) {
|
|
294
|
+
async function callTool(id, name, args, extraHeaders = {}) {
|
|
226
295
|
const res = await fetch(`http://${HOST}:${serverPort}/mcp`, {
|
|
227
296
|
method: 'POST',
|
|
228
297
|
headers: {
|
|
229
298
|
'Content-Type': 'application/json',
|
|
230
299
|
'Accept': 'application/json, text/event-stream',
|
|
300
|
+
...extraHeaders,
|
|
231
301
|
'Private-Token': MOCK_TOKEN,
|
|
232
302
|
'mcp-session-id': sessionId,
|
|
233
303
|
},
|
|
@@ -269,6 +339,97 @@ describe('Remote Downloads - Tool Behavior via MCP Protocol', { timeout: 60_000
|
|
|
269
339
|
assert.ok(buf.length > 0, 'Downloaded content should not be empty');
|
|
270
340
|
assert.ok(buf.includes(Buffer.from('PK')), 'Should contain zip magic bytes');
|
|
271
341
|
});
|
|
342
|
+
test('download_job_artifacts ignores forwarded hosts containing URL components', async () => {
|
|
343
|
+
const result = await callTool(14, 'download_job_artifacts', {
|
|
344
|
+
project_id: TEST_PROJECT_ID,
|
|
345
|
+
job_id: TEST_JOB_ID,
|
|
346
|
+
}, {
|
|
347
|
+
...FORWARDED_HEADERS,
|
|
348
|
+
'X-Forwarded-Host': 'gitlab.mcp.example.test@attacker.example.test',
|
|
349
|
+
});
|
|
350
|
+
assert.ok(result.result, 'Should have a result');
|
|
351
|
+
const content = result.result.content;
|
|
352
|
+
assert.ok(content && content.length > 0, 'Should have content');
|
|
353
|
+
const textBlock = content.find(c => c.type === 'text');
|
|
354
|
+
assert.ok(textBlock?.text, 'Should have text content');
|
|
355
|
+
const parsed = JSON.parse(textBlock.text);
|
|
356
|
+
assert.ok(parsed.download_url, 'Should contain download_url');
|
|
357
|
+
assert.ok(parsed.download_url.startsWith(`http://${HOST}:${serverPort}/downloads/job-artifacts?`), `URL should fall back to local server address for unsafe forwarded host, got ${parsed.download_url}`);
|
|
358
|
+
assert.ok(!parsed.download_url.includes('attacker.example.test'), `URL should not contain unsafe forwarded host, got ${parsed.download_url}`);
|
|
359
|
+
assert.ok(parsed.download_url.includes('_token='), 'URL should contain embedded auth token');
|
|
360
|
+
});
|
|
361
|
+
test('download_job_artifacts ignores authority-style forwarded prefixes', async () => {
|
|
362
|
+
const result = await callTool(15, 'download_job_artifacts', {
|
|
363
|
+
project_id: TEST_PROJECT_ID,
|
|
364
|
+
job_id: TEST_JOB_ID,
|
|
365
|
+
}, {
|
|
366
|
+
...FORWARDED_HEADERS,
|
|
367
|
+
'X-Forwarded-Prefix': '//attacker.example.test',
|
|
368
|
+
});
|
|
369
|
+
assert.ok(result.result, 'Should have a result');
|
|
370
|
+
const content = result.result.content;
|
|
371
|
+
assert.ok(content && content.length > 0, 'Should have content');
|
|
372
|
+
const textBlock = content.find(c => c.type === 'text');
|
|
373
|
+
assert.ok(textBlock?.text, 'Should have text content');
|
|
374
|
+
const parsed = JSON.parse(textBlock.text);
|
|
375
|
+
assert.ok(parsed.download_url, 'Should contain download_url');
|
|
376
|
+
assert.ok(parsed.download_url.startsWith('https://gitlab.mcp.example.test/downloads/job-artifacts?'), `URL should keep forwarded proto/host while ignoring unsafe prefix, got ${parsed.download_url}`);
|
|
377
|
+
assert.ok(!parsed.download_url.includes('attacker.example.test'), `URL should not contain authority-style prefix, got ${parsed.download_url}`);
|
|
378
|
+
assert.ok(!parsed.download_url.startsWith(`http://${HOST}:${serverPort}`), 'URL should not fall back to local server address');
|
|
379
|
+
assert.ok(parsed.download_url.includes('_token='), 'URL should contain embedded auth token');
|
|
380
|
+
});
|
|
381
|
+
test('download_job_artifacts uses rightmost forwarded header values', async () => {
|
|
382
|
+
const result = await callTool(16, 'download_job_artifacts', {
|
|
383
|
+
project_id: TEST_PROJECT_ID,
|
|
384
|
+
job_id: TEST_JOB_ID,
|
|
385
|
+
}, {
|
|
386
|
+
'X-Forwarded-Proto': 'http, https',
|
|
387
|
+
'X-Forwarded-Host': 'attacker.example.test, gitlab.mcp.example.test',
|
|
388
|
+
'X-Forwarded-Prefix': '/attacker, /gitlab-mcp',
|
|
389
|
+
});
|
|
390
|
+
assert.ok(result.result, 'Should have a result');
|
|
391
|
+
const content = result.result.content;
|
|
392
|
+
assert.ok(content && content.length > 0, 'Should have content');
|
|
393
|
+
const textBlock = content.find(c => c.type === 'text');
|
|
394
|
+
assert.ok(textBlock?.text, 'Should have text content');
|
|
395
|
+
const parsed = JSON.parse(textBlock.text);
|
|
396
|
+
assert.ok(parsed.download_url, 'Should contain download_url');
|
|
397
|
+
assert.ok(parsed.download_url.startsWith(`${FORWARDED_BASE_URL}/downloads/job-artifacts?`), `URL should use the rightmost forwarded header values, got ${parsed.download_url}`);
|
|
398
|
+
assert.ok(!parsed.download_url.includes('attacker.example.test'), `URL should not use the leftmost untrusted forwarded header value, got ${parsed.download_url}`);
|
|
399
|
+
assert.ok(parsed.download_url.includes('_token='), 'URL should contain embedded auth token');
|
|
400
|
+
});
|
|
401
|
+
test('download_job_artifacts derives download_url from RFC 7239 Forwarded header', async () => {
|
|
402
|
+
const result = await callTool(18, 'download_job_artifacts', {
|
|
403
|
+
project_id: TEST_PROJECT_ID,
|
|
404
|
+
job_id: TEST_JOB_ID,
|
|
405
|
+
}, RFC_FORWARDED_HEADERS);
|
|
406
|
+
assert.ok(result.result, 'Should have a result');
|
|
407
|
+
const content = result.result.content;
|
|
408
|
+
assert.ok(content && content.length > 0, 'Should have content');
|
|
409
|
+
const textBlock = content.find(c => c.type === 'text');
|
|
410
|
+
assert.ok(textBlock?.text, 'Should have text content');
|
|
411
|
+
const parsed = JSON.parse(textBlock.text);
|
|
412
|
+
assert.ok(parsed.download_url, 'Should contain download_url');
|
|
413
|
+
assert.ok(parsed.download_url.startsWith(`${FORWARDED_BASE_URL}/downloads/job-artifacts?`), `URL should use RFC 7239 Forwarded header values, got ${parsed.download_url}`);
|
|
414
|
+
assert.ok(!parsed.download_url.includes('attacker.example.test'), `URL should not use the leftmost untrusted Forwarded value, got ${parsed.download_url}`);
|
|
415
|
+
assert.ok(parsed.download_url.includes('_token='), 'URL should contain embedded auth token');
|
|
416
|
+
});
|
|
417
|
+
test('download_job_artifacts derives download_url from forwarded headers when MCP_SERVER_URL is unset', async () => {
|
|
418
|
+
const result = await callTool(17, 'download_job_artifacts', {
|
|
419
|
+
project_id: TEST_PROJECT_ID,
|
|
420
|
+
job_id: TEST_JOB_ID,
|
|
421
|
+
}, FORWARDED_HEADERS);
|
|
422
|
+
assert.ok(result.result, 'Should have a result');
|
|
423
|
+
const content = result.result.content;
|
|
424
|
+
assert.ok(content && content.length > 0, 'Should have content');
|
|
425
|
+
const textBlock = content.find(c => c.type === 'text');
|
|
426
|
+
assert.ok(textBlock?.text, 'Should have text content');
|
|
427
|
+
const parsed = JSON.parse(textBlock.text);
|
|
428
|
+
assert.ok(parsed.download_url, 'Should contain download_url');
|
|
429
|
+
assert.ok(parsed.download_url.startsWith(`${FORWARDED_BASE_URL}/downloads/job-artifacts?`), `URL should use forwarded public base URL, got ${parsed.download_url}`);
|
|
430
|
+
assert.ok(!parsed.download_url.startsWith(`http://${HOST}:${serverPort}`), 'URL should not fall back to local server address');
|
|
431
|
+
assert.ok(parsed.download_url.includes('_token='), 'URL should contain embedded auth token');
|
|
432
|
+
});
|
|
272
433
|
test('download_attachment for non-image returns download_url', async () => {
|
|
273
434
|
const result = await callTool(11, 'download_attachment', {
|
|
274
435
|
project_id: TEST_PROJECT_ID,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, test } from "node:test";
|
|
3
|
+
import { ipKeyGenerator } from "express-rate-limit";
|
|
4
|
+
import { normalizeProxyClientIpForRateLimit } from "../../utils/proxy-client-ip.js";
|
|
5
|
+
function rateLimitKeyFromForwardedIp(ip) {
|
|
6
|
+
return ipKeyGenerator(normalizeProxyClientIpForRateLimit(ip));
|
|
7
|
+
}
|
|
8
|
+
describe("When normalizeProxyClientIpForRateLimit runs", () => {
|
|
9
|
+
describe("with proxy-forwarded client addresses", () => {
|
|
10
|
+
test("should strip IPv4 ports before ipKeyGenerator accepts the address", () => {
|
|
11
|
+
const key = rateLimitKeyFromForwardedIp("160.79.106.36:38914");
|
|
12
|
+
assert.equal(key, "160.79.106.36");
|
|
13
|
+
});
|
|
14
|
+
test("should strip bracketed IPv6 ports before ipKeyGenerator accepts the address", () => {
|
|
15
|
+
const key = rateLimitKeyFromForwardedIp("[2001:db8::1]:5678");
|
|
16
|
+
assert.equal(key, "2001:db8::/56");
|
|
17
|
+
});
|
|
18
|
+
test("should strip brackets from bracketed IPv6 addresses without ports", () => {
|
|
19
|
+
const key = rateLimitKeyFromForwardedIp("[2001:db8::1]");
|
|
20
|
+
assert.equal(key, "2001:db8::/56");
|
|
21
|
+
});
|
|
22
|
+
test("should leave plain IPv4 and IPv6 addresses unchanged", () => {
|
|
23
|
+
assert.equal(rateLimitKeyFromForwardedIp("1.2.3.4"), "1.2.3.4");
|
|
24
|
+
assert.equal(rateLimitKeyFromForwardedIp("2001:db8::1"), "2001:db8::/56");
|
|
25
|
+
assert.equal(rateLimitKeyFromForwardedIp("::1"), "::/56");
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize Express req.ip values from reverse proxies before rate limiting.
|
|
3
|
+
*
|
|
4
|
+
* Some proxies append the client port to X-Forwarded-For (e.g. "1.2.3.4:5678"
|
|
5
|
+
* or "[2001:db8::1]:5678"). express-rate-limit rejects those unless stripped.
|
|
6
|
+
*/
|
|
7
|
+
export function normalizeProxyClientIpForRateLimit(ip) {
|
|
8
|
+
return ip
|
|
9
|
+
.replace(/^(\d+\.\d+\.\d+\.\d+):\d+$/, "$1")
|
|
10
|
+
.replace(/^\[([^\]]+)\](?::\d+)?$/, "$1");
|
|
11
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zereight/mcp-gitlab",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.23",
|
|
4
4
|
"mcpName": "io.github.zereight/gitlab-mcp",
|
|
5
5
|
"description": "GitLab MCP server for projects, merge requests, issues, pipelines, wiki, releases, and more",
|
|
6
6
|
"keywords": [
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"changelog": "auto-changelog -p",
|
|
52
52
|
"test": "npm run test:all",
|
|
53
53
|
"test:all": "npm run build && npm run test:mock && npm run test:live",
|
|
54
|
-
"test:mock": "node --import tsx/esm --test test/remote-auth-simple-test.ts && node --import tsx/esm --test test/mcp-oauth-tests.ts && node --import tsx/esm --test test/streamable-http-static-token-auth.test.ts && tsx test/oauth-tests.ts && tsx test/test-list-merge-requests.ts && tsx test/test-list-issues.ts && node --import tsx/esm --test test/test-merge-request-pipelines.ts && tsx test/test-list-project-members.ts && tsx test/test-download-attachment.ts && node --import tsx/esm --test test/test-upload-markdown.ts && node --import tsx/esm --test test/test-job-artifacts.ts && node --import tsx/esm --test test/test-deployment-tools.ts && node --import tsx/esm --test test/test-merge-request-approval-state-tools.ts && node --import tsx/esm --test test/test-search-code.ts && node --import tsx/esm --test test/test-tags.ts && node --import tsx/esm --test test/test-protected-branches.ts && node --import tsx/esm --test test/test-toolset-filtering.ts && node --import tsx/esm --test test/test-ci-lint.ts && node --import tsx/esm --test test/test-todos.ts && node --import tsx/esm --test test/test-auth-retry.ts && node --import tsx/esm --test test/test-issue-description-patch.ts && node --import tsx/esm --test test/test-geteffectiveprojectid.ts && node --import tsx/esm --test test/test-get-file-blame.ts && node --import tsx/esm --test test/stateless/codec.test.ts test/stateless/client-id.test.ts test/stateless/callback-proxy.test.ts test/stateless/session-id.test.ts test/stateless/session-id-integration.test.ts test/stateless/config-ttl.test.ts && node --import tsx/esm --test test/utils/tool-args.test.ts && node --import tsx/esm --test test/utils/graphql-query.test.ts && node --import tsx/esm --test test/utils/wiki-title.test.ts && node --import tsx/esm --test test/utils/merge-request-position.test.ts && node --import tsx/esm --test test/nullish-tool-arguments-schema.test.ts && node --import tsx/esm --test test/test-ci-variables.ts && node --import tsx/esm --test test/test-dependency-proxy.ts",
|
|
54
|
+
"test:mock": "node --import tsx/esm --test test/remote-auth-simple-test.ts && node --import tsx/esm --test test/mcp-oauth-tests.ts && node --import tsx/esm --test test/test-oauth-proxy-rate-limit.ts && node --import tsx/esm --test test/streamable-http-static-token-auth.test.ts && tsx test/oauth-tests.ts && tsx test/test-list-merge-requests.ts && tsx test/test-list-issues.ts && node --import tsx/esm --test test/test-merge-request-pipelines.ts && tsx test/test-list-project-members.ts && tsx test/test-download-attachment.ts && node --import tsx/esm --test test/test-upload-markdown.ts && node --import tsx/esm --test test/test-job-artifacts.ts && node --import tsx/esm --test test/test-remote-downloads.ts && node --import tsx/esm --test test/test-deployment-tools.ts && node --import tsx/esm --test test/test-merge-request-approval-state-tools.ts && node --import tsx/esm --test test/test-search-code.ts && node --import tsx/esm --test test/test-tags.ts && node --import tsx/esm --test test/test-protected-branches.ts && node --import tsx/esm --test test/test-toolset-filtering.ts && node --import tsx/esm --test test/test-ci-lint.ts && node --import tsx/esm --test test/test-todos.ts && node --import tsx/esm --test test/test-auth-retry.ts && node --import tsx/esm --test test/test-issue-description-patch.ts && node --import tsx/esm --test test/test-geteffectiveprojectid.ts && node --import tsx/esm --test test/test-get-file-blame.ts && node --import tsx/esm --test test/stateless/codec.test.ts test/stateless/client-id.test.ts test/stateless/callback-proxy.test.ts test/stateless/session-id.test.ts test/stateless/session-id-integration.test.ts test/stateless/config-ttl.test.ts && node --import tsx/esm --test test/utils/tool-args.test.ts && node --import tsx/esm --test test/utils/proxy-client-ip.test.ts && node --import tsx/esm --test test/utils/graphql-query.test.ts && node --import tsx/esm --test test/utils/wiki-title.test.ts && node --import tsx/esm --test test/utils/merge-request-position.test.ts && node --import tsx/esm --test test/nullish-tool-arguments-schema.test.ts && node --import tsx/esm --test test/test-ci-variables.ts && node --import tsx/esm --test test/test-dependency-proxy.ts",
|
|
55
55
|
"test:stateless": "npm run build && node --import tsx/esm --test test/stateless/codec.test.ts test/stateless/client-id.test.ts test/stateless/callback-proxy.test.ts test/stateless/session-id.test.ts test/stateless/session-id-integration.test.ts test/stateless/config-ttl.test.ts",
|
|
56
56
|
"test:mcp-oauth": "npm run build && node --import tsx/esm --test test/mcp-oauth-tests.ts",
|
|
57
57
|
"test:live": "node test/validate-api.js",
|