@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.
- package/README.ko.md +4 -4
- package/README.md +31 -28
- package/README.zh-CN.md +4 -4
- package/build/auth-retry.js +37 -20
- package/build/config.js +2 -0
- package/build/downloads/proxy.js +6 -6
- package/build/gitlab-client-pool.js +148 -112
- package/build/index.js +233 -51
- package/build/schemas.js +119 -8
- package/build/scripts/generate-tool-docs.js +1 -1
- package/build/stateless/session-id.js +6 -1
- package/build/test/no-proxy-test.js +11 -0
- package/build/test/remote-auth-tests.js +0 -1
- package/build/test/stateless/session-id.test.js +26 -0
- package/build/test/test-auth-retry.js +39 -32
- package/build/test/test-dynamic-project-scope.js +362 -0
- package/build/test/test-permission-mode.js +1 -0
- package/build/test/test-toolset-filtering.js +2 -2
- package/build/test/test-webhooks.js +218 -0
- package/build/test-note.js +0 -1
- package/build/test-resolve-issue-note.js +0 -1
- package/build/tools/registry.js +21 -1
- package/package.json +3 -7
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
42
|
-
if (pattern === '*') {
|
|
39
|
+
if (pattern === "*") {
|
|
43
40
|
return true;
|
|
44
41
|
}
|
|
45
|
-
|
|
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
|
-
|
|
52
|
-
if (patternHost.startsWith('.')) {
|
|
46
|
+
if (patternHost.startsWith(".")) {
|
|
53
47
|
const suffix = patternHost.substring(1);
|
|
54
|
-
if (hostname === suffix || hostname.endsWith(
|
|
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
|
|
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
|
|
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
|
-
|
|
158
|
+
const tls = {};
|
|
86
159
|
if (rejectUnauthorized === false) {
|
|
87
|
-
|
|
160
|
+
tls.rejectUnauthorized = false;
|
|
88
161
|
}
|
|
89
162
|
else if (caCertPath) {
|
|
90
163
|
try {
|
|
91
|
-
|
|
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
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
|
129
|
-
*
|
|
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
|
|
187
|
+
const dispatchers = this.getOrCreateDispatchersForUrl(apiUrl);
|
|
135
188
|
const url = new URL(apiUrl);
|
|
136
|
-
return url.protocol === "https:" ?
|
|
189
|
+
return url.protocol === "https:" ? dispatchers.httpsDispatcher : dispatchers.httpDispatcher;
|
|
137
190
|
}
|
|
138
191
|
/**
|
|
139
|
-
* Returns
|
|
140
|
-
*
|
|
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
|
-
|
|
148
|
-
|
|
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
|
-
*
|
|
155
|
-
*
|
|
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
|
-
|
|
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
|
|
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.
|
|
218
|
+
this.clients.set(baseUrl, this.createDispatchersForUrl(baseUrl));
|
|
167
219
|
}
|
|
168
|
-
const
|
|
169
|
-
if (!
|
|
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
|
|
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.
|
|
235
|
+
this.clients.set(defaultUrl, this.createDispatchersForUrl(defaultUrl));
|
|
194
236
|
}
|
|
195
|
-
|
|
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 [,
|
|
204
|
-
|
|
205
|
-
|
|
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
|
}
|