@zereight/mcp-gitlab 2.0.32 → 2.0.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -1
- package/build/gitlab-client-pool.js +108 -6
- package/build/index.js +274 -64
- package/build/oauth.js +11 -6
- package/build/schemas.js +69 -0
- package/build/test/no-proxy-integration-test.js +183 -0
- package/build/test/no-proxy-test.js +138 -0
- package/build/test/remote-auth-simple-test.js +12 -1
- package/build/test/test-geteffectiveprojectid.js +236 -0
- package/build/test/test-upload-markdown.js +148 -0
- package/build/test/utils/mock-gitlab-server.js +5 -1
- package/package.json +1 -1
package/build/oauth.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as crypto from "crypto";
|
|
1
2
|
import * as fs from "fs";
|
|
2
3
|
import * as os from "os";
|
|
3
4
|
import * as path from "path";
|
|
@@ -10,7 +11,11 @@ import { pino } from "pino";
|
|
|
10
11
|
const logger = pino({
|
|
11
12
|
name: "gitlab-mcp-oauth",
|
|
12
13
|
level: process.env.LOG_LEVEL || "info",
|
|
13
|
-
});
|
|
14
|
+
}, pino.destination(2));
|
|
15
|
+
function escapeHtml(str) {
|
|
16
|
+
const map = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" };
|
|
17
|
+
return String(str).replace(/[&<>"']/g, c => map[c] || c);
|
|
18
|
+
}
|
|
14
19
|
// Track pending auth requests across multiple MCP instances
|
|
15
20
|
const pendingAuthRequests = new Map();
|
|
16
21
|
/**
|
|
@@ -225,7 +230,7 @@ export class GitLabOAuth {
|
|
|
225
230
|
*/
|
|
226
231
|
async startOAuthFlow() {
|
|
227
232
|
const callbackPort = parseInt(new URL(this.config.redirectUri).port || "8888");
|
|
228
|
-
const requestId =
|
|
233
|
+
const requestId = crypto.randomUUID();
|
|
229
234
|
// Check if port is already in use
|
|
230
235
|
const portInUse = await isPortInUse(callbackPort);
|
|
231
236
|
if (portInUse) {
|
|
@@ -250,7 +255,7 @@ export class GitLabOAuth {
|
|
|
250
255
|
const requestIdToOAuthInstance = new Map();
|
|
251
256
|
return new Promise((resolve, reject) => {
|
|
252
257
|
// Create initial request
|
|
253
|
-
const state =
|
|
258
|
+
const state = crypto.randomUUID();
|
|
254
259
|
stateToRequestId.set(state, initialRequestId);
|
|
255
260
|
requestIdToOAuthInstance.set(initialRequestId, this);
|
|
256
261
|
const timeout = setTimeout(() => {
|
|
@@ -271,7 +276,7 @@ export class GitLabOAuth {
|
|
|
271
276
|
}
|
|
272
277
|
logger.info(`Received auth request from another instance: ${newRequestId}`);
|
|
273
278
|
// Create a new OAuth flow for this request
|
|
274
|
-
const newState =
|
|
279
|
+
const newState = crypto.randomUUID();
|
|
275
280
|
stateToRequestId.set(newState, newRequestId);
|
|
276
281
|
// Store a reference to use the same OAuth config
|
|
277
282
|
requestIdToOAuthInstance.set(newRequestId, this);
|
|
@@ -315,7 +320,7 @@ export class GitLabOAuth {
|
|
|
315
320
|
<html>
|
|
316
321
|
<body>
|
|
317
322
|
<h1>Authentication Failed</h1>
|
|
318
|
-
<p>Error: ${error}</p>
|
|
323
|
+
<p>Error: ${escapeHtml(String(error))}</p>
|
|
319
324
|
<p>You can close this window.</p>
|
|
320
325
|
</body>
|
|
321
326
|
</html>
|
|
@@ -523,7 +528,7 @@ export async function initializeOAuthClient(gitlabUrl = "https://gitlab.com") {
|
|
|
523
528
|
clientSecret,
|
|
524
529
|
redirectUri,
|
|
525
530
|
gitlabUrl,
|
|
526
|
-
scopes: ["api"],
|
|
531
|
+
scopes: [process.env.GITLAB_READ_ONLY_MODE === "true" ? "read_api" : "api"],
|
|
527
532
|
tokenStoragePath,
|
|
528
533
|
});
|
|
529
534
|
// Single call: triggers browser flow if needed, or reads cached token
|
package/build/schemas.js
CHANGED
|
@@ -1333,6 +1333,9 @@ export const GitLabMergeRequestApprovalStateSchema = z.object({
|
|
|
1333
1333
|
export const GetMergeRequestApprovalStateSchema = ProjectParamsSchema.extend({
|
|
1334
1334
|
merge_request_iid: z.coerce.string().describe("The IID of the merge request"),
|
|
1335
1335
|
});
|
|
1336
|
+
export const GetMergeRequestConflictsSchema = ProjectParamsSchema.extend({
|
|
1337
|
+
merge_request_iid: z.coerce.string().describe("The IID of the merge request"),
|
|
1338
|
+
});
|
|
1336
1339
|
export const GetMergeRequestDiffsSchema = GetMergeRequestSchema.extend({
|
|
1337
1340
|
view: z.enum(["inline", "parallel"]).optional().describe("Diff view type"),
|
|
1338
1341
|
excluded_file_patterns: z
|
|
@@ -2449,3 +2452,69 @@ export const DownloadReleaseAssetSchema = z.object({
|
|
|
2449
2452
|
.string()
|
|
2450
2453
|
.describe("Path to the release asset file as specified when creating or updating its link"),
|
|
2451
2454
|
});
|
|
2455
|
+
// --- Webhook schemas ---
|
|
2456
|
+
export const ListWebhooksSchema = z
|
|
2457
|
+
.object({
|
|
2458
|
+
project_id: z.coerce
|
|
2459
|
+
.string()
|
|
2460
|
+
.optional()
|
|
2461
|
+
.describe("Project ID or URL-encoded path. Provide either project_id or group_id, not both."),
|
|
2462
|
+
group_id: z.coerce
|
|
2463
|
+
.string()
|
|
2464
|
+
.optional()
|
|
2465
|
+
.describe("Group ID or URL-encoded path. Provide either project_id or group_id, not both."),
|
|
2466
|
+
})
|
|
2467
|
+
.merge(PaginationOptionsSchema)
|
|
2468
|
+
.refine(data => (data.project_id || data.group_id) && !(data.project_id && data.group_id), {
|
|
2469
|
+
message: "Provide exactly one of project_id or group_id",
|
|
2470
|
+
});
|
|
2471
|
+
export const ListWebhookEventsSchema = z
|
|
2472
|
+
.object({
|
|
2473
|
+
project_id: z.coerce
|
|
2474
|
+
.string()
|
|
2475
|
+
.optional()
|
|
2476
|
+
.describe("Project ID or URL-encoded path. Provide either project_id or group_id, not both."),
|
|
2477
|
+
group_id: z.coerce
|
|
2478
|
+
.string()
|
|
2479
|
+
.optional()
|
|
2480
|
+
.describe("Group ID or URL-encoded path. Provide either project_id or group_id, not both."),
|
|
2481
|
+
hook_id: z.coerce.number().describe("ID of the webhook"),
|
|
2482
|
+
status: z
|
|
2483
|
+
.union([z.number(), z.string()])
|
|
2484
|
+
.optional()
|
|
2485
|
+
.describe("Filter by response status code (e.g. 200, 500) or category: successful, client_failure, server_failure"),
|
|
2486
|
+
summary: z
|
|
2487
|
+
.boolean()
|
|
2488
|
+
.optional()
|
|
2489
|
+
.describe("If true, return only summary fields (id, url, trigger, response_status, execution_duration) without full request/response payloads. Recommended for overview queries to avoid huge responses."),
|
|
2490
|
+
per_page: z
|
|
2491
|
+
.number()
|
|
2492
|
+
.max(20)
|
|
2493
|
+
.optional()
|
|
2494
|
+
.default(20)
|
|
2495
|
+
.describe("Number of events per page"),
|
|
2496
|
+
page: z.number().optional().describe("Page number for pagination"),
|
|
2497
|
+
})
|
|
2498
|
+
.refine(data => (data.project_id || data.group_id) && !(data.project_id && data.group_id), {
|
|
2499
|
+
message: "Provide exactly one of project_id or group_id",
|
|
2500
|
+
});
|
|
2501
|
+
export const GetWebhookEventSchema = z
|
|
2502
|
+
.object({
|
|
2503
|
+
project_id: z.coerce
|
|
2504
|
+
.string()
|
|
2505
|
+
.optional()
|
|
2506
|
+
.describe("Project ID or URL-encoded path. Provide either project_id or group_id, not both."),
|
|
2507
|
+
group_id: z.coerce
|
|
2508
|
+
.string()
|
|
2509
|
+
.optional()
|
|
2510
|
+
.describe("Group ID or URL-encoded path. Provide either project_id or group_id, not both."),
|
|
2511
|
+
hook_id: z.coerce.number().describe("ID of the webhook"),
|
|
2512
|
+
event_id: z.coerce.number().describe("ID of the webhook event to retrieve"),
|
|
2513
|
+
page: z
|
|
2514
|
+
.number()
|
|
2515
|
+
.optional()
|
|
2516
|
+
.describe("If known, the page where the event is located (from list_webhook_events). Skips auto-pagination and fetches only this page."),
|
|
2517
|
+
})
|
|
2518
|
+
.refine(data => (data.project_id || data.group_id) && !(data.project_id && data.group_id), {
|
|
2519
|
+
message: "Provide exactly one of project_id or group_id",
|
|
2520
|
+
});
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NO_PROXY Integration Test
|
|
3
|
+
* Tests NO_PROXY functionality with mock servers
|
|
4
|
+
*/
|
|
5
|
+
import { describe, test, after, before } 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
|
+
// Test constants
|
|
11
|
+
const MOCK_TOKEN = 'glpat-mock-token-12345';
|
|
12
|
+
// Port ranges
|
|
13
|
+
const MOCK_GITLAB_PORT_BASE = 9600;
|
|
14
|
+
const MCP_SERVER_PORT_BASE = 3600;
|
|
15
|
+
console.log('🌐 NO_PROXY Integration Test Suite');
|
|
16
|
+
console.log('');
|
|
17
|
+
describe('NO_PROXY Integration Tests', () => {
|
|
18
|
+
let mcpUrl;
|
|
19
|
+
let mockGitLab;
|
|
20
|
+
let servers = [];
|
|
21
|
+
let mockGitLabUrl;
|
|
22
|
+
let mockGitLabHost;
|
|
23
|
+
before(async () => {
|
|
24
|
+
// Start mock GitLab server
|
|
25
|
+
const mockPort = await findMockServerPort(MOCK_GITLAB_PORT_BASE);
|
|
26
|
+
mockGitLab = new MockGitLabServer({
|
|
27
|
+
port: mockPort,
|
|
28
|
+
validTokens: [MOCK_TOKEN]
|
|
29
|
+
});
|
|
30
|
+
await mockGitLab.start();
|
|
31
|
+
mockGitLabUrl = mockGitLab.getUrl();
|
|
32
|
+
// Extract the host:port part for NO_PROXY
|
|
33
|
+
const url = new URL(mockGitLabUrl);
|
|
34
|
+
mockGitLabHost = url.host; // This includes port if non-standard
|
|
35
|
+
console.log(`Mock GitLab: ${mockGitLabUrl}`);
|
|
36
|
+
console.log(`Mock GitLab Host: ${mockGitLabHost}`);
|
|
37
|
+
});
|
|
38
|
+
after(async () => {
|
|
39
|
+
cleanupServers(servers);
|
|
40
|
+
if (mockGitLab) {
|
|
41
|
+
await mockGitLab.stop();
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
test('should bypass proxy when hostname is in NO_PROXY', async () => {
|
|
45
|
+
// Start MCP server with proxy settings and NO_PROXY
|
|
46
|
+
const mcpPort = await findAvailablePort(MCP_SERVER_PORT_BASE);
|
|
47
|
+
const server = await launchServer({
|
|
48
|
+
mode: TransportMode.STREAMABLE_HTTP,
|
|
49
|
+
port: mcpPort,
|
|
50
|
+
timeout: 5000,
|
|
51
|
+
env: {
|
|
52
|
+
STREAMABLE_HTTP: 'true',
|
|
53
|
+
REMOTE_AUTHORIZATION: 'true',
|
|
54
|
+
GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
|
|
55
|
+
// Set a fake proxy that would fail if used
|
|
56
|
+
HTTP_PROXY: 'http://nonexistent-proxy.example.com:9999',
|
|
57
|
+
HTTPS_PROXY: 'http://nonexistent-proxy.example.com:9999',
|
|
58
|
+
// Bypass proxy for our mock GitLab server
|
|
59
|
+
NO_PROXY: mockGitLabHost,
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
servers.push(server);
|
|
63
|
+
mcpUrl = `http://${HOST}:${mcpPort}/mcp`;
|
|
64
|
+
console.log(`MCP Server: ${mcpUrl}`);
|
|
65
|
+
console.log(`NO_PROXY: ${mockGitLabHost}`);
|
|
66
|
+
// Create client and make a request
|
|
67
|
+
const client = new CustomHeaderClient({
|
|
68
|
+
'authorization': `Bearer ${MOCK_TOKEN}`,
|
|
69
|
+
});
|
|
70
|
+
await client.connect(mcpUrl);
|
|
71
|
+
// This should succeed because the proxy is bypassed
|
|
72
|
+
const result = await client.callTool('list_projects', { per_page: 1 });
|
|
73
|
+
console.log(' ✓ Request succeeded with NO_PROXY bypass');
|
|
74
|
+
assert.ok(result, 'Request should succeed');
|
|
75
|
+
await client.disconnect();
|
|
76
|
+
});
|
|
77
|
+
test('should use proxy when hostname is NOT in NO_PROXY', async () => {
|
|
78
|
+
// Start MCP server with proxy settings but NO_PROXY doesn't match
|
|
79
|
+
const mcpPort = await findAvailablePort(MCP_SERVER_PORT_BASE + 1);
|
|
80
|
+
const server = await launchServer({
|
|
81
|
+
mode: TransportMode.STREAMABLE_HTTP,
|
|
82
|
+
port: mcpPort,
|
|
83
|
+
timeout: 5000,
|
|
84
|
+
env: {
|
|
85
|
+
STREAMABLE_HTTP: 'true',
|
|
86
|
+
REMOTE_AUTHORIZATION: 'true',
|
|
87
|
+
GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
|
|
88
|
+
// Set a fake proxy that would fail if used
|
|
89
|
+
HTTP_PROXY: 'http://nonexistent-proxy.example.com:9999',
|
|
90
|
+
HTTPS_PROXY: 'http://nonexistent-proxy.example.com:9999',
|
|
91
|
+
// NO_PROXY doesn't match our server
|
|
92
|
+
NO_PROXY: 'different-host.example.com,10.0.0.1',
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
servers.push(server);
|
|
96
|
+
mcpUrl = `http://${HOST}:${mcpPort}/mcp`;
|
|
97
|
+
console.log(`MCP Server: ${mcpUrl}`);
|
|
98
|
+
console.log(`NO_PROXY: different-host.example.com,10.0.0.1 (should NOT match)`);
|
|
99
|
+
// Create client and make a request
|
|
100
|
+
const client = new CustomHeaderClient({
|
|
101
|
+
'authorization': `Bearer ${MOCK_TOKEN}`,
|
|
102
|
+
});
|
|
103
|
+
await client.connect(mcpUrl);
|
|
104
|
+
// This should fail because it tries to use the nonexistent proxy
|
|
105
|
+
try {
|
|
106
|
+
await client.callTool('list_projects', { per_page: 1 });
|
|
107
|
+
assert.fail('Request should have failed due to proxy connection error');
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
console.log(' ✓ Request failed as expected (proxy error)');
|
|
111
|
+
// Expected to fail with connection/proxy error
|
|
112
|
+
assert.ok(error, 'Should throw an error when proxy fails');
|
|
113
|
+
}
|
|
114
|
+
await client.disconnect();
|
|
115
|
+
});
|
|
116
|
+
test('should bypass proxy with wildcard NO_PROXY', async () => {
|
|
117
|
+
// Start MCP server with wildcard NO_PROXY
|
|
118
|
+
const mcpPort = await findAvailablePort(MCP_SERVER_PORT_BASE + 2);
|
|
119
|
+
const server = await launchServer({
|
|
120
|
+
mode: TransportMode.STREAMABLE_HTTP,
|
|
121
|
+
port: mcpPort,
|
|
122
|
+
timeout: 5000,
|
|
123
|
+
env: {
|
|
124
|
+
STREAMABLE_HTTP: 'true',
|
|
125
|
+
REMOTE_AUTHORIZATION: 'true',
|
|
126
|
+
GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
|
|
127
|
+
// Set a fake proxy that would fail if used
|
|
128
|
+
HTTP_PROXY: 'http://nonexistent-proxy.example.com:9999',
|
|
129
|
+
HTTPS_PROXY: 'http://nonexistent-proxy.example.com:9999',
|
|
130
|
+
// Wildcard bypasses all proxies
|
|
131
|
+
NO_PROXY: '*',
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
servers.push(server);
|
|
135
|
+
mcpUrl = `http://${HOST}:${mcpPort}/mcp`;
|
|
136
|
+
console.log(`MCP Server: ${mcpUrl}`);
|
|
137
|
+
console.log(`NO_PROXY: * (wildcard - bypasses all)`);
|
|
138
|
+
// Create client and make a request
|
|
139
|
+
const client = new CustomHeaderClient({
|
|
140
|
+
'authorization': `Bearer ${MOCK_TOKEN}`,
|
|
141
|
+
});
|
|
142
|
+
await client.connect(mcpUrl);
|
|
143
|
+
// This should succeed because wildcard bypasses all proxies
|
|
144
|
+
const result = await client.callTool('list_projects', { per_page: 1 });
|
|
145
|
+
console.log(' ✓ Request succeeded with wildcard NO_PROXY');
|
|
146
|
+
assert.ok(result, 'Request should succeed');
|
|
147
|
+
await client.disconnect();
|
|
148
|
+
});
|
|
149
|
+
test('should bypass proxy with exact IP and localhost', async () => {
|
|
150
|
+
// Start MCP server with explicit IP and localhost patterns
|
|
151
|
+
const mcpPort = await findAvailablePort(MCP_SERVER_PORT_BASE + 3);
|
|
152
|
+
const server = await launchServer({
|
|
153
|
+
mode: TransportMode.STREAMABLE_HTTP,
|
|
154
|
+
port: mcpPort,
|
|
155
|
+
timeout: 5000,
|
|
156
|
+
env: {
|
|
157
|
+
STREAMABLE_HTTP: 'true',
|
|
158
|
+
REMOTE_AUTHORIZATION: 'true',
|
|
159
|
+
GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
|
|
160
|
+
// Set a fake proxy that would fail if used
|
|
161
|
+
HTTP_PROXY: 'http://nonexistent-proxy.example.com:9999',
|
|
162
|
+
HTTPS_PROXY: 'http://nonexistent-proxy.example.com:9999',
|
|
163
|
+
// Use explicit localhost and 127.0.0.1 patterns
|
|
164
|
+
NO_PROXY: 'localhost,127.0.0.1',
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
servers.push(server);
|
|
168
|
+
mcpUrl = `http://${HOST}:${mcpPort}/mcp`;
|
|
169
|
+
console.log(`MCP Server: ${mcpUrl}`);
|
|
170
|
+
console.log(`NO_PROXY: localhost,127.0.0.1 (exact matches)`);
|
|
171
|
+
// Create client and make a request
|
|
172
|
+
const client = new CustomHeaderClient({
|
|
173
|
+
'authorization': `Bearer ${MOCK_TOKEN}`,
|
|
174
|
+
});
|
|
175
|
+
await client.connect(mcpUrl);
|
|
176
|
+
// This should succeed because 127.0.0.1 and localhost are explicitly in NO_PROXY
|
|
177
|
+
const result = await client.callTool('list_projects', { per_page: 1 });
|
|
178
|
+
console.log(' ✓ Request succeeded with exact IP/localhost NO_PROXY match');
|
|
179
|
+
assert.ok(result, 'Request should succeed');
|
|
180
|
+
await client.disconnect();
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
console.log('✅ NO_PROXY integration tests completed');
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NO_PROXY Test Suite
|
|
3
|
+
* Tests NO_PROXY pattern matching and proxy bypass functionality
|
|
4
|
+
*/
|
|
5
|
+
import { describe, test } from 'node:test';
|
|
6
|
+
import assert from 'node:assert';
|
|
7
|
+
import { GitLabClientPool } from '../gitlab-client-pool.js';
|
|
8
|
+
console.log('🚫 NO_PROXY Test Suite');
|
|
9
|
+
console.log('');
|
|
10
|
+
describe('NO_PROXY Pattern Matching', () => {
|
|
11
|
+
test('should bypass proxy for exact hostname match', () => {
|
|
12
|
+
const pool = new GitLabClientPool({
|
|
13
|
+
httpProxy: 'http://proxy.example.com:8080',
|
|
14
|
+
httpsProxy: 'http://proxy.example.com:8080',
|
|
15
|
+
noProxy: 'gitlab.internal.com',
|
|
16
|
+
});
|
|
17
|
+
// Create agent for the NO_PROXY matched host
|
|
18
|
+
const agent = pool.getOrCreateAgentForUrl('https://gitlab.internal.com/api/v4');
|
|
19
|
+
// The agent should NOT be a proxy agent
|
|
20
|
+
// It should be a standard HTTPS agent
|
|
21
|
+
assert.ok(agent, 'Agent should be created');
|
|
22
|
+
assert.strictEqual(agent.constructor.name, 'Agent', 'Should be a standard Agent, not a proxy agent');
|
|
23
|
+
});
|
|
24
|
+
test('should use proxy for non-matching hostname', () => {
|
|
25
|
+
const pool = new GitLabClientPool({
|
|
26
|
+
httpProxy: 'http://proxy.example.com:8080',
|
|
27
|
+
httpsProxy: 'http://proxy.example.com:8080',
|
|
28
|
+
noProxy: 'gitlab.internal.com',
|
|
29
|
+
});
|
|
30
|
+
// Create agent for a host that should use proxy
|
|
31
|
+
const agent = pool.getOrCreateAgentForUrl('https://gitlab.external.com/api/v4');
|
|
32
|
+
// The agent should be a proxy agent
|
|
33
|
+
assert.ok(agent, 'Agent should be created');
|
|
34
|
+
assert.notStrictEqual(agent.constructor.name, 'Agent', 'Should be a proxy agent, not standard Agent');
|
|
35
|
+
});
|
|
36
|
+
test('should bypass proxy for domain suffix match', () => {
|
|
37
|
+
const pool = new GitLabClientPool({
|
|
38
|
+
httpProxy: 'http://proxy.example.com:8080',
|
|
39
|
+
httpsProxy: 'http://proxy.example.com:8080',
|
|
40
|
+
noProxy: '.internal.com',
|
|
41
|
+
});
|
|
42
|
+
// Test multiple subdomains
|
|
43
|
+
const agent1 = pool.getOrCreateAgentForUrl('https://gitlab.internal.com/api/v4');
|
|
44
|
+
const agent2 = pool.getOrCreateAgentForUrl('https://api.internal.com/api/v4');
|
|
45
|
+
const agent3 = pool.getOrCreateAgentForUrl('https://dev.gitlab.internal.com/api/v4');
|
|
46
|
+
assert.strictEqual(agent1.constructor.name, 'Agent', 'gitlab.internal.com should bypass proxy');
|
|
47
|
+
assert.strictEqual(agent2.constructor.name, 'Agent', 'api.internal.com should bypass proxy');
|
|
48
|
+
assert.strictEqual(agent3.constructor.name, 'Agent', 'dev.gitlab.internal.com should bypass proxy');
|
|
49
|
+
});
|
|
50
|
+
test('should bypass proxy for localhost', () => {
|
|
51
|
+
const pool = new GitLabClientPool({
|
|
52
|
+
httpProxy: 'http://proxy.example.com:8080',
|
|
53
|
+
httpsProxy: 'http://proxy.example.com:8080',
|
|
54
|
+
noProxy: 'localhost,127.0.0.1',
|
|
55
|
+
});
|
|
56
|
+
const agent1 = pool.getOrCreateAgentForUrl('http://localhost:8080/api/v4');
|
|
57
|
+
const agent2 = pool.getOrCreateAgentForUrl('http://127.0.0.1:8080/api/v4');
|
|
58
|
+
assert.strictEqual(agent1.constructor.name, 'Agent', 'localhost should bypass proxy');
|
|
59
|
+
assert.strictEqual(agent2.constructor.name, 'Agent', '127.0.0.1 should bypass proxy');
|
|
60
|
+
});
|
|
61
|
+
test('should bypass proxy for wildcard pattern', () => {
|
|
62
|
+
const pool = new GitLabClientPool({
|
|
63
|
+
httpProxy: 'http://proxy.example.com:8080',
|
|
64
|
+
httpsProxy: 'http://proxy.example.com:8080',
|
|
65
|
+
noProxy: '*',
|
|
66
|
+
});
|
|
67
|
+
const agent = pool.getOrCreateAgentForUrl('https://gitlab.com/api/v4');
|
|
68
|
+
assert.strictEqual(agent.constructor.name, 'Agent', 'Wildcard should bypass all proxies');
|
|
69
|
+
});
|
|
70
|
+
test('should handle multiple NO_PROXY patterns', () => {
|
|
71
|
+
const pool = new GitLabClientPool({
|
|
72
|
+
httpProxy: 'http://proxy.example.com:8080',
|
|
73
|
+
httpsProxy: 'http://proxy.example.com:8080',
|
|
74
|
+
noProxy: 'localhost,.internal.com,192.168.1.1',
|
|
75
|
+
});
|
|
76
|
+
const agent1 = pool.getOrCreateAgentForUrl('http://localhost/api/v4');
|
|
77
|
+
const agent2 = pool.getOrCreateAgentForUrl('https://gitlab.internal.com/api/v4');
|
|
78
|
+
const agent3 = pool.getOrCreateAgentForUrl('http://192.168.1.1/api/v4');
|
|
79
|
+
const agent4 = pool.getOrCreateAgentForUrl('https://gitlab.com/api/v4');
|
|
80
|
+
assert.strictEqual(agent1.constructor.name, 'Agent', 'localhost should bypass proxy');
|
|
81
|
+
assert.strictEqual(agent2.constructor.name, 'Agent', '.internal.com should bypass proxy');
|
|
82
|
+
assert.strictEqual(agent3.constructor.name, 'Agent', '192.168.1.1 should bypass proxy');
|
|
83
|
+
assert.notStrictEqual(agent4.constructor.name, 'Agent', 'gitlab.com should use proxy');
|
|
84
|
+
});
|
|
85
|
+
test('should handle NO_PROXY with whitespace', () => {
|
|
86
|
+
const pool = new GitLabClientPool({
|
|
87
|
+
httpProxy: 'http://proxy.example.com:8080',
|
|
88
|
+
httpsProxy: 'http://proxy.example.com:8080',
|
|
89
|
+
noProxy: ' localhost , .internal.com , 192.168.1.1 ',
|
|
90
|
+
});
|
|
91
|
+
const agent1 = pool.getOrCreateAgentForUrl('http://localhost/api/v4');
|
|
92
|
+
const agent2 = pool.getOrCreateAgentForUrl('https://gitlab.internal.com/api/v4');
|
|
93
|
+
assert.strictEqual(agent1.constructor.name, 'Agent', 'Should handle whitespace in NO_PROXY');
|
|
94
|
+
assert.strictEqual(agent2.constructor.name, 'Agent', 'Should handle whitespace in NO_PROXY');
|
|
95
|
+
});
|
|
96
|
+
test('should work without NO_PROXY set', () => {
|
|
97
|
+
const pool = new GitLabClientPool({
|
|
98
|
+
httpProxy: 'http://proxy.example.com:8080',
|
|
99
|
+
httpsProxy: 'http://proxy.example.com:8080',
|
|
100
|
+
});
|
|
101
|
+
const agent = pool.getOrCreateAgentForUrl('https://gitlab.com/api/v4');
|
|
102
|
+
// Should use proxy when NO_PROXY is not set
|
|
103
|
+
assert.notStrictEqual(agent.constructor.name, 'Agent', 'Should use proxy when NO_PROXY is not set');
|
|
104
|
+
});
|
|
105
|
+
test('should work with NO_PROXY set but empty', () => {
|
|
106
|
+
const pool = new GitLabClientPool({
|
|
107
|
+
httpProxy: 'http://proxy.example.com:8080',
|
|
108
|
+
httpsProxy: 'http://proxy.example.com:8080',
|
|
109
|
+
noProxy: '',
|
|
110
|
+
});
|
|
111
|
+
const agent = pool.getOrCreateAgentForUrl('https://gitlab.com/api/v4');
|
|
112
|
+
// Should use proxy when NO_PROXY is empty
|
|
113
|
+
assert.notStrictEqual(agent.constructor.name, 'Agent', 'Should use proxy when NO_PROXY is empty');
|
|
114
|
+
});
|
|
115
|
+
test('should handle port-specific patterns', () => {
|
|
116
|
+
const pool = new GitLabClientPool({
|
|
117
|
+
httpProxy: 'http://proxy.example.com:8080',
|
|
118
|
+
httpsProxy: 'http://proxy.example.com:8080',
|
|
119
|
+
noProxy: 'gitlab.internal.com:443',
|
|
120
|
+
});
|
|
121
|
+
const agent1 = pool.getOrCreateAgentForUrl('https://gitlab.internal.com/api/v4'); // HTTPS uses port 443 by default
|
|
122
|
+
const agent2 = pool.getOrCreateAgentForUrl('http://gitlab.internal.com/api/v4'); // HTTP uses port 80 by default
|
|
123
|
+
assert.strictEqual(agent1.constructor.name, 'Agent', 'Should bypass proxy for matching port');
|
|
124
|
+
assert.notStrictEqual(agent2.constructor.name, 'Agent', 'Should use proxy for non-matching port');
|
|
125
|
+
});
|
|
126
|
+
test('should handle case-insensitive matching', () => {
|
|
127
|
+
const pool = new GitLabClientPool({
|
|
128
|
+
httpProxy: 'http://proxy.example.com:8080',
|
|
129
|
+
httpsProxy: 'http://proxy.example.com:8080',
|
|
130
|
+
noProxy: 'GitLab.Internal.COM',
|
|
131
|
+
});
|
|
132
|
+
const agent1 = pool.getOrCreateAgentForUrl('https://gitlab.internal.com/api/v4');
|
|
133
|
+
const agent2 = pool.getOrCreateAgentForUrl('https://GITLAB.INTERNAL.COM/api/v4');
|
|
134
|
+
assert.strictEqual(agent1.constructor.name, 'Agent', 'Should match case-insensitively (lowercase)');
|
|
135
|
+
assert.strictEqual(agent2.constructor.name, 'Agent', 'Should match case-insensitively (uppercase)');
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
console.log('✅ NO_PROXY tests completed');
|
|
@@ -9,6 +9,7 @@ import { MockGitLabServer, findMockServerPort } from './utils/mock-gitlab-server
|
|
|
9
9
|
import { CustomHeaderClient } from './clients/custom-header-client.js';
|
|
10
10
|
// Test constants
|
|
11
11
|
const MOCK_TOKEN = 'glpat-mock-token-12345';
|
|
12
|
+
const MOCK_JOB_TOKEN = 'glcbt-mock-job-token-9876';
|
|
12
13
|
const TEST_PROJECT_ID = '123';
|
|
13
14
|
// Port ranges to avoid collisions
|
|
14
15
|
const MOCK_GITLAB_PORT_BASE = 9000;
|
|
@@ -32,7 +33,7 @@ describe('Remote Authorization - Basic Functionality', () => {
|
|
|
32
33
|
const mockPort = await findMockServerPort(MOCK_GITLAB_PORT_BASE);
|
|
33
34
|
mockGitLab = new MockGitLabServer({
|
|
34
35
|
port: mockPort,
|
|
35
|
-
validTokens: [MOCK_TOKEN]
|
|
36
|
+
validTokens: [MOCK_TOKEN, MOCK_JOB_TOKEN],
|
|
36
37
|
});
|
|
37
38
|
await mockGitLab.start();
|
|
38
39
|
const mockGitLabUrl = mockGitLab.getUrl();
|
|
@@ -80,6 +81,16 @@ describe('Remote Authorization - Basic Functionality', () => {
|
|
|
80
81
|
console.log(` ✓ Connected with Private-Token, got ${tools.tools.length} tools`);
|
|
81
82
|
await client.disconnect();
|
|
82
83
|
});
|
|
84
|
+
test('should connect with JOB-TOKEN header', async () => {
|
|
85
|
+
const client = new CustomHeaderClient({
|
|
86
|
+
'job-token': MOCK_JOB_TOKEN,
|
|
87
|
+
});
|
|
88
|
+
await client.connect(mcpUrl);
|
|
89
|
+
const tools = await client.listTools();
|
|
90
|
+
assert.ok(tools.tools.length > 0, 'Should have tools');
|
|
91
|
+
console.log(` ✓ Connected with JOB-TOKEN, got ${tools.tools.length} tools`);
|
|
92
|
+
await client.disconnect();
|
|
93
|
+
});
|
|
83
94
|
test('should successfully call listTools with auth', async () => {
|
|
84
95
|
const client = new CustomHeaderClient({
|
|
85
96
|
'authorization': `Bearer ${MOCK_TOKEN}`
|