@zereight/mcp-gitlab 2.1.56 → 2.1.58

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.
@@ -1,9 +1,7 @@
1
- import { Agent } from "http";
2
- import { Agent as HttpsAgent } from "https";
3
- import { HttpProxyAgent } from "http-proxy-agent";
4
- import { HttpsProxyAgent } from "https-proxy-agent";
1
+ import fs from "node:fs";
2
+ import http from "node:http";
5
3
  import { SocksProxyAgent } from "socks-proxy-agent";
6
- import fs from "fs";
4
+ import { Agent, Dispatcher, ProxyAgent } from "undici";
7
5
  /**
8
6
  * Checks if a URL should bypass the proxy based on NO_PROXY patterns.
9
7
  * Supports:
@@ -21,7 +19,6 @@ function shouldBypassProxy(url, noProxy) {
21
19
  if (!noProxy) {
22
20
  return false;
23
21
  }
24
- // Parse URL to get hostname and port
25
22
  let hostname;
26
23
  let port;
27
24
  let protocol;
@@ -29,185 +26,224 @@ function shouldBypassProxy(url, noProxy) {
29
26
  const parsedUrl = new URL(url);
30
27
  hostname = parsedUrl.hostname.toLowerCase();
31
28
  protocol = parsedUrl.protocol;
32
- // Use explicit port if provided, otherwise use default port based on protocol
33
- port = parsedUrl.port || (protocol === 'https:' ? '443' : '80');
29
+ port = parsedUrl.port || (protocol === "https:" ? "443" : "80");
34
30
  }
35
31
  catch {
36
32
  return false;
37
33
  }
38
- // Split NO_PROXY into patterns and trim whitespace
39
- const patterns = noProxy.split(',').map(p => p.trim().toLowerCase()).filter(p => p.length > 0);
34
+ const patterns = noProxy
35
+ .split(",")
36
+ .map(p => p.trim().toLowerCase())
37
+ .filter(p => p.length > 0);
40
38
  for (const pattern of patterns) {
41
- // Wildcard matches everything
42
- if (pattern === '*') {
39
+ if (pattern === "*") {
43
40
  return true;
44
41
  }
45
- // Handle port-specific patterns (e.g., "example.com:8080")
46
- const [patternHost, patternPort] = pattern.split(':');
47
- // If pattern specifies a port, check if it matches
42
+ const [patternHost, patternPort] = pattern.split(":");
48
43
  if (patternPort && port !== patternPort) {
49
44
  continue;
50
45
  }
51
- // Check for domain suffix match (e.g., ".example.com")
52
- if (patternHost.startsWith('.')) {
46
+ if (patternHost.startsWith(".")) {
53
47
  const suffix = patternHost.substring(1);
54
- if (hostname === suffix || hostname.endsWith('.' + suffix)) {
48
+ if (hostname === suffix || hostname.endsWith("." + suffix)) {
55
49
  return true;
56
50
  }
57
51
  }
58
- // Check for exact hostname match
59
52
  else if (hostname === patternHost) {
60
53
  return true;
61
54
  }
62
55
  }
63
56
  return false;
64
57
  }
58
+ class ProtocolDispatcher extends Dispatcher {
59
+ httpDirect;
60
+ httpsDirect;
61
+ httpProxyDispatcher;
62
+ httpsProxyDispatcher;
63
+ noProxy;
64
+ constructor(httpDirect, httpsDirect, httpProxyDispatcher, httpsProxyDispatcher, noProxy) {
65
+ super();
66
+ this.httpDirect = httpDirect;
67
+ this.httpsDirect = httpsDirect;
68
+ this.httpProxyDispatcher = httpProxyDispatcher;
69
+ this.httpsProxyDispatcher = httpsProxyDispatcher;
70
+ this.noProxy = noProxy;
71
+ }
72
+ dispatcherForOrigin(originText) {
73
+ const useHttps = originText.startsWith("https:");
74
+ const bypass = shouldBypassProxy(originText, this.noProxy);
75
+ if (bypass) {
76
+ return useHttps ? this.httpsDirect : this.httpDirect;
77
+ }
78
+ return useHttps ? this.httpsProxyDispatcher : this.httpProxyDispatcher;
79
+ }
80
+ dispatch(options, handler) {
81
+ const origin = options.origin;
82
+ const originText = origin instanceof URL ? origin.href : String(origin ?? "");
83
+ return this.dispatcherForOrigin(originText).dispatch(options, handler);
84
+ }
85
+ }
86
+ function createSocksConnect(proxyUrl, tls) {
87
+ const socksAgent = new SocksProxyAgent(proxyUrl);
88
+ return (options, callback) => {
89
+ const port = Number(options.port);
90
+ const host = options.hostname;
91
+ const dummyReq = http.request({
92
+ hostname: "127.0.0.1",
93
+ port: 9,
94
+ path: "/",
95
+ method: "HEAD",
96
+ agent: false,
97
+ });
98
+ dummyReq.on("error", () => undefined);
99
+ dummyReq.destroy();
100
+ const connectThroughSocks = (connectOpts) => {
101
+ void socksAgent
102
+ .connect(dummyReq, connectOpts)
103
+ .then(socket => {
104
+ callback(null, socket);
105
+ })
106
+ .catch((error) => {
107
+ const err = error instanceof Error ? error : new Error("SOCKS connect failed");
108
+ callback(err, null);
109
+ });
110
+ };
111
+ if (options.protocol === "https:") {
112
+ connectThroughSocks({
113
+ secureEndpoint: true,
114
+ host,
115
+ port,
116
+ servername: options.servername,
117
+ rejectUnauthorized: tls.rejectUnauthorized,
118
+ ca: tls.ca,
119
+ });
120
+ return;
121
+ }
122
+ connectThroughSocks({
123
+ secureEndpoint: false,
124
+ host,
125
+ port,
126
+ });
127
+ };
128
+ }
129
+ function createDispatcher(proxyUrl, tls) {
130
+ const hasTls = tls.rejectUnauthorized === false || tls.ca !== undefined;
131
+ if (proxyUrl?.startsWith("socks")) {
132
+ return new Agent({ connect: createSocksConnect(proxyUrl, tls) });
133
+ }
134
+ if (proxyUrl) {
135
+ if (hasTls) {
136
+ return new ProxyAgent({ uri: proxyUrl, requestTls: tls });
137
+ }
138
+ return new ProxyAgent(proxyUrl);
139
+ }
140
+ if (hasTls) {
141
+ return new Agent({ connect: tls });
142
+ }
143
+ return new Agent();
144
+ }
65
145
  /**
66
- * Manages a pool of HTTP/HTTPS agents for different GitLab API URLs.
146
+ * Manages a pool of undici dispatchers for different GitLab API URLs.
67
147
  * This allows the server to efficiently handle requests to multiple GitLab instances
68
- * by reusing agents and their underlying TCP connections.
148
+ * by reusing dispatchers and their underlying TCP connections.
69
149
  */
70
150
  export class GitLabClientPool {
71
151
  clients = new Map();
72
152
  options;
73
153
  constructor(options) {
74
154
  this.options = options;
75
- // Initialization is now done on-demand
76
155
  }
77
- /**
78
- * Creates a pair of HTTP and HTTPS agents for a specific API URL,
79
- * considering proxy and SSL/TLS settings.
80
- * @param apiUrl The base URL for which to create the agents.
81
- * @returns A `ClientAgents` object containing the configured agents.
82
- */
83
- createAgentsForUrl(apiUrl) {
156
+ createDispatchersForUrl(apiUrl) {
84
157
  const { httpProxy, httpsProxy, noProxy, rejectUnauthorized, caCertPath } = this.options;
85
- let sslOptions = {};
158
+ const tls = {};
86
159
  if (rejectUnauthorized === false) {
87
- sslOptions.rejectUnauthorized = false;
160
+ tls.rejectUnauthorized = false;
88
161
  }
89
162
  else if (caCertPath) {
90
163
  try {
91
- sslOptions.ca = fs.readFileSync(caCertPath);
164
+ tls.ca = fs.readFileSync(caCertPath);
92
165
  }
93
166
  catch (error) {
94
167
  console.error(`Failed to read CA certificate from ${caCertPath}:`, error);
95
168
  throw new Error(`Failed to read CA certificate: ${caCertPath}`);
96
169
  }
97
170
  }
98
- // Check if this URL should bypass the proxy
171
+ const httpDirect = createDispatcher(undefined, tls);
172
+ const httpsDirect = createDispatcher(undefined, tls);
173
+ const httpProxyDispatcher = createDispatcher(httpProxy, tls);
174
+ const httpsProxyDispatcher = createDispatcher(httpsProxy, tls);
99
175
  const bypassProxy = shouldBypassProxy(apiUrl, noProxy);
100
- let httpAgent;
101
- let httpsAgent;
102
- // Configure HTTP agent with proxy if specified and not bypassed
103
- if (httpProxy && !bypassProxy) {
104
- httpAgent = httpProxy.startsWith("socks")
105
- ? new SocksProxyAgent(httpProxy)
106
- : new HttpProxyAgent(httpProxy);
107
- }
108
- else {
109
- httpAgent = new Agent({ keepAlive: true });
110
- }
111
- // Configure HTTPS agent with proxy and SSL options if specified and not bypassed
112
- if (httpsProxy && !bypassProxy) {
113
- httpsAgent = httpsProxy.startsWith("socks")
114
- // The `as any` cast is used here to bypass a TypeScript type mismatch error.
115
- // The `socks-proxy-agent` documentation indicates that TLS options like
116
- // `rejectUnauthorized` and `ca` are valid in the constructor's options
117
- // object, but the type definitions in this environment seem to disagree.
118
- // This cast ensures the options are passed through at runtime.
119
- ? new SocksProxyAgent(httpsProxy, sslOptions)
120
- : new HttpsProxyAgent(httpsProxy, { ...sslOptions });
121
- }
122
- else {
123
- httpsAgent = new HttpsAgent({ ...sslOptions, keepAlive: true });
124
- }
125
- return { httpAgent, httpsAgent };
176
+ return {
177
+ httpDispatcher: bypassProxy ? httpDirect : httpProxyDispatcher,
178
+ httpsDispatcher: bypassProxy ? httpsDirect : httpsProxyDispatcher,
179
+ dispatcher: new ProtocolDispatcher(httpDirect, httpsDirect, httpProxyDispatcher, httpsProxyDispatcher, noProxy),
180
+ };
126
181
  }
127
182
  /**
128
- * Retrieves the appropriate agent (HTTP or HTTPS) for a given API URL.
129
- * If an agent for the URL does not exist, it creates and caches one.
130
- * @param apiUrl The full URL of the request.
131
- * @returns The corresponding `Agent` for the URL's protocol.
183
+ * Retrieves the protocol-specific dispatcher for a given API URL.
184
+ * Used by NO_PROXY tests to distinguish Agent vs ProxyAgent.
132
185
  */
133
186
  getOrCreateAgentForUrl(apiUrl) {
134
- const agents = this.getOrCreateAgentsForUrl(apiUrl);
187
+ const dispatchers = this.getOrCreateDispatchersForUrl(apiUrl);
135
188
  const url = new URL(apiUrl);
136
- return url.protocol === "https:" ? agents.httpsAgent : agents.httpAgent;
189
+ return url.protocol === "https:" ? dispatchers.httpsDispatcher : dispatchers.httpDispatcher;
137
190
  }
138
191
  /**
139
- * Returns an agent-selection function for use with node-fetch's `agent` option.
140
- * The returned function picks the correct HTTP or HTTPS agent based on the
141
- * request URL's protocol. This is critical for self-hosted GitLab instances
142
- * where the server may redirect between HTTP and HTTPS (e.g., when
143
- * `external_url` differs from the actual internal protocol).
144
- * @param apiUrl The base API URL used to look up or create the agent pair.
145
- * @returns A function `(parsedURL: URL) => Agent` suitable for node-fetch.
192
+ * Returns a dispatcher that picks HTTP vs HTTPS based on the request origin.
193
+ * Needed when a self-hosted GitLab redirects between HTTP and HTTPS.
146
194
  */
147
- getAgentFunctionForUrl(apiUrl) {
148
- const agents = this.getOrCreateAgentsForUrl(apiUrl);
149
- return (parsedURL) => {
150
- return parsedURL.protocol === "https:" ? agents.httpsAgent : agents.httpAgent;
151
- };
195
+ getDispatcherForUrl(apiUrl) {
196
+ return this.getOrCreateDispatchersForUrl(apiUrl).dispatcher;
152
197
  }
153
198
  /**
154
- * Ensures agents exist for the given API URL and returns the pair.
155
- * @param apiUrl The full URL of the request.
156
- * @returns The `ClientAgents` (both HTTP and HTTPS agents) for the URL.
199
+ * Which inner dispatcher would handle `originUrl` after following a redirect
200
+ * from `apiUrl`. Used to verify NO_PROXY is re-evaluated per origin.
157
201
  */
158
- getOrCreateAgentsForUrl(apiUrl) {
202
+ getDispatcherForOrigin(apiUrl, originUrl) {
203
+ const dispatcher = this.getOrCreateDispatchersForUrl(apiUrl).dispatcher;
204
+ if (dispatcher instanceof ProtocolDispatcher) {
205
+ return dispatcher.dispatcherForOrigin(originUrl);
206
+ }
207
+ return dispatcher;
208
+ }
209
+ getOrCreateDispatchersForUrl(apiUrl) {
159
210
  const url = new URL(apiUrl);
160
- const baseUrl = `${url.protocol}//${url.host}${url.pathname.substring(0, url.pathname.lastIndexOf('/api/v4') + '/api/v4'.length)}`;
211
+ const apiIndex = url.pathname.lastIndexOf("/api/v4");
212
+ const basePath = apiIndex === -1 ? url.pathname : url.pathname.substring(0, apiIndex + "/api/v4".length);
213
+ const baseUrl = `${url.protocol}//${url.host}${basePath}`;
161
214
  if (!this.clients.has(baseUrl)) {
162
- // Check pool size limit
163
215
  if (this.options.poolMaxSize !== undefined && this.clients.size >= this.options.poolMaxSize) {
164
216
  throw new Error(`Server capacity reached: Connection pool is full (max ${this.options.poolMaxSize} instances). Please try again later.`);
165
217
  }
166
- this.clients.set(baseUrl, this.createAgentsForUrl(baseUrl));
218
+ this.clients.set(baseUrl, this.createDispatchersForUrl(baseUrl));
167
219
  }
168
- const agents = this.clients.get(baseUrl);
169
- if (!agents) {
170
- // This should not happen given the logic above, but it satisfies TypeScript
220
+ const dispatchers = this.clients.get(baseUrl);
221
+ if (!dispatchers) {
171
222
  throw new Error(`Failed to create or get client for URL: ${baseUrl}`);
172
223
  }
173
- return agents;
224
+ return dispatchers;
174
225
  }
175
- /**
176
- * Retrieves the client agents for a specific base API URL.
177
- * @param apiUrl The base API URL (e.g., "https://gitlab.com/api/v4").
178
- * @returns The `ClientAgents` object or undefined if not found.
179
- */
180
226
  getClient(apiUrl) {
181
227
  return this.clients.get(apiUrl);
182
228
  }
183
- /**
184
- * Returns the default client agents, which corresponds to the first URL in the list.
185
- * @returns The default `ClientAgents`.
186
- */
187
229
  getDefaultClient() {
188
230
  const defaultUrl = this.options.apiUrls?.[0];
189
231
  if (!defaultUrl) {
190
232
  throw new Error("No default API URL configured.");
191
233
  }
192
234
  if (!this.clients.has(defaultUrl)) {
193
- this.clients.set(defaultUrl, this.createAgentsForUrl(defaultUrl));
235
+ this.clients.set(defaultUrl, this.createDispatchersForUrl(defaultUrl));
194
236
  }
195
- return this.clients.get(defaultUrl);
237
+ const client = this.clients.get(defaultUrl);
238
+ if (!client) {
239
+ throw new Error("No default API URL configured.");
240
+ }
241
+ return client;
196
242
  }
197
- /**
198
- * Destroy all pooled agents and clear pool state.
199
- * This should be called on graceful shutdown so sockets are closed
200
- * and the process can exit cleanly.
201
- */
202
243
  closeAll() {
203
- for (const [, agents] of this.clients) {
204
- const destroyIfSupported = (agent) => {
205
- if (agent && typeof agent.destroy === "function") {
206
- agent.destroy();
207
- }
208
- };
209
- destroyIfSupported(agents.httpAgent);
210
- destroyIfSupported(agents.httpsAgent);
244
+ for (const [, dispatchers] of this.clients) {
245
+ void dispatchers.httpDispatcher.destroy();
246
+ void dispatchers.httpsDispatcher.destroy();
211
247
  }
212
248
  this.clients.clear();
213
249
  }