@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.
@@ -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
  }
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 nodeFetch from "node-fetch";
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(nodeFetch, defaultAuthRetryConfig());
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(nodeFetch, newJar) : nodeFetch, defaultAuthRetryConfig());
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(nodeFetch, defaultAuthRetryConfig());
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 agent = clientPool.getAgentFunctionForUrl(effectiveApiUrl);
1063
+ const dispatcher = clientPool.getDispatcherForUrl(effectiveApiUrl);
1064
1064
  return {
1065
1065
  headers: { ...BASE_HEADERS, ...buildAuthHeaders() },
1066
- agent: agent,
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 {import("node-fetch").Response} response - The response from GitLab API
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"]; // Let form-data set the correct Content-Type with boundary
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
- getAgentFunctionForUrl: clientPool.getAgentFunctionForUrl.bind(clientPool),
9985
- fetch: nodeFetch,
10049
+ getDispatcherForUrl: clientPool.getDispatcherForUrl.bind(clientPool),
10050
+ fetch: undiciFetch,
9986
10051
  logger,
9987
10052
  };
9988
10053
  }