@zereight/mcp-gitlab 2.1.55 → 2.1.57
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 +5 -5
- package/README.md +32 -29
- package/README.zh-CN.md +5 -5
- package/build/auth-retry.js +37 -20
- package/build/downloads/proxy.js +6 -6
- package/build/gitlab-client-pool.js +148 -112
- package/build/index.js +81 -16
- package/build/schemas.js +118 -8
- package/build/scripts/generate-tool-docs.js +1 -1
- package/build/server/request-helpers.js +4 -1
- package/build/test/no-proxy-test.js +11 -0
- package/build/test/remote-auth-tests.js +0 -1
- package/build/test/server/request-helpers.test.js +11 -1
- package/build/test/streamable-http-unauthenticated-discovery.test.js +48 -0
- package/build/test/test-auth-retry.js +39 -32
- 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
|
}
|
package/build/index.js
CHANGED
|
@@ -69,7 +69,7 @@ import fetchCookie from "fetch-cookie";
|
|
|
69
69
|
import fs from "node:fs";
|
|
70
70
|
import { pipeline as streamPipeline } from "node:stream/promises";
|
|
71
71
|
import os from "node:os";
|
|
72
|
-
import
|
|
72
|
+
import { fetch as undiciFetch, File, FormData } from "undici";
|
|
73
73
|
import path from "node:path";
|
|
74
74
|
import { CookieJar, parse as parseCookie } from "tough-cookie";
|
|
75
75
|
import { URL } from "node:url";
|
|
@@ -111,7 +111,7 @@ GitLabDiscussionNoteSchema, // Added
|
|
|
111
111
|
GitLabDiscussionSchema,
|
|
112
112
|
// Draft Notes Schemas
|
|
113
113
|
GitLabDraftNoteSchema, GitLabForkSchema, GitLabBranchSchema, GitLabProtectedBranchSchema, GitLabGroupSchema, GitLabIssueLinkSchema, GitLabIssueSchema, GitLabIssueWithLinkDetailsSchema, GitLabMarkdownUploadSchema, GitLabMergeRequestPipelineSchema, GitLabMergeRequestSchema, GitLabMilestonesSchema, GitLabGroupMilestonesSchema, GitLabNamespaceExistsResponseSchema, GitLabNamespaceSchema, GitLabPipelineJobSchema, GitLabDeploymentSchema, GitLabEnvironmentSchema, GitLabPipelineSchema, GitLabPipelineScheduleSchema, GitLabPipelineScheduleVariableSchema, GitLabPipelineSchedulePlayResultSchema, ListPipelineSchedulesSchema, GetPipelineScheduleSchema, ListPipelineSchedulePipelinesSchema, CreatePipelineScheduleSchema, UpdatePipelineScheduleSchema, DeletePipelineScheduleSchema, PlayPipelineScheduleSchema, TakeOwnershipPipelineScheduleSchema, GetPipelineScheduleVariableSchema, CreatePipelineScheduleVariableSchema, UpdatePipelineScheduleVariableSchema, DeletePipelineScheduleVariableSchema, GitLabPipelineTriggerJobSchema, GitLabProjectMemberSchema, GitLabProjectSchema, GitLabTodoSchema, GitLabReferenceSchema, GitLabRepositorySchema, GitLabSearchBlobResultSchema, GitLabSearchResponseSchema, GitLabTreeItemSchema, GitLabUserSchema, GitLabUsersResponseSchema, GitLabWikiPageSchema, GroupIteration, ListCommitStatusesSchema, ListBranchesSchema, ListCommitsSchema, ListDraftNotesSchema, ListGroupIterationsSchema, ListGroupProjectsSchema, GitLabCiVariableSchema, ListProjectVariablesSchema, GetProjectVariableSchema, CreateProjectVariableSchema, UpdateProjectVariableSchema, DeleteProjectVariableSchema, ListGroupVariablesSchema, GetGroupVariableSchema, CreateGroupVariableSchema, UpdateGroupVariableSchema, DeleteGroupVariableSchema, GitLabDependencyProxySchema, GitLabDependencyProxyBlobSchema, GetDependencyProxySettingsSchema, UpdateDependencyProxySettingsSchema, ListDependencyProxyBlobsSchema, PurgeDependencyProxyCacheSchema, ListProjectVulnerabilitiesSchema, GetVulnerabilitySchema, DismissVulnerabilitySchema, ConfirmVulnerabilitySchema, ListIssueDiscussionsSchema, ListIssueLinksSchema, ListIssuesSchema, ListTodosSchema, ListLabelsSchema, ListMergeRequestDiffsSchema, // Added
|
|
114
|
-
GetMergeRequestFileDiffSchema, ListMergeRequestChangedFilesSchema, ListMergeRequestDiscussionsSchema, ListMergeRequestPipelinesSchema, ListMergeRequestsSchema, ListGroupMergeRequestsSchema, ListMergeRequestVersionsSchema, GetMergeRequestVersionSchema, GitLabMergeRequestVersionSchema, GitLabMergeRequestVersionDetailSchema, ListNamespacesSchema, ListPipelineJobsSchema, ListPipelinesSchema, ListDeploymentsSchema, ListEnvironmentsSchema, ListPipelineTriggerJobsSchema, ValidateCiLintSchema, ValidateProjectCiLintSchema, ListCiCatalogResourcesSchema, GetCiCatalogResourceSchema, ListProjectMembersSchema, ListGroupMembersSchema, ListProjectMilestonesSchema, ListGroupMilestonesSchema, ListProjectsSchema, ListWikiPagesSchema, GetGroupWikiPageSchema, ListGroupWikiPagesSchema, UpdateGroupWikiPageSchema, MarkdownUploadSchema, MarkdownUploadRemoteSchema, DownloadAttachmentSchema, DownloadJobArtifactsSchema, GetJobArtifactFileSchema, GitLabArtifactEntrySchema, ListJobArtifactsSchema, MergeMergeRequestSchema, ApproveMergeRequestSchema, UnapproveMergeRequestSchema, GetMergeRequestApprovalStateSchema, GetMergeRequestConflictsSchema, GitLabMergeRequestApprovalsResponseSchema, GitLabMergeRequestApprovalStateSchema, MyIssuesSchema, MarkAllTodosDoneSchema, MarkTodoDoneSchema, PaginatedDiscussionsResponseSchema, PromoteProjectMilestoneSchema, PublishDraftNoteSchema, PlayPipelineJobSchema, PushFilesSchema, RetryPipelineJobSchema, RetryPipelineSchema, SearchCodeSchema, SearchGroupCodeSchema, SearchProjectCodeSchema, SearchRepositoriesSchema, UpdateDraftNoteSchema, UpdateIssueNoteSchema, UpdateIssueSchema, UpdateIssueDescriptionPatchSchema, UpdateLabelSchema, UpdateProjectSchema, UpdateMergeRequestNoteSchema, UpdateMergeRequestDiscussionNoteSchema, UpdateMergeRequestSchema, UpdateWikiPageSchema, VerifyNamespaceSchema, GitLabEventSchema, ListEventsSchema, GetProjectEventsSchema, ExecuteGraphQLSchema, GitLabReleaseSchema, ListReleasesSchema, GetReleaseSchema, CreateReleaseSchema, UpdateReleaseSchema, DeleteReleaseSchema, CreateReleaseEvidenceSchema, DownloadReleaseAssetSchema, ListTagsSchema, GetTagSchema, CreateTagSchema, DeleteTagSchema, GetTagSignatureSchema, GitLabTagSchema, GitLabTagSignatureSchema, GetMergeRequestNotesSchema, GetMergeRequestNoteSchema, DeleteMergeRequestDiscussionNoteSchema, ResolveMergeRequestThreadSchema, GetWorkItemSchema, ListWorkItemsSchema, CreateWorkItemSchema, UpdateWorkItemSchema, ConvertWorkItemTypeSchema, ListWorkItemStatusesSchema, ListWorkItemNotesSchema, CreateWorkItemNoteSchema, CreateWorkItemEmojiReactionSchema, CreateWorkItemNoteEmojiReactionSchema, ListWorkItemEmojiReactionsSchema, ListWorkItemNoteEmojiReactionsSchema, DeleteWorkItemEmojiReactionSchema, DeleteWorkItemNoteEmojiReactionSchema, MoveWorkItemSchema, ListCustomFieldDefinitionsSchema, GetTimelineEventsSchema, CreateTimelineEventSchema, ListWebhooksSchema, ListWebhookEventsSchema, GetWebhookEventSchema, HealthCheckSchema, } from "./schemas.js";
|
|
114
|
+
GetMergeRequestFileDiffSchema, ListMergeRequestChangedFilesSchema, ListMergeRequestDiscussionsSchema, ListMergeRequestPipelinesSchema, ListMergeRequestsSchema, ListGroupMergeRequestsSchema, ListMergeRequestVersionsSchema, GetMergeRequestVersionSchema, GitLabMergeRequestVersionSchema, GitLabMergeRequestVersionDetailSchema, ListNamespacesSchema, ListPipelineJobsSchema, ListPipelinesSchema, ListDeploymentsSchema, ListEnvironmentsSchema, ListPipelineTriggerJobsSchema, ValidateCiLintSchema, ValidateProjectCiLintSchema, ListCiCatalogResourcesSchema, GetCiCatalogResourceSchema, ListProjectMembersSchema, ListGroupMembersSchema, ListProjectMilestonesSchema, ListGroupMilestonesSchema, ListProjectsSchema, ListWikiPagesSchema, GetGroupWikiPageSchema, ListGroupWikiPagesSchema, UpdateGroupWikiPageSchema, MarkdownUploadSchema, MarkdownUploadRemoteSchema, DownloadAttachmentSchema, DownloadJobArtifactsSchema, GetJobArtifactFileSchema, GitLabArtifactEntrySchema, ListJobArtifactsSchema, MergeMergeRequestSchema, ApproveMergeRequestSchema, UnapproveMergeRequestSchema, GetMergeRequestApprovalStateSchema, GetMergeRequestConflictsSchema, GitLabMergeRequestApprovalsResponseSchema, GitLabMergeRequestApprovalStateSchema, MyIssuesSchema, MarkAllTodosDoneSchema, MarkTodoDoneSchema, PaginatedDiscussionsResponseSchema, PromoteProjectMilestoneSchema, PublishDraftNoteSchema, PlayPipelineJobSchema, PushFilesSchema, RetryPipelineJobSchema, RetryPipelineSchema, SearchCodeSchema, SearchGroupCodeSchema, SearchProjectCodeSchema, SearchRepositoriesSchema, UpdateDraftNoteSchema, UpdateIssueNoteSchema, UpdateIssueSchema, UpdateIssueDescriptionPatchSchema, UpdateLabelSchema, UpdateProjectSchema, UpdateMergeRequestNoteSchema, UpdateMergeRequestDiscussionNoteSchema, UpdateMergeRequestSchema, UpdateWikiPageSchema, VerifyNamespaceSchema, GitLabEventSchema, ListEventsSchema, GetProjectEventsSchema, ExecuteGraphQLSchema, GitLabReleaseSchema, ListReleasesSchema, GetReleaseSchema, CreateReleaseSchema, UpdateReleaseSchema, DeleteReleaseSchema, CreateReleaseEvidenceSchema, DownloadReleaseAssetSchema, ListTagsSchema, GetTagSchema, CreateTagSchema, DeleteTagSchema, GetTagSignatureSchema, GitLabTagSchema, GitLabTagSignatureSchema, GetMergeRequestNotesSchema, GetMergeRequestNoteSchema, DeleteMergeRequestDiscussionNoteSchema, ResolveMergeRequestThreadSchema, GetWorkItemSchema, ListWorkItemsSchema, CreateWorkItemSchema, UpdateWorkItemSchema, ConvertWorkItemTypeSchema, ListWorkItemStatusesSchema, ListWorkItemNotesSchema, CreateWorkItemNoteSchema, CreateWorkItemEmojiReactionSchema, CreateWorkItemNoteEmojiReactionSchema, ListWorkItemEmojiReactionsSchema, ListWorkItemNoteEmojiReactionsSchema, DeleteWorkItemEmojiReactionSchema, DeleteWorkItemNoteEmojiReactionSchema, MoveWorkItemSchema, ListCustomFieldDefinitionsSchema, GetTimelineEventsSchema, CreateTimelineEventSchema, ListWebhooksSchema, CreateWebhookSchema, UpdateWebhookSchema, DeleteWebhookSchema, ListWebhookEventsSchema, GetWebhookEventSchema, HealthCheckSchema, } from "./schemas.js";
|
|
115
115
|
import { randomUUID, timingSafeEqual, } from "node:crypto";
|
|
116
116
|
import { createLogger } from "./utils/logger.js";
|
|
117
117
|
const logger = createLogger();
|
|
@@ -913,7 +913,7 @@ function defaultAuthRetryConfig() {
|
|
|
913
913
|
}
|
|
914
914
|
// Cookie jar and fetch - reloaded when cookie file changes
|
|
915
915
|
let cookieJar = null;
|
|
916
|
-
let fetch = wrapWithAuthRetry(
|
|
916
|
+
let fetch = wrapWithAuthRetry(undiciFetch, defaultAuthRetryConfig());
|
|
917
917
|
let lastCookieMtime = 0;
|
|
918
918
|
let cookieReloadLock = null; // Mutex to prevent parallel reloads
|
|
919
919
|
// Auth proxies may redirect and set cookies on the first request. We make a throwaway
|
|
@@ -933,7 +933,7 @@ async function reloadCookiesIfChanged() {
|
|
|
933
933
|
lastCookieMtime = mtime;
|
|
934
934
|
const newJar = await createCookieJar();
|
|
935
935
|
cookieJar = newJar;
|
|
936
|
-
fetch = wrapWithAuthRetry(newJar ? fetchCookie(
|
|
936
|
+
fetch = wrapWithAuthRetry(newJar ? fetchCookie(undiciFetch, newJar) : undiciFetch, defaultAuthRetryConfig());
|
|
937
937
|
initialSessionRequestMade = false;
|
|
938
938
|
}
|
|
939
939
|
}
|
|
@@ -942,7 +942,7 @@ async function reloadCookiesIfChanged() {
|
|
|
942
942
|
if (cookieJar) {
|
|
943
943
|
logger.info("Cookie file removed, clearing cached cookies");
|
|
944
944
|
cookieJar = null;
|
|
945
|
-
fetch = wrapWithAuthRetry(
|
|
945
|
+
fetch = wrapWithAuthRetry(undiciFetch, defaultAuthRetryConfig());
|
|
946
946
|
lastCookieMtime = 0;
|
|
947
947
|
initialSessionRequestMade = false;
|
|
948
948
|
}
|
|
@@ -1060,10 +1060,10 @@ function getEffectiveApiUrl() {
|
|
|
1060
1060
|
*/
|
|
1061
1061
|
const getFetchConfig = () => {
|
|
1062
1062
|
const effectiveApiUrl = getEffectiveApiUrl();
|
|
1063
|
-
const
|
|
1063
|
+
const dispatcher = clientPool.getDispatcherForUrl(effectiveApiUrl);
|
|
1064
1064
|
return {
|
|
1065
1065
|
headers: { ...BASE_HEADERS, ...buildAuthHeaders() },
|
|
1066
|
-
|
|
1066
|
+
dispatcher,
|
|
1067
1067
|
};
|
|
1068
1068
|
};
|
|
1069
1069
|
// Compute at startup
|
|
@@ -1193,7 +1193,7 @@ if (getPositionalCliCommand(process.argv) !== "auth" &&
|
|
|
1193
1193
|
* Utility function for handling GitLab API errors
|
|
1194
1194
|
* API 에러 처리를 위한 유틸리티 함수 (Utility function for handling API errors)
|
|
1195
1195
|
*
|
|
1196
|
-
* @param {
|
|
1196
|
+
* @param {Response} response - The response from GitLab API
|
|
1197
1197
|
* @throws {Error} Throws an error with response details if the request failed
|
|
1198
1198
|
*/
|
|
1199
1199
|
async function handleGitLabError(response) {
|
|
@@ -4816,6 +4816,54 @@ async function listWebhooks(options) {
|
|
|
4816
4816
|
await handleGitLabError(response);
|
|
4817
4817
|
return (await response.json());
|
|
4818
4818
|
}
|
|
4819
|
+
function buildWebhookMutationBody(options) {
|
|
4820
|
+
const body = {};
|
|
4821
|
+
for (const [key, value] of Object.entries(options)) {
|
|
4822
|
+
if (key === "project_id" || key === "group_id" || key === "hook_id")
|
|
4823
|
+
continue;
|
|
4824
|
+
if (value !== undefined)
|
|
4825
|
+
body[key] = value;
|
|
4826
|
+
}
|
|
4827
|
+
return body;
|
|
4828
|
+
}
|
|
4829
|
+
/**
|
|
4830
|
+
* Create a webhook on a project or group
|
|
4831
|
+
*/
|
|
4832
|
+
async function createWebhook(options) {
|
|
4833
|
+
const url = buildWebhookBaseUrl(options.project_id, options.group_id);
|
|
4834
|
+
const response = await fetch(url, {
|
|
4835
|
+
...getFetchConfig(),
|
|
4836
|
+
method: "POST",
|
|
4837
|
+
body: JSON.stringify(buildWebhookMutationBody(options)),
|
|
4838
|
+
});
|
|
4839
|
+
await handleGitLabError(response);
|
|
4840
|
+
return await response.json();
|
|
4841
|
+
}
|
|
4842
|
+
/**
|
|
4843
|
+
* Update a webhook on a project or group
|
|
4844
|
+
*/
|
|
4845
|
+
async function updateWebhook(options) {
|
|
4846
|
+
const url = `${buildWebhookBaseUrl(options.project_id, options.group_id)}/${options.hook_id}`;
|
|
4847
|
+
const response = await fetch(url, {
|
|
4848
|
+
...getFetchConfig(),
|
|
4849
|
+
method: "PUT",
|
|
4850
|
+
body: JSON.stringify(buildWebhookMutationBody(options)),
|
|
4851
|
+
});
|
|
4852
|
+
await handleGitLabError(response);
|
|
4853
|
+
return await response.json();
|
|
4854
|
+
}
|
|
4855
|
+
/**
|
|
4856
|
+
* Delete a webhook from a project or group
|
|
4857
|
+
*/
|
|
4858
|
+
async function deleteWebhook(options) {
|
|
4859
|
+
const url = `${buildWebhookBaseUrl(options.project_id, options.group_id)}/${options.hook_id}`;
|
|
4860
|
+
const response = await fetch(url, {
|
|
4861
|
+
...getFetchConfig(),
|
|
4862
|
+
method: "DELETE",
|
|
4863
|
+
});
|
|
4864
|
+
await handleGitLabError(response);
|
|
4865
|
+
return { status: "deleted", hook_id: options.hook_id };
|
|
4866
|
+
}
|
|
4819
4867
|
/**
|
|
4820
4868
|
* Fetch a single page of webhook events
|
|
4821
4869
|
*/
|
|
@@ -7015,15 +7063,11 @@ async function markdownUpload(projectId, filePath, content, filename) {
|
|
|
7015
7063
|
throw new Error("Either file_path or content must be provided");
|
|
7016
7064
|
}
|
|
7017
7065
|
// Create form data
|
|
7018
|
-
const FormData = (await import("form-data")).default;
|
|
7019
7066
|
const form = new FormData();
|
|
7020
|
-
form.append("file", fileBuffer, {
|
|
7021
|
-
filename: fileName,
|
|
7022
|
-
contentType: "application/octet-stream",
|
|
7023
|
-
});
|
|
7067
|
+
form.append("file", new File([fileBuffer], fileName, { type: "application/octet-stream" }));
|
|
7024
7068
|
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/uploads`);
|
|
7025
7069
|
const defaultFetchConfig = getFetchConfig();
|
|
7026
|
-
delete defaultFetchConfig.headers["Content-Type"];
|
|
7070
|
+
delete defaultFetchConfig.headers["Content-Type"];
|
|
7027
7071
|
const response = await fetch(url.toString(), {
|
|
7028
7072
|
...defaultFetchConfig,
|
|
7029
7073
|
method: "POST",
|
|
@@ -9744,6 +9788,27 @@ async function handleToolCall(params) {
|
|
|
9744
9788
|
content: [{ type: "text", text: JSON.stringify(webhooks) }],
|
|
9745
9789
|
};
|
|
9746
9790
|
}
|
|
9791
|
+
case "create_webhook": {
|
|
9792
|
+
const args = CreateWebhookSchema.parse(params.arguments);
|
|
9793
|
+
const webhook = await createWebhook(args);
|
|
9794
|
+
return {
|
|
9795
|
+
content: [{ type: "text", text: JSON.stringify(webhook) }],
|
|
9796
|
+
};
|
|
9797
|
+
}
|
|
9798
|
+
case "update_webhook": {
|
|
9799
|
+
const args = UpdateWebhookSchema.parse(params.arguments);
|
|
9800
|
+
const webhook = await updateWebhook(args);
|
|
9801
|
+
return {
|
|
9802
|
+
content: [{ type: "text", text: JSON.stringify(webhook) }],
|
|
9803
|
+
};
|
|
9804
|
+
}
|
|
9805
|
+
case "delete_webhook": {
|
|
9806
|
+
const args = DeleteWebhookSchema.parse(params.arguments);
|
|
9807
|
+
const result = await deleteWebhook(args);
|
|
9808
|
+
return {
|
|
9809
|
+
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
9810
|
+
};
|
|
9811
|
+
}
|
|
9747
9812
|
case "list_webhook_events": {
|
|
9748
9813
|
const args = ListWebhookEventsSchema.parse(params.arguments);
|
|
9749
9814
|
const events = await listWebhookEvents(args);
|
|
@@ -9981,8 +10046,8 @@ function buildDownloadProxyDeps() {
|
|
|
9981
10046
|
encodeGitLabPathSegment,
|
|
9982
10047
|
encodeGitLabPath,
|
|
9983
10048
|
getEffectiveProjectId,
|
|
9984
|
-
|
|
9985
|
-
fetch:
|
|
10049
|
+
getDispatcherForUrl: clientPool.getDispatcherForUrl.bind(clientPool),
|
|
10050
|
+
fetch: undiciFetch,
|
|
9986
10051
|
logger,
|
|
9987
10052
|
};
|
|
9988
10053
|
}
|