@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/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 = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" };
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 = Math.random().toString(36).substring(7);
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 = Math.random().toString(36).substring(7);
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 = Math.random().toString(36).substring(7);
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}`