@zereight/mcp-gitlab 2.0.36 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.js CHANGED
@@ -1,23 +1,6 @@
1
1
  #!/usr/bin/env node
2
- // Parse CLI arguments
3
- const args = process.argv.slice(2);
4
- const cliArgs = {};
5
- for (let i = 0; i < args.length; i++) {
6
- const arg = args[i];
7
- if (arg.startsWith("--")) {
8
- const [key, value] = arg.slice(2).split("=");
9
- if (value) {
10
- cliArgs[key] = value;
11
- }
12
- else if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
13
- cliArgs[key] = args[++i];
14
- }
15
- }
16
- }
17
- function getConfig(cliKey, envKey, defaultValue) {
18
- return cliArgs[cliKey] || process.env[envKey] || defaultValue;
19
- }
20
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import { getConfig, ENABLE_DYNAMIC_API_URL, GITLAB_AUTH_COOKIE_PATH, GITLAB_CA_CERT_PATH, GITLAB_JOB_TOKEN, GITLAB_MCP_OAUTH, GITLAB_OAUTH_APP_ID, GITLAB_OAUTH_SCOPES, GITLAB_PERSONAL_ACCESS_TOKEN, GITLAB_POOL_MAX_SIZE, GITLAB_READ_ONLY_MODE, GITLAB_TOOLSETS_RAW, GITLAB_TOOLS_RAW, HOST, HTTP_PROXY, HTTPS_PROXY, IS_OLD, MCP_SERVER_URL, NODE_TLS_REJECT_UNAUTHORIZED, NO_PROXY, PORT, REMOTE_AUTHORIZATION, SESSION_TIMEOUT_SECONDS, SSE, STREAMABLE_HTTP, USE_GITLAB_WIKI, USE_MILESTONE, USE_OAUTH, USE_PIPELINE, GITLAB_TOOL_POLICY_APPROVE_RAW, GITLAB_TOOL_POLICY_HIDDEN_RAW, } from "./config.js";
3
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
21
4
  import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
22
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
23
6
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
@@ -27,23 +10,19 @@ import express from "express";
27
10
  import fetchCookie from "fetch-cookie";
28
11
  import fs from "node:fs";
29
12
  import os from "node:os";
30
- import { HttpProxyAgent } from "http-proxy-agent";
31
- import { HttpsProxyAgent } from "https-proxy-agent";
32
13
  import nodeFetch from "node-fetch";
33
14
  import path, { dirname } from "node:path";
34
- import { SocksProxyAgent } from "socks-proxy-agent";
35
15
  import { CookieJar, parse as parseCookie } from "tough-cookie";
36
16
  import { fileURLToPath, URL } from "node:url";
37
17
  import { z } from "zod";
38
- import { zodToJsonSchema } from "zod-to-json-schema";
39
18
  import { initializeOAuthClient } from "./oauth.js";
40
19
  import { createGitLabOAuthProvider } from "./oauth-proxy.js";
41
20
  import { mcpAuthRouter } from "@modelcontextprotocol/sdk/server/auth/router.js";
21
+ import { normalizeGitLabApiUrl } from "./utils/url.js";
22
+ import { estimateMergeCommitCount, filterDiffsByPatterns, summarizeWebhookEvents } from "./utils/helpers.js";
42
23
  import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js";
43
24
  import { GitLabClientPool } from "./gitlab-client-pool.js";
44
- // Add type imports for proxy agents
45
- import { Agent } from "node:http";
46
- import { Agent as HttpsAgent } from "node:https";
25
+ import { allTools, readOnlyTools, destructiveTools, parseEnabledToolsets, parseIndividualTools, buildFeatureFlagOverrides, isToolInEnabledToolset, TOOLSET_DEFINITIONS, ALL_TOOLSET_IDS, } from "./tools/registry.js";
47
26
  import { BulkPublishDraftNotesSchema, CancelPipelineJobSchema, CancelPipelineSchema, CreateBranchSchema, CreateDraftNoteSchema, CreateIssueLinkSchema, CreateIssueNoteSchema, CreateIssueSchema, CreateLabelSchema, // Added
48
27
  CreateMergeRequestNoteSchema, CreateMergeRequestDiscussionNoteSchema, CreateMergeRequestSchema, CreateMergeRequestThreadSchema, CreateNoteSchema, CreateOrUpdateFileSchema, CreatePipelineSchema, CreateProjectMilestoneSchema, CreateRepositorySchema, CreateWikiPageSchema, CreateGroupWikiPageSchema, DeleteDraftNoteSchema, DeleteGroupWikiPageSchema, DeleteIssueLinkSchema, DeleteIssueSchema, DeleteLabelSchema, DeleteProjectMilestoneSchema, DeleteWikiPageSchema, DeleteMergeRequestNoteSchema, EditProjectMilestoneSchema, ForkRepositorySchema, GetBranchDiffsSchema, GetCommitDiffSchema, GetCommitSchema, GetDraftNoteSchema, GetFileContentsSchema, GetIssueLinkSchema, GetIssueSchema, GetLabelSchema, GetMergeRequestDiffsSchema, GetMergeRequestSchema, GetMilestoneBurndownEventsSchema, GetMilestoneIssuesSchema, GetMilestoneMergeRequestsSchema, GetDeploymentSchema, GetEnvironmentSchema, GetNamespaceSchema,
49
28
  // pipeline job schemas
@@ -52,7 +31,7 @@ GetPipelineJobOutputSchema, GetPipelineSchema, GetProjectMilestoneSchema, GetPro
52
31
  GitLabDiscussionNoteSchema, // Added
53
32
  GitLabDiscussionSchema,
54
33
  // Draft Notes Schemas
55
- GitLabDraftNoteSchema, GitLabForkSchema, GitLabIssueLinkSchema, GitLabIssueSchema, GitLabIssueWithLinkDetailsSchema, GitLabMarkdownUploadSchema, GitLabMergeRequestSchema, GitLabMilestonesSchema, GitLabNamespaceExistsResponseSchema, GitLabNamespaceSchema, GitLabPipelineJobSchema, GitLabDeploymentSchema, GitLabEnvironmentSchema, GitLabPipelineSchema, GitLabPipelineTriggerJobSchema, GitLabProjectMemberSchema, GitLabProjectSchema, GitLabReferenceSchema, GitLabRepositorySchema, GitLabSearchBlobResultSchema, GitLabSearchResponseSchema, GitLabTreeItemSchema, GitLabTreeSchema, GitLabUserSchema, GitLabUsersResponseSchema, GitLabWikiPageSchema, GroupIteration, ListCommitsSchema, ListDraftNotesSchema, ListGroupIterationsSchema, ListGroupProjectsSchema, ListIssueDiscussionsSchema, ListIssueLinksSchema, ListIssuesSchema, ListLabelsSchema, ListMergeRequestDiffsSchema, // Added
34
+ GitLabDraftNoteSchema, GitLabForkSchema, GitLabIssueLinkSchema, GitLabIssueSchema, GitLabIssueWithLinkDetailsSchema, GitLabMarkdownUploadSchema, GitLabMergeRequestSchema, GitLabMilestonesSchema, GitLabNamespaceExistsResponseSchema, GitLabNamespaceSchema, GitLabPipelineJobSchema, GitLabDeploymentSchema, GitLabEnvironmentSchema, GitLabPipelineSchema, GitLabPipelineTriggerJobSchema, GitLabProjectMemberSchema, GitLabProjectSchema, GitLabReferenceSchema, GitLabRepositorySchema, GitLabSearchBlobResultSchema, GitLabSearchResponseSchema, GitLabTreeItemSchema, GitLabUserSchema, GitLabUsersResponseSchema, GitLabWikiPageSchema, GroupIteration, ListCommitsSchema, ListDraftNotesSchema, ListGroupIterationsSchema, ListGroupProjectsSchema, ListIssueDiscussionsSchema, ListIssueLinksSchema, ListIssuesSchema, ListLabelsSchema, ListMergeRequestDiffsSchema, // Added
56
35
  GetMergeRequestFileDiffSchema, ListMergeRequestChangedFilesSchema, ListMergeRequestDiscussionsSchema, ListMergeRequestsSchema, ListMergeRequestVersionsSchema, GetMergeRequestVersionSchema, GitLabMergeRequestVersionSchema, GitLabMergeRequestVersionDetailSchema, ListNamespacesSchema, ListPipelineJobsSchema, ListPipelinesSchema, ListDeploymentsSchema, ListEnvironmentsSchema, ListPipelineTriggerJobsSchema, ListProjectMembersSchema, ListProjectMilestonesSchema, ListProjectsSchema, ListWikiPagesSchema, GetGroupWikiPageSchema, ListGroupWikiPagesSchema, UpdateGroupWikiPageSchema, MarkdownUploadSchema, DownloadAttachmentSchema, DownloadJobArtifactsSchema, GetJobArtifactFileSchema, GitLabArtifactEntrySchema, ListJobArtifactsSchema, MergeMergeRequestSchema, ApproveMergeRequestSchema, UnapproveMergeRequestSchema, GetMergeRequestApprovalStateSchema, GetMergeRequestConflictsSchema, GitLabMergeRequestApprovalsResponseSchema, GitLabMergeRequestApprovalStateSchema, MyIssuesSchema, PaginatedDiscussionsResponseSchema, PromoteProjectMilestoneSchema, PublishDraftNoteSchema, PlayPipelineJobSchema, PushFilesSchema, RetryPipelineJobSchema, RetryPipelineSchema, SearchCodeSchema, SearchGroupCodeSchema, SearchProjectCodeSchema, SearchRepositoriesSchema, UpdateDraftNoteSchema, UpdateIssueNoteSchema, UpdateIssueSchema, UpdateLabelSchema, UpdateMergeRequestNoteSchema, UpdateMergeRequestDiscussionNoteSchema, UpdateMergeRequestSchema, UpdateWikiPageSchema, VerifyNamespaceSchema, GitLabEventSchema, ListEventsSchema, GetProjectEventsSchema, ExecuteGraphQLSchema, GitLabReleaseSchema, ListReleasesSchema, GetReleaseSchema, CreateReleaseSchema, UpdateReleaseSchema, DeleteReleaseSchema, CreateReleaseEvidenceSchema, DownloadReleaseAssetSchema, GetMergeRequestNotesSchema, GetMergeRequestNoteSchema, DeleteMergeRequestDiscussionNoteSchema, ResolveMergeRequestThreadSchema, GetWorkItemSchema, ListWorkItemsSchema, CreateWorkItemSchema, UpdateWorkItemSchema, ConvertWorkItemTypeSchema, ListWorkItemStatusesSchema, ListWorkItemNotesSchema, CreateWorkItemNoteSchema, MoveWorkItemSchema, ListCustomFieldDefinitionsSchema, GetTimelineEventsSchema, CreateTimelineEventSchema, ListWebhooksSchema, ListWebhookEventsSchema, GetWebhookEventSchema, } from "./schemas.js";
57
36
  import { randomUUID } from "node:crypto";
58
37
  import { pino } from "pino";
@@ -118,10 +97,25 @@ function createServer() {
118
97
  ? toolsAfterLegacy.filter(tool => readOnlyTools.has(tool.name))
119
98
  : toolsAfterLegacy;
120
99
  // Step 5: Regex denial filter
121
- const precomputedFilteredTools = GITLAB_DENIED_TOOLS_REGEX
100
+ let filteredTools = GITLAB_DENIED_TOOLS_REGEX
122
101
  ? toolsAfterReadOnly.filter(tool => !GITLAB_DENIED_TOOLS_REGEX.test(tool.name))
123
- : toolsAfterReadOnly;
124
- const serverInstance = new Server({
102
+ : [...toolsAfterReadOnly];
103
+ // Step 5.5: Always include discover_tools meta-tool (bypasses toolset filter)
104
+ const discoverTool = allTools.find(t => t.name === "discover_tools");
105
+ const filteredToolNames = new Set(filteredTools.map(t => t.name));
106
+ if (discoverTool && !filteredToolNames.has("discover_tools")) {
107
+ // Respect read-only and regex denial filters
108
+ const passesReadOnly = !GITLAB_READ_ONLY_MODE || readOnlyTools.has("discover_tools");
109
+ const passesRegex = !GITLAB_DENIED_TOOLS_REGEX?.test("discover_tools");
110
+ if (passesReadOnly && passesRegex) {
111
+ filteredTools.push(discoverTool);
112
+ }
113
+ }
114
+ // Step 5.7: Remove hidden policy tools
115
+ if (hiddenToolSet.size > 0) {
116
+ filteredTools = filteredTools.filter(tool => !hiddenToolSet.has(tool.name));
117
+ }
118
+ const mcpServer = new McpServer({
125
119
  name: "better-gitlab-mcp-server",
126
120
  version: SERVER_VERSION,
127
121
  }, {
@@ -129,48 +123,190 @@ function createServer() {
129
123
  tools: {},
130
124
  },
131
125
  });
132
- serverInstance.setRequestHandler(ListToolsRequestSchema, async () => {
133
- // Step 6: Gemini $schema cleanup (only dynamic step per request)
126
+ mcpServer.server.setRequestHandler(ListToolsRequestSchema, async () => {
127
+ // Step 6: Gemini $schema cleanup + annotations (only dynamic step per request)
134
128
  // <<< START: Remove $schema for Gemini compatibility >>>
135
- const tools = precomputedFilteredTools.map(tool => {
136
- // Check if inputSchema exists and is an object
137
- if (tool.inputSchema && typeof tool.inputSchema === "object" && tool.inputSchema !== null) {
138
- // Remove $schema key if present
139
- if ("$schema" in tool.inputSchema) {
140
- // Create a new object to preserve immutability (optional but recommended)
141
- const modifiedSchema = { ...tool.inputSchema };
142
- delete modifiedSchema.$schema;
143
- return { ...tool, inputSchema: modifiedSchema };
129
+ const tools = filteredTools.map(tool => {
130
+ const modified = { ...tool };
131
+ // Safety net: remove $schema if present (toJSONSchema strips it for zod schemas,
132
+ // but manually-defined schemas like discover_tools may still have it)
133
+ if (modified.inputSchema && typeof modified.inputSchema === "object" && modified.inputSchema !== null) {
134
+ if ("$schema" in modified.inputSchema) {
135
+ modified.inputSchema = { ...modified.inputSchema };
136
+ delete modified.inputSchema.$schema;
144
137
  }
145
138
  }
146
- // Return as-is if no modification needed
147
- return tool;
139
+ // Add MCP tool annotations
140
+ modified.annotations = {
141
+ ...(readOnlyTools.has(tool.name) ? { readOnlyHint: true } : {}),
142
+ ...(destructiveTools.has(tool.name) ? { destructiveHint: true } : {}),
143
+ ...(approveToolSet.has(tool.name) ? { confirmationHint: true } : {}),
144
+ openWorldHint: true,
145
+ };
146
+ // Inject _confirmed optional parameter for approve-policy tools
147
+ if (approveToolSet.has(tool.name) && modified.inputSchema?.properties) {
148
+ modified.inputSchema = {
149
+ ...modified.inputSchema,
150
+ properties: {
151
+ ...modified.inputSchema.properties,
152
+ _confirmed: {
153
+ type: "boolean",
154
+ description: "Set to true to confirm execution of this approval-required tool.",
155
+ },
156
+ },
157
+ };
158
+ }
159
+ return modified;
148
160
  });
149
161
  // <<< END: Remove $schema for Gemini compatibility >>>
150
162
  return {
151
163
  tools, // return tool list with $schema removed
152
164
  };
153
165
  });
154
- serverInstance.setRequestHandler(CallToolRequestSchema, async (request) => {
166
+ mcpServer.server.setRequestHandler(CallToolRequestSchema, async (request) => {
155
167
  // Manually retrieve the session context using the session ID passed in the request.
156
168
  // This is a robust workaround for AsyncLocalStorage context loss.
157
169
  const sessionId = request.params.sessionId;
158
- if ((REMOTE_AUTHORIZATION || GITLAB_MCP_OAUTH) && sessionId && authBySession[sessionId]) {
159
- const authData = authBySession[sessionId];
160
- const sessionContext = {
161
- sessionId,
162
- header: authData.header,
163
- token: authData.token,
164
- lastUsed: authData.lastUsed,
165
- apiUrl: authData.apiUrl,
166
- };
167
- // Run the handler within the retrieved context
168
- return await sessionAuthStore.run(sessionContext, () => handleToolCall(request.params));
170
+ const toolName = request.params.name;
171
+ const start = Date.now();
172
+ const logCompletion = (result) => {
173
+ const durationMs = Date.now() - start;
174
+ logger.info({ tool: toolName, event: "tool_call_done", durationMs }, `tool_call_done: ${toolName} (${durationMs}ms)`);
175
+ return result;
176
+ };
177
+ const logError = (error) => {
178
+ const durationMs = Date.now() - start;
179
+ logger.error({ tool: toolName, event: "tool_call_error", durationMs, error: error instanceof Error ? error.message : String(error) }, `tool_call_error: ${toolName} (${durationMs}ms)`);
180
+ throw error;
181
+ };
182
+ try {
183
+ // Handle discover_tools meta-tool directly (needs access to mcpServer and filteredTools)
184
+ if (toolName === "discover_tools") {
185
+ const category = request.params.arguments?.category?.trim()?.toLowerCase();
186
+ const currentToolNames = new Set(filteredTools.map(t => t.name));
187
+ if (!category) {
188
+ // List available categories with activation status
189
+ const categories = TOOLSET_DEFINITIONS.map(def => ({
190
+ id: def.id,
191
+ toolCount: def.tools.size,
192
+ active: [...def.tools].some(t => currentToolNames.has(t)),
193
+ isDefault: def.isDefault,
194
+ }));
195
+ return logCompletion({
196
+ content: [{
197
+ type: "text",
198
+ text: JSON.stringify({ categories, hint: "Call discover_tools with a category name to activate it" }, null, 2),
199
+ }],
200
+ });
201
+ }
202
+ if (!ALL_TOOLSET_IDS.has(category)) {
203
+ return logCompletion({
204
+ content: [{
205
+ type: "text",
206
+ text: `Unknown category "${category}". Available: ${[...ALL_TOOLSET_IDS].join(", ")}`,
207
+ }],
208
+ isError: true,
209
+ });
210
+ }
211
+ const toolsetDef = TOOLSET_DEFINITIONS.find(d => d.id === category);
212
+ if (!toolsetDef) {
213
+ return logCompletion({
214
+ content: [{ type: "text", text: `Category "${category}" not found.` }],
215
+ isError: true,
216
+ });
217
+ }
218
+ // Check if already fully active
219
+ const alreadyActive = [...toolsetDef.tools].every(t => currentToolNames.has(t));
220
+ if (alreadyActive) {
221
+ return logCompletion({
222
+ content: [{
223
+ type: "text",
224
+ text: `Category "${category}" is already active (${toolsetDef.tools.size} tools).`,
225
+ }],
226
+ });
227
+ }
228
+ // Add tools from this toolset, respecting all filtering policies
229
+ const newTools = [];
230
+ for (const tool of allTools) {
231
+ if (!toolsetDef.tools.has(tool.name))
232
+ continue;
233
+ if (currentToolNames.has(tool.name))
234
+ continue;
235
+ if (GITLAB_READ_ONLY_MODE && !readOnlyTools.has(tool.name))
236
+ continue;
237
+ if (GITLAB_DENIED_TOOLS_REGEX?.test(tool.name))
238
+ continue;
239
+ if (hiddenToolSet.has(tool.name))
240
+ continue;
241
+ newTools.push(tool);
242
+ }
243
+ if (newTools.length === 0) {
244
+ return logCompletion({
245
+ content: [{
246
+ type: "text",
247
+ text: `Category "${category}" has no additional tools to activate (all already active or filtered).`,
248
+ }],
249
+ });
250
+ }
251
+ filteredTools.push(...newTools);
252
+ // Notify client that tool list has changed
253
+ try {
254
+ await mcpServer.server.sendToolListChanged();
255
+ }
256
+ catch {
257
+ // Client may not support notifications - safe to ignore
258
+ }
259
+ const addedNames = newTools.map(t => t.name);
260
+ logger.info({ event: "toolset_activated", category, toolCount: addedNames.length }, `Activated toolset: ${category} (+${addedNames.length} tools)`);
261
+ return logCompletion({
262
+ content: [{
263
+ type: "text",
264
+ text: JSON.stringify({
265
+ activated: category,
266
+ addedTools: addedNames,
267
+ totalTools: filteredTools.length,
268
+ }, null, 2),
269
+ }],
270
+ });
271
+ }
272
+ // Check approve policy: tool is exposed but requires explicit confirmation
273
+ if (approveToolSet.has(toolName)) {
274
+ const confirmed = request.params.arguments?._confirmed === true;
275
+ if (!confirmed) {
276
+ logger.info({ tool: toolName, event: "tool_call_approval_required" }, `Approval required: ${toolName}`);
277
+ return logCompletion({
278
+ content: [{
279
+ type: "text",
280
+ text: `Tool "${toolName}" requires confirmation. This tool is marked as requiring approval before execution. Re-call with _confirmed: true to proceed.`,
281
+ }],
282
+ });
283
+ }
284
+ // Strip _confirmed from args before forwarding to handler
285
+ const { _confirmed, ...cleanArgs } = request.params.arguments || {};
286
+ request.params.arguments = cleanArgs;
287
+ }
288
+ if ((REMOTE_AUTHORIZATION || GITLAB_MCP_OAUTH) && sessionId && authBySession[sessionId]) {
289
+ const authData = authBySession[sessionId];
290
+ const sessionContext = {
291
+ sessionId,
292
+ header: authData.header,
293
+ token: authData.token,
294
+ lastUsed: authData.lastUsed,
295
+ apiUrl: authData.apiUrl,
296
+ };
297
+ // Run the handler within the retrieved context
298
+ const result = await sessionAuthStore.run(sessionContext, () => handleToolCall(request.params));
299
+ return logCompletion(result);
300
+ }
301
+ // Fallback for non-remote-auth mode or if session is not found
302
+ const result = await handleToolCall(request.params);
303
+ return logCompletion(result);
304
+ }
305
+ catch (error) {
306
+ logError(error);
169
307
  }
170
- // Fallback for non-remote-auth mode or if session is not found
171
- return handleToolCall(request.params);
172
308
  });
173
- return serverInstance;
309
+ return mcpServer;
174
310
  }
175
311
  /**
176
312
  * Validate configuration at startup
@@ -274,8 +410,6 @@ function validateConfiguration() {
274
410
  }
275
411
  logger.info("Configuration validation passed");
276
412
  }
277
- const GITLAB_PERSONAL_ACCESS_TOKEN = getConfig("token", "GITLAB_PERSONAL_ACCESS_TOKEN");
278
- const GITLAB_JOB_TOKEN = getConfig("job-token", "GITLAB_JOB_TOKEN");
279
413
  let OAUTH_ACCESS_TOKEN = null;
280
414
  let oauthClient = null;
281
415
  /**
@@ -299,10 +433,6 @@ async function ensureValidOAuthToken() {
299
433
  throw error;
300
434
  }
301
435
  }
302
- const GITLAB_AUTH_COOKIE_PATH = getConfig("cookie-path", "GITLAB_AUTH_COOKIE_PATH");
303
- const USE_OAUTH = getConfig("use-oauth", "GITLAB_USE_OAUTH") === "true";
304
- const IS_OLD = getConfig("is-old", "GITLAB_IS_OLD") === "true";
305
- const GITLAB_READ_ONLY_MODE = getConfig("read-only", "GITLAB_READ_ONLY_MODE") === "true";
306
436
  const GITLAB_DENIED_TOOLS_REGEX = (() => {
307
437
  const pattern = getConfig("denied-tools-regex", "GITLAB_DENIED_TOOLS_REGEX");
308
438
  if (!pattern)
@@ -332,64 +462,31 @@ const GITLAB_DENIED_TOOLS_REGEX = (() => {
332
462
  return undefined;
333
463
  }
334
464
  })();
335
- const USE_GITLAB_WIKI = getConfig("use-wiki", "USE_GITLAB_WIKI") === "true";
336
- const USE_MILESTONE = getConfig("use-milestone", "USE_MILESTONE") === "true";
337
- const USE_PIPELINE = getConfig("use-pipeline", "USE_PIPELINE") === "true";
338
- const GITLAB_TOOLSETS_RAW = getConfig("toolsets", "GITLAB_TOOLSETS");
339
- const GITLAB_TOOLS_RAW = getConfig("tools", "GITLAB_TOOLS");
340
- const SSE = getConfig("sse", "SSE") === "true";
341
- const STREAMABLE_HTTP = getConfig("streamable-http", "STREAMABLE_HTTP") === "true";
342
- const REMOTE_AUTHORIZATION = getConfig("remote-auth", "REMOTE_AUTHORIZATION") === "true";
343
- const GITLAB_MCP_OAUTH = getConfig("mcp-oauth", "GITLAB_MCP_OAUTH") === "true";
344
- const MCP_SERVER_URL = getConfig("mcp-server-url", "MCP_SERVER_URL");
345
- const GITLAB_OAUTH_APP_ID = getConfig("oauth-app-id", "GITLAB_OAUTH_APP_ID");
346
- const GITLAB_OAUTH_SCOPES_RAW = getConfig("oauth-scopes", "GITLAB_OAUTH_SCOPES");
347
- const GITLAB_OAUTH_SCOPES = GITLAB_OAUTH_SCOPES_RAW
348
- ? GITLAB_OAUTH_SCOPES_RAW.split(",").map((s) => s.trim()).filter(Boolean)
349
- : undefined;
350
- const ENABLE_DYNAMIC_API_URL = getConfig("enable-dynamic-api-url", "ENABLE_DYNAMIC_API_URL") === "true";
351
- const SESSION_TIMEOUT_SECONDS = Number.parseInt(getConfig("session-timeout", "SESSION_TIMEOUT_SECONDS", "3600"), 10);
352
- const HOST = getConfig("host", "HOST") || "127.0.0.1";
353
- const PORT = Number.parseInt(getConfig("port", "PORT", "3002"), 10);
354
- // Add proxy configuration
355
- const HTTP_PROXY = getConfig("http-proxy", "HTTP_PROXY");
356
- const HTTPS_PROXY = getConfig("https-proxy", "HTTPS_PROXY");
357
- const NO_PROXY = getConfig("no-proxy", "NO_PROXY");
358
- const NODE_TLS_REJECT_UNAUTHORIZED = getConfig("tls-reject-unauthorized", "NODE_TLS_REJECT_UNAUTHORIZED");
359
- const GITLAB_CA_CERT_PATH = getConfig("ca-cert-path", "GITLAB_CA_CERT_PATH");
360
- const GITLAB_POOL_MAX_SIZE = getConfig("pool-max-size", "GITLAB_POOL_MAX_SIZE")
361
- ? Number.parseInt(getConfig("pool-max-size", "GITLAB_POOL_MAX_SIZE"), 10)
362
- : 100;
363
- let sslOptions = undefined;
364
- if (NODE_TLS_REJECT_UNAUTHORIZED === "0") {
365
- sslOptions = { rejectUnauthorized: false };
366
- }
367
- else if (GITLAB_CA_CERT_PATH) {
368
- const ca = fs.readFileSync(GITLAB_CA_CERT_PATH);
369
- sslOptions = { ca };
370
- }
371
- // Configure proxy agents if proxies are set
372
- let httpAgent = undefined;
373
- let httpsAgent = undefined;
374
- if (HTTP_PROXY) {
375
- if (HTTP_PROXY.startsWith("socks")) {
376
- httpAgent = new SocksProxyAgent(HTTP_PROXY);
377
- }
378
- else {
379
- httpAgent = new HttpProxyAgent(HTTP_PROXY);
380
- }
381
- }
382
- if (HTTPS_PROXY) {
383
- if (HTTPS_PROXY.startsWith("socks")) {
384
- httpsAgent = new SocksProxyAgent(HTTPS_PROXY);
465
+ // ---------------------------------------------------------------------------
466
+ // Tool policy: approve / hidden sets
467
+ // ---------------------------------------------------------------------------
468
+ const approveToolSet = new Set((GITLAB_TOOL_POLICY_APPROVE_RAW || "")
469
+ .split(",")
470
+ .map(s => s.trim())
471
+ .filter(Boolean));
472
+ const hiddenToolSet = new Set((GITLAB_TOOL_POLICY_HIDDEN_RAW || "")
473
+ .split(",")
474
+ .map(s => s.trim())
475
+ .filter(Boolean));
476
+ // Validate approve/hidden tool names against known tools at startup
477
+ {
478
+ const knownToolNames = new Set(allTools.map(t => t.name));
479
+ for (const name of approveToolSet) {
480
+ if (!knownToolNames.has(name)) {
481
+ logger.warn({ event: "unknown_approve_tool", name }, `GITLAB_TOOL_POLICY_APPROVE contains unknown tool: "${name}"`);
482
+ }
385
483
  }
386
- else {
387
- httpsAgent = new HttpsProxyAgent(HTTPS_PROXY, sslOptions);
484
+ for (const name of hiddenToolSet) {
485
+ if (!knownToolNames.has(name)) {
486
+ logger.warn({ event: "unknown_hidden_tool", name }, `GITLAB_TOOL_POLICY_HIDDEN contains unknown tool: "${name}"`);
487
+ }
388
488
  }
389
489
  }
390
- httpsAgent = httpsAgent || new HttpsAgent(sslOptions);
391
- httpAgent = httpAgent || new Agent();
392
- // Initialize the client pool for managing multiple GitLab instances
393
490
  const clientPool = new GitLabClientPool({
394
491
  apiUrls: (getConfig("api-url", "GITLAB_API_URL") || "https://gitlab.com")
395
492
  .split(",")
@@ -586,1190 +683,6 @@ const getFetchConfig = () => {
586
683
  agent: agent,
587
684
  };
588
685
  };
589
- const toJSONSchema = (schema) => {
590
- const jsonSchema = zodToJsonSchema(schema, { $refStrategy: "none" });
591
- // Post-process to fix nullable/optional fields that should truly be optional
592
- function fixNullableOptional(obj) {
593
- if (obj && typeof obj === "object") {
594
- // If this object has properties, process them
595
- if (obj.properties) {
596
- const requiredSet = new Set(obj.required || []);
597
- Object.keys(obj.properties).forEach(key => {
598
- const prop = obj.properties[key];
599
- // Handle fields that can be null or omitted
600
- // If a property has type: ["object", "null"] or anyOf with null, it should not be required
601
- if (prop.anyOf && prop.anyOf.some((t) => t.type === "null")) {
602
- requiredSet.delete(key);
603
- }
604
- else if (Array.isArray(prop.type) && prop.type.includes("null")) {
605
- requiredSet.delete(key);
606
- }
607
- // Recursively process nested objects
608
- obj.properties[key] = fixNullableOptional(prop);
609
- });
610
- // Normalize the required array after processing all properties
611
- if (requiredSet.size > 0) {
612
- obj.required = Array.from(requiredSet);
613
- }
614
- else if (Object.prototype.hasOwnProperty.call(obj, "required")) {
615
- delete obj.required;
616
- }
617
- }
618
- // Process anyOf/allOf/oneOf
619
- ["anyOf", "allOf", "oneOf"].forEach(combiner => {
620
- if (obj[combiner]) {
621
- obj[combiner] = obj[combiner].map(fixNullableOptional);
622
- }
623
- });
624
- }
625
- return obj;
626
- }
627
- return fixNullableOptional(jsonSchema);
628
- };
629
- // Define all available tools
630
- const allTools = [
631
- {
632
- name: "merge_merge_request",
633
- description: "Merge a merge request in a GitLab project",
634
- inputSchema: toJSONSchema(MergeMergeRequestSchema),
635
- },
636
- {
637
- name: "approve_merge_request",
638
- description: "Approve a merge request. Requires appropriate permissions.",
639
- inputSchema: toJSONSchema(ApproveMergeRequestSchema),
640
- },
641
- {
642
- name: "unapprove_merge_request",
643
- description: "Unapprove a previously approved merge request. Requires appropriate permissions.",
644
- inputSchema: toJSONSchema(UnapproveMergeRequestSchema),
645
- },
646
- {
647
- name: "get_merge_request_approval_state",
648
- description: "Get merge request approval details including approvers (uses approval_state when available, falls back to approvals endpoint)",
649
- inputSchema: toJSONSchema(GetMergeRequestApprovalStateSchema),
650
- },
651
- {
652
- name: "get_merge_request_conflicts",
653
- description: "Get the conflicts of a merge request in a GitLab project",
654
- inputSchema: toJSONSchema(GetMergeRequestConflictsSchema),
655
- },
656
- {
657
- name: "execute_graphql",
658
- description: "Execute a GitLab GraphQL query",
659
- inputSchema: zodToJsonSchema(ExecuteGraphQLSchema),
660
- },
661
- {
662
- name: "create_or_update_file",
663
- description: "Create or update a single file in a GitLab project",
664
- inputSchema: toJSONSchema(CreateOrUpdateFileSchema),
665
- },
666
- {
667
- name: "search_repositories",
668
- description: "Search for GitLab projects",
669
- inputSchema: toJSONSchema(SearchRepositoriesSchema),
670
- },
671
- {
672
- name: "create_repository",
673
- description: "Create a new GitLab project",
674
- inputSchema: toJSONSchema(CreateRepositorySchema),
675
- },
676
- {
677
- name: "get_file_contents",
678
- description: "Get the contents of a file or directory from a GitLab project",
679
- inputSchema: toJSONSchema(GetFileContentsSchema),
680
- },
681
- {
682
- name: "push_files",
683
- description: "Push multiple files to a GitLab project in a single commit",
684
- inputSchema: toJSONSchema(PushFilesSchema),
685
- },
686
- {
687
- name: "create_issue",
688
- description: "Create a new issue in a GitLab project",
689
- inputSchema: toJSONSchema(CreateIssueSchema),
690
- },
691
- {
692
- name: "create_merge_request",
693
- description: "Create a new merge request in a GitLab project",
694
- inputSchema: toJSONSchema(CreateMergeRequestSchema),
695
- },
696
- {
697
- name: "fork_repository",
698
- description: "Fork a GitLab project to your account or specified namespace",
699
- inputSchema: toJSONSchema(ForkRepositorySchema),
700
- },
701
- {
702
- name: "create_branch",
703
- description: "Create a new branch in a GitLab project",
704
- inputSchema: toJSONSchema(CreateBranchSchema),
705
- },
706
- {
707
- name: "get_merge_request",
708
- description: "Get details of a merge request with compact deployment, commit addition, and approval summaries (Either mergeRequestIid or branchName must be provided)",
709
- inputSchema: toJSONSchema(GetMergeRequestSchema),
710
- },
711
- {
712
- name: "get_merge_request_diffs",
713
- description: "Get the changes/diffs of a merge request (Either mergeRequestIid or branchName must be provided)",
714
- inputSchema: toJSONSchema(GetMergeRequestDiffsSchema),
715
- },
716
- {
717
- name: "list_merge_request_changed_files",
718
- description: "STEP 1 of code review workflow. " +
719
- "Returns ONLY the list of changed file paths in a merge request — WITHOUT diff content. " +
720
- "Call this first to get file paths, then call get_merge_request_file_diff with multiple files in a single batched call (recommended 3-5 files per call). " +
721
- "This avoids loading the entire diff payload at once and reduces API calls. " +
722
- "Supports excluded_file_patterns filtering using regex. " +
723
- "Returns: new_path, old_path, new_file, deleted_file, renamed_file flags for each file. " +
724
- "(Either mergeRequestIid or branchName must be provided)",
725
- inputSchema: toJSONSchema(ListMergeRequestChangedFilesSchema),
726
- },
727
- {
728
- name: "list_merge_request_diffs",
729
- description: "List merge request diffs with pagination support (Either mergeRequestIid or branchName must be provided)",
730
- inputSchema: toJSONSchema(ListMergeRequestDiffsSchema),
731
- },
732
- {
733
- name: "get_merge_request_file_diff",
734
- description: "STEP 2 of code review workflow. " +
735
- "Get diffs for one or more files from a merge request. " +
736
- "Call list_merge_request_changed_files first to get file paths, then pass them as an array to fetch their diffs efficiently. " +
737
- "Batching multiple files (recommended 3-5) is supported and preferred over individual requests. " +
738
- "Returns an array of results - one per requested file path. Files not found are returned with error messages. " +
739
- "(Either mergeRequestIid or branchName must be provided)",
740
- inputSchema: toJSONSchema(GetMergeRequestFileDiffSchema),
741
- },
742
- {
743
- name: "list_merge_request_versions",
744
- description: "List all versions of a merge request",
745
- inputSchema: toJSONSchema(ListMergeRequestVersionsSchema),
746
- },
747
- {
748
- name: "get_merge_request_version",
749
- description: "Get a specific version of a merge request",
750
- inputSchema: toJSONSchema(GetMergeRequestVersionSchema),
751
- },
752
- {
753
- name: "get_branch_diffs",
754
- description: "Get the changes/diffs between two branches or commits in a GitLab project",
755
- inputSchema: toJSONSchema(GetBranchDiffsSchema),
756
- },
757
- {
758
- name: "update_merge_request",
759
- description: "Update a merge request (Either mergeRequestIid or branchName must be provided)",
760
- inputSchema: toJSONSchema(UpdateMergeRequestSchema),
761
- },
762
- {
763
- name: "create_note",
764
- description: "Create a new note (comment) to an issue or merge request",
765
- inputSchema: toJSONSchema(CreateNoteSchema),
766
- },
767
- {
768
- name: "create_merge_request_thread",
769
- description: "Create a new thread on a merge request",
770
- inputSchema: toJSONSchema(CreateMergeRequestThreadSchema),
771
- },
772
- {
773
- name: "resolve_merge_request_thread",
774
- description: "Resolve a thread on a merge request",
775
- inputSchema: toJSONSchema(ResolveMergeRequestThreadSchema),
776
- },
777
- {
778
- name: "mr_discussions",
779
- description: "List discussion items for a merge request",
780
- inputSchema: toJSONSchema(ListMergeRequestDiscussionsSchema),
781
- },
782
- {
783
- name: "delete_merge_request_discussion_note",
784
- description: "Delete a discussion note on a merge request",
785
- inputSchema: toJSONSchema(DeleteMergeRequestDiscussionNoteSchema),
786
- },
787
- {
788
- name: "update_merge_request_discussion_note",
789
- description: "Update a discussion note on a merge request",
790
- inputSchema: toJSONSchema(UpdateMergeRequestDiscussionNoteSchema),
791
- },
792
- {
793
- name: "create_merge_request_discussion_note",
794
- description: "Add a new discussion note to an existing merge request thread",
795
- inputSchema: toJSONSchema(CreateMergeRequestDiscussionNoteSchema),
796
- },
797
- {
798
- name: "create_merge_request_note",
799
- description: "Add a new note to a merge request",
800
- inputSchema: toJSONSchema(CreateMergeRequestNoteSchema),
801
- },
802
- {
803
- name: "delete_merge_request_note",
804
- description: "Delete an existing merge request note",
805
- inputSchema: toJSONSchema(DeleteMergeRequestNoteSchema),
806
- },
807
- {
808
- name: "get_merge_request_note",
809
- description: "Get a specific note for a merge request",
810
- inputSchema: toJSONSchema(GetMergeRequestNoteSchema),
811
- },
812
- {
813
- name: "get_merge_request_notes",
814
- description: "List notes for a merge request",
815
- inputSchema: toJSONSchema(GetMergeRequestNotesSchema),
816
- },
817
- {
818
- name: "update_merge_request_note",
819
- description: "Modify an existing merge request note",
820
- inputSchema: toJSONSchema(UpdateMergeRequestNoteSchema),
821
- },
822
- {
823
- name: "get_draft_note",
824
- description: "Get a single draft note from a merge request",
825
- inputSchema: toJSONSchema(GetDraftNoteSchema),
826
- },
827
- {
828
- name: "list_draft_notes",
829
- description: "List draft notes for a merge request",
830
- inputSchema: toJSONSchema(ListDraftNotesSchema),
831
- },
832
- {
833
- name: "create_draft_note",
834
- description: "Create a draft note for a merge request",
835
- inputSchema: toJSONSchema(CreateDraftNoteSchema),
836
- },
837
- {
838
- name: "update_draft_note",
839
- description: "Update an existing draft note",
840
- inputSchema: toJSONSchema(UpdateDraftNoteSchema),
841
- },
842
- {
843
- name: "delete_draft_note",
844
- description: "Delete a draft note",
845
- inputSchema: toJSONSchema(DeleteDraftNoteSchema),
846
- },
847
- {
848
- name: "publish_draft_note",
849
- description: "Publish a single draft note",
850
- inputSchema: toJSONSchema(PublishDraftNoteSchema),
851
- },
852
- {
853
- name: "bulk_publish_draft_notes",
854
- description: "Publish all draft notes for a merge request",
855
- inputSchema: toJSONSchema(BulkPublishDraftNotesSchema),
856
- },
857
- {
858
- name: "update_issue_note",
859
- description: "Modify an existing issue thread note",
860
- inputSchema: toJSONSchema(UpdateIssueNoteSchema),
861
- },
862
- {
863
- name: "create_issue_note",
864
- description: "Add a note to an issue. Creates a top-level comment, or replies to a discussion thread if discussion_id is provided",
865
- inputSchema: toJSONSchema(CreateIssueNoteSchema),
866
- },
867
- {
868
- name: "list_issues",
869
- description: "List issues (default: created by current user only; use scope='all' for all accessible issues)",
870
- inputSchema: toJSONSchema(ListIssuesSchema),
871
- },
872
- {
873
- name: "my_issues",
874
- description: "List issues assigned to the authenticated user (defaults to open issues)",
875
- inputSchema: toJSONSchema(MyIssuesSchema),
876
- },
877
- {
878
- name: "get_issue",
879
- description: "Get details of a specific issue in a GitLab project",
880
- inputSchema: toJSONSchema(GetIssueSchema),
881
- },
882
- {
883
- name: "update_issue",
884
- description: "Update an issue in a GitLab project",
885
- inputSchema: toJSONSchema(UpdateIssueSchema),
886
- },
887
- {
888
- name: "delete_issue",
889
- description: "Delete an issue from a GitLab project",
890
- inputSchema: toJSONSchema(DeleteIssueSchema),
891
- },
892
- {
893
- name: "list_issue_links",
894
- description: "List all issue links for a specific issue",
895
- inputSchema: toJSONSchema(ListIssueLinksSchema),
896
- },
897
- {
898
- name: "list_issue_discussions",
899
- description: "List discussions for an issue in a GitLab project",
900
- inputSchema: toJSONSchema(ListIssueDiscussionsSchema),
901
- },
902
- {
903
- name: "get_issue_link",
904
- description: "Get a specific issue link",
905
- inputSchema: toJSONSchema(GetIssueLinkSchema),
906
- },
907
- {
908
- name: "create_issue_link",
909
- description: "Create an issue link between two issues",
910
- inputSchema: toJSONSchema(CreateIssueLinkSchema),
911
- },
912
- {
913
- name: "delete_issue_link",
914
- description: "Delete an issue link",
915
- inputSchema: toJSONSchema(DeleteIssueLinkSchema),
916
- },
917
- {
918
- name: "list_namespaces",
919
- description: "List all namespaces available to the current user",
920
- inputSchema: toJSONSchema(ListNamespacesSchema),
921
- },
922
- {
923
- name: "get_namespace",
924
- description: "Get details of a namespace by ID or path",
925
- inputSchema: toJSONSchema(GetNamespaceSchema),
926
- },
927
- {
928
- name: "verify_namespace",
929
- description: "Verify if a namespace path exists",
930
- inputSchema: toJSONSchema(VerifyNamespaceSchema),
931
- },
932
- {
933
- name: "get_project",
934
- description: "Get details of a specific project",
935
- inputSchema: toJSONSchema(GetProjectSchema),
936
- },
937
- {
938
- name: "list_projects",
939
- description: "List projects accessible by the current user",
940
- inputSchema: toJSONSchema(ListProjectsSchema),
941
- },
942
- {
943
- name: "list_project_members",
944
- description: "List members of a GitLab project",
945
- inputSchema: toJSONSchema(ListProjectMembersSchema),
946
- },
947
- {
948
- name: "list_labels",
949
- description: "List labels for a project",
950
- inputSchema: toJSONSchema(ListLabelsSchema),
951
- },
952
- {
953
- name: "get_label",
954
- description: "Get a single label from a project",
955
- inputSchema: toJSONSchema(GetLabelSchema),
956
- },
957
- {
958
- name: "create_label",
959
- description: "Create a new label in a project",
960
- inputSchema: toJSONSchema(CreateLabelSchema),
961
- },
962
- {
963
- name: "update_label",
964
- description: "Update an existing label in a project",
965
- inputSchema: toJSONSchema(UpdateLabelSchema),
966
- },
967
- {
968
- name: "delete_label",
969
- description: "Delete a label from a project",
970
- inputSchema: toJSONSchema(DeleteLabelSchema),
971
- },
972
- {
973
- name: "list_group_projects",
974
- description: "List projects in a GitLab group with filtering options",
975
- inputSchema: toJSONSchema(ListGroupProjectsSchema),
976
- },
977
- {
978
- name: "list_wiki_pages",
979
- description: "List wiki pages in a GitLab project",
980
- inputSchema: toJSONSchema(ListWikiPagesSchema),
981
- },
982
- {
983
- name: "get_wiki_page",
984
- description: "Get details of a specific wiki page",
985
- inputSchema: toJSONSchema(GetWikiPageSchema),
986
- },
987
- {
988
- name: "create_wiki_page",
989
- description: "Create a new wiki page in a GitLab project",
990
- inputSchema: toJSONSchema(CreateWikiPageSchema),
991
- },
992
- {
993
- name: "update_wiki_page",
994
- description: "Update an existing wiki page in a GitLab project",
995
- inputSchema: toJSONSchema(UpdateWikiPageSchema),
996
- },
997
- {
998
- name: "delete_wiki_page",
999
- description: "Delete a wiki page from a GitLab project",
1000
- inputSchema: toJSONSchema(DeleteWikiPageSchema),
1001
- },
1002
- {
1003
- name: "list_group_wiki_pages",
1004
- description: "List wiki pages in a GitLab group",
1005
- inputSchema: toJSONSchema(ListGroupWikiPagesSchema),
1006
- },
1007
- {
1008
- name: "get_group_wiki_page",
1009
- description: "Get details of a specific group wiki page",
1010
- inputSchema: toJSONSchema(GetGroupWikiPageSchema),
1011
- },
1012
- {
1013
- name: "create_group_wiki_page",
1014
- description: "Create a new wiki page in a GitLab group",
1015
- inputSchema: toJSONSchema(CreateGroupWikiPageSchema),
1016
- },
1017
- {
1018
- name: "update_group_wiki_page",
1019
- description: "Update an existing wiki page in a GitLab group",
1020
- inputSchema: toJSONSchema(UpdateGroupWikiPageSchema),
1021
- },
1022
- {
1023
- name: "delete_group_wiki_page",
1024
- description: "Delete a wiki page from a GitLab group",
1025
- inputSchema: toJSONSchema(DeleteGroupWikiPageSchema),
1026
- },
1027
- {
1028
- name: "get_repository_tree",
1029
- description: "Get the repository tree for a GitLab project (list files and directories)",
1030
- inputSchema: toJSONSchema(GetRepositoryTreeSchema),
1031
- },
1032
- {
1033
- name: "list_pipelines",
1034
- description: "List pipelines in a GitLab project with filtering options",
1035
- inputSchema: toJSONSchema(ListPipelinesSchema),
1036
- },
1037
- {
1038
- name: "get_pipeline",
1039
- description: "Get details of a specific pipeline in a GitLab project",
1040
- inputSchema: toJSONSchema(GetPipelineSchema),
1041
- },
1042
- {
1043
- name: "list_deployments",
1044
- description: "List deployments in a GitLab project with filtering options",
1045
- inputSchema: toJSONSchema(ListDeploymentsSchema),
1046
- },
1047
- {
1048
- name: "get_deployment",
1049
- description: "Get details of a specific deployment in a GitLab project",
1050
- inputSchema: toJSONSchema(GetDeploymentSchema),
1051
- },
1052
- {
1053
- name: "list_environments",
1054
- description: "List environments in a GitLab project",
1055
- inputSchema: toJSONSchema(ListEnvironmentsSchema),
1056
- },
1057
- {
1058
- name: "get_environment",
1059
- description: "Get details of a specific environment in a GitLab project",
1060
- inputSchema: toJSONSchema(GetEnvironmentSchema),
1061
- },
1062
- {
1063
- name: "list_pipeline_jobs",
1064
- description: "List all jobs in a specific pipeline",
1065
- inputSchema: toJSONSchema(ListPipelineJobsSchema),
1066
- },
1067
- {
1068
- name: "list_pipeline_trigger_jobs",
1069
- description: "List all trigger jobs (bridges) in a specific pipeline that trigger downstream pipelines",
1070
- inputSchema: toJSONSchema(ListPipelineTriggerJobsSchema),
1071
- },
1072
- {
1073
- name: "get_pipeline_job",
1074
- description: "Get details of a GitLab pipeline job number",
1075
- inputSchema: toJSONSchema(GetPipelineJobOutputSchema),
1076
- },
1077
- {
1078
- name: "get_pipeline_job_output",
1079
- description: "Get the output/trace of a GitLab pipeline job with optional pagination to limit context window usage",
1080
- inputSchema: toJSONSchema(GetPipelineJobOutputSchema),
1081
- },
1082
- {
1083
- name: "create_pipeline",
1084
- description: "Create a new pipeline for a branch or tag",
1085
- inputSchema: toJSONSchema(CreatePipelineSchema),
1086
- },
1087
- {
1088
- name: "retry_pipeline",
1089
- description: "Retry a failed or canceled pipeline",
1090
- inputSchema: toJSONSchema(RetryPipelineSchema),
1091
- },
1092
- {
1093
- name: "cancel_pipeline",
1094
- description: "Cancel a running pipeline",
1095
- inputSchema: toJSONSchema(CancelPipelineSchema),
1096
- },
1097
- {
1098
- name: "play_pipeline_job",
1099
- description: "Run a manual pipeline job",
1100
- inputSchema: toJSONSchema(PlayPipelineJobSchema),
1101
- },
1102
- {
1103
- name: "retry_pipeline_job",
1104
- description: "Retry a failed or canceled pipeline job",
1105
- inputSchema: toJSONSchema(RetryPipelineJobSchema),
1106
- },
1107
- {
1108
- name: "cancel_pipeline_job",
1109
- description: "Cancel a running pipeline job",
1110
- inputSchema: toJSONSchema(CancelPipelineJobSchema),
1111
- },
1112
- {
1113
- name: "list_job_artifacts",
1114
- description: "List artifact files in a job's artifacts archive. Returns file names, paths, types, and sizes.",
1115
- inputSchema: toJSONSchema(ListJobArtifactsSchema),
1116
- },
1117
- {
1118
- name: "download_job_artifacts",
1119
- description: "Download the entire artifact archive (zip) for a job to a local path. Returns the saved file path.",
1120
- inputSchema: toJSONSchema(DownloadJobArtifactsSchema),
1121
- },
1122
- {
1123
- name: "get_job_artifact_file",
1124
- description: "Get the content of a single file from a job's artifacts by its path within the archive",
1125
- inputSchema: toJSONSchema(GetJobArtifactFileSchema),
1126
- },
1127
- {
1128
- name: "list_merge_requests",
1129
- description: "List merge requests. Without project_id, lists MRs assigned to the authenticated user by default (use scope='all' for all accessible MRs). With project_id, lists MRs for that specific project.",
1130
- inputSchema: toJSONSchema(ListMergeRequestsSchema),
1131
- },
1132
- {
1133
- name: "list_milestones",
1134
- description: "List milestones in a GitLab project with filtering options",
1135
- inputSchema: toJSONSchema(ListProjectMilestonesSchema),
1136
- },
1137
- {
1138
- name: "get_milestone",
1139
- description: "Get details of a specific milestone",
1140
- inputSchema: toJSONSchema(GetProjectMilestoneSchema),
1141
- },
1142
- {
1143
- name: "create_milestone",
1144
- description: "Create a new milestone in a GitLab project",
1145
- inputSchema: toJSONSchema(CreateProjectMilestoneSchema),
1146
- },
1147
- {
1148
- name: "edit_milestone",
1149
- description: "Edit an existing milestone in a GitLab project",
1150
- inputSchema: toJSONSchema(EditProjectMilestoneSchema),
1151
- },
1152
- {
1153
- name: "delete_milestone",
1154
- description: "Delete a milestone from a GitLab project",
1155
- inputSchema: toJSONSchema(DeleteProjectMilestoneSchema),
1156
- },
1157
- {
1158
- name: "get_milestone_issue",
1159
- description: "Get issues associated with a specific milestone",
1160
- inputSchema: toJSONSchema(GetMilestoneIssuesSchema),
1161
- },
1162
- {
1163
- name: "get_milestone_merge_requests",
1164
- description: "Get merge requests associated with a specific milestone",
1165
- inputSchema: toJSONSchema(GetMilestoneMergeRequestsSchema),
1166
- },
1167
- {
1168
- name: "promote_milestone",
1169
- description: "Promote a milestone to the next stage",
1170
- inputSchema: toJSONSchema(PromoteProjectMilestoneSchema),
1171
- },
1172
- {
1173
- name: "get_milestone_burndown_events",
1174
- description: "Get burndown events for a specific milestone",
1175
- inputSchema: toJSONSchema(GetMilestoneBurndownEventsSchema),
1176
- },
1177
- {
1178
- name: "get_users",
1179
- description: "Get GitLab user details by usernames",
1180
- inputSchema: toJSONSchema(GetUsersSchema),
1181
- },
1182
- {
1183
- name: "list_commits",
1184
- description: "List repository commits with filtering options",
1185
- inputSchema: toJSONSchema(ListCommitsSchema),
1186
- },
1187
- {
1188
- name: "get_commit",
1189
- description: "Get details of a specific commit",
1190
- inputSchema: toJSONSchema(GetCommitSchema),
1191
- },
1192
- {
1193
- name: "get_commit_diff",
1194
- description: "Get changes/diffs of a specific commit",
1195
- inputSchema: toJSONSchema(GetCommitDiffSchema),
1196
- },
1197
- {
1198
- name: "list_group_iterations",
1199
- description: "List group iterations with filtering options",
1200
- inputSchema: toJSONSchema(ListGroupIterationsSchema),
1201
- },
1202
- {
1203
- name: "upload_markdown",
1204
- description: "Upload a file to a GitLab project for use in markdown content",
1205
- inputSchema: toJSONSchema(MarkdownUploadSchema),
1206
- },
1207
- {
1208
- name: "download_attachment",
1209
- description: "Download an uploaded file from a GitLab project by secret and filename. Image files (png, jpg, gif, webp, svg, bmp, ico) are returned inline as base64 image content so the AI can view them directly. Non-image files are saved to disk. Use local_path to force saving image files to disk instead.",
1210
- inputSchema: toJSONSchema(DownloadAttachmentSchema),
1211
- },
1212
- {
1213
- name: "list_events",
1214
- description: "List all events for the currently authenticated user. Note: before/after parameters accept date format YYYY-MM-DD only",
1215
- inputSchema: toJSONSchema(ListEventsSchema),
1216
- },
1217
- {
1218
- name: "get_project_events",
1219
- description: "List all visible events for a specified project. Note: before/after parameters accept date format YYYY-MM-DD only",
1220
- inputSchema: toJSONSchema(GetProjectEventsSchema),
1221
- },
1222
- {
1223
- name: "list_releases",
1224
- description: "List all releases for a project",
1225
- inputSchema: toJSONSchema(ListReleasesSchema),
1226
- },
1227
- {
1228
- name: "get_release",
1229
- description: "Get a release by tag name",
1230
- inputSchema: toJSONSchema(GetReleaseSchema),
1231
- },
1232
- {
1233
- name: "create_release",
1234
- description: "Create a new release in a GitLab project",
1235
- inputSchema: toJSONSchema(CreateReleaseSchema),
1236
- },
1237
- {
1238
- name: "update_release",
1239
- description: "Update an existing release in a GitLab project",
1240
- inputSchema: toJSONSchema(UpdateReleaseSchema),
1241
- },
1242
- {
1243
- name: "delete_release",
1244
- description: "Delete a release from a GitLab project (does not delete the associated tag)",
1245
- inputSchema: toJSONSchema(DeleteReleaseSchema),
1246
- },
1247
- {
1248
- name: "create_release_evidence",
1249
- description: "Create release evidence for an existing release (GitLab Premium/Ultimate only)",
1250
- inputSchema: toJSONSchema(CreateReleaseEvidenceSchema),
1251
- },
1252
- {
1253
- name: "download_release_asset",
1254
- description: "Download a release asset file by direct asset path",
1255
- inputSchema: toJSONSchema(DownloadReleaseAssetSchema),
1256
- },
1257
- // --- Work item tools (GraphQL-based) ---
1258
- {
1259
- name: "get_work_item",
1260
- description: "Get a single work item with full details including status, hierarchy (parent/children), type, labels, assignees, and all widgets.",
1261
- inputSchema: toJSONSchema(GetWorkItemSchema),
1262
- },
1263
- {
1264
- name: "list_work_items",
1265
- description: "List work items in a project with filters (type, state, search, assignees, labels). Returns items with status and hierarchy info.",
1266
- inputSchema: toJSONSchema(ListWorkItemsSchema),
1267
- },
1268
- {
1269
- name: "create_work_item",
1270
- description: "Create a new work item (issue, task, incident, test_case, epic, key_result, objective, requirement, ticket). Supports setting title, description, labels, assignees, weight, parent, health status, start/due dates, milestone, and confidentiality.",
1271
- inputSchema: toJSONSchema(CreateWorkItemSchema),
1272
- },
1273
- {
1274
- name: "update_work_item",
1275
- description: "Update a work item. Can modify title, description, labels, assignees, weight, state, status, parent hierarchy, children, health status, start/due dates, milestone, confidentiality, linked items, and custom fields.",
1276
- inputSchema: toJSONSchema(UpdateWorkItemSchema),
1277
- },
1278
- {
1279
- name: "convert_work_item_type",
1280
- description: "Convert a work item to a different type (e.g. issue to task, task to incident).",
1281
- inputSchema: toJSONSchema(ConvertWorkItemTypeSchema),
1282
- },
1283
- {
1284
- name: "list_work_item_statuses",
1285
- description: "List available statuses for a work item type in a project. Requires GitLab Premium/Ultimate with configurable statuses.",
1286
- inputSchema: toJSONSchema(ListWorkItemStatusesSchema),
1287
- },
1288
- {
1289
- name: "list_custom_field_definitions",
1290
- description: "List available custom field definitions for a work item type in a project. Returns field names, types, and IDs needed for setting custom fields via update_work_item.",
1291
- inputSchema: toJSONSchema(ListCustomFieldDefinitionsSchema),
1292
- },
1293
- {
1294
- name: "move_work_item",
1295
- description: "Move a work item (issue, task, etc.) to a different project. Uses GitLab GraphQL issueMove mutation.",
1296
- inputSchema: toJSONSchema(MoveWorkItemSchema),
1297
- },
1298
- {
1299
- name: "list_work_item_notes",
1300
- description: "List notes and discussions on a work item. Returns threaded discussions with author, body, timestamps, and system/internal flags.",
1301
- inputSchema: toJSONSchema(ListWorkItemNotesSchema),
1302
- },
1303
- {
1304
- name: "create_work_item_note",
1305
- description: "Add a note/comment to a work item. Supports Markdown, internal notes, and threaded replies.",
1306
- inputSchema: toJSONSchema(CreateWorkItemNoteSchema),
1307
- },
1308
- // --- Incident timeline event tools ---
1309
- {
1310
- name: "get_timeline_events",
1311
- description: "List timeline events for an incident. Returns chronological events with notes, timestamps, and tags (Start time, End time, Impact detected, etc.).",
1312
- inputSchema: toJSONSchema(GetTimelineEventsSchema),
1313
- },
1314
- {
1315
- name: "create_timeline_event",
1316
- description: "Create a timeline event on an incident. Supports tags: 'Start time', 'End time', 'Impact detected', 'Response initiated', 'Impact mitigated', 'Cause identified'.",
1317
- inputSchema: toJSONSchema(CreateTimelineEventSchema),
1318
- },
1319
- {
1320
- name: "list_webhooks",
1321
- description: "List all configured webhooks for a GitLab project or group. Provide either project_id or group_id.",
1322
- inputSchema: toJSONSchema(ListWebhooksSchema),
1323
- },
1324
- {
1325
- name: "list_webhook_events",
1326
- description: "List recent webhook events (past 7 days) for a project or group webhook. Use summary mode for overview, then get_webhook_event for full details.",
1327
- inputSchema: toJSONSchema(ListWebhookEventsSchema),
1328
- },
1329
- {
1330
- name: "get_webhook_event",
1331
- description: "Get full details of a specific webhook event by ID, including request/response payloads. Searches up to 500 most recent events.",
1332
- inputSchema: toJSONSchema(GetWebhookEventSchema),
1333
- },
1334
- {
1335
- name: "search_code",
1336
- description: "Search for code across all projects on the GitLab instance (requires advanced search or exact code search to be enabled). If exact code search (Zoekt) is enabled, the search query supports rich syntax including file:, lang:, sym: filters.",
1337
- inputSchema: toJSONSchema(SearchCodeSchema),
1338
- },
1339
- {
1340
- name: "search_project_code",
1341
- description: "Search for code within a specific GitLab project (requires advanced search or exact code search to be enabled). If exact code search (Zoekt) is enabled, the search query supports rich syntax including file:, lang:, sym: filters.",
1342
- inputSchema: toJSONSchema(SearchProjectCodeSchema),
1343
- },
1344
- {
1345
- name: "search_group_code",
1346
- description: "Search for code within a specific GitLab group (requires advanced search or exact code search to be enabled). If exact code search (Zoekt) is enabled, the search query supports rich syntax including file:, lang:, sym: filters.",
1347
- inputSchema: toJSONSchema(SearchGroupCodeSchema),
1348
- },
1349
- ];
1350
- // Define which tools are read-only
1351
- const readOnlyTools = new Set([
1352
- "search_repositories",
1353
- "search_code",
1354
- "search_project_code",
1355
- "search_group_code",
1356
- "execute_graphql",
1357
- "get_file_contents",
1358
- "get_merge_request",
1359
- "get_merge_request_diffs",
1360
- "list_merge_request_changed_files",
1361
- "list_merge_request_diffs",
1362
- "get_merge_request_file_diff",
1363
- "list_merge_request_versions",
1364
- "get_merge_request_version",
1365
- "get_branch_diffs",
1366
- "get_merge_request_note",
1367
- "get_merge_request_notes",
1368
- "get_draft_note",
1369
- "list_draft_notes",
1370
- "mr_discussions",
1371
- "list_issues",
1372
- "my_issues",
1373
- "list_merge_requests",
1374
- "get_issue",
1375
- "list_issue_links",
1376
- "list_issue_discussions",
1377
- "get_issue_link",
1378
- "list_namespaces",
1379
- "get_namespace",
1380
- "verify_namespace",
1381
- "get_project",
1382
- "list_projects",
1383
- "list_project_members",
1384
- "get_pipeline",
1385
- "list_pipelines",
1386
- "list_deployments",
1387
- "get_deployment",
1388
- "list_environments",
1389
- "get_environment",
1390
- "list_pipeline_jobs",
1391
- "list_pipeline_trigger_jobs",
1392
- "get_pipeline_job",
1393
- "get_pipeline_job_output",
1394
- "list_job_artifacts",
1395
- "download_job_artifacts",
1396
- "get_job_artifact_file",
1397
- "list_labels",
1398
- "get_label",
1399
- "list_group_projects",
1400
- "get_repository_tree",
1401
- "list_milestones",
1402
- "get_milestone",
1403
- "get_milestone_issue",
1404
- "get_milestone_merge_requests",
1405
- "get_milestone_burndown_events",
1406
- "list_wiki_pages",
1407
- "get_wiki_page",
1408
- "list_group_wiki_pages",
1409
- "get_group_wiki_page",
1410
- "get_users",
1411
- "list_commits",
1412
- "get_commit",
1413
- "get_commit_diff",
1414
- "list_group_iterations",
1415
- "get_group_iteration",
1416
- "download_attachment",
1417
- "list_events",
1418
- "get_project_events",
1419
- "list_releases",
1420
- "get_release",
1421
- "download_release_asset",
1422
- "get_merge_request_approval_state",
1423
- "get_work_item",
1424
- "list_work_items",
1425
- "list_work_item_statuses",
1426
- "list_custom_field_definitions",
1427
- "list_work_item_notes",
1428
- "get_timeline_events",
1429
- "get_merge_request_conflicts",
1430
- "list_webhooks",
1431
- "list_webhook_events",
1432
- "get_webhook_event",
1433
- ]);
1434
- // Define which tools are related to wiki and can be toggled by USE_GITLAB_WIKI
1435
- const wikiToolNames = new Set([
1436
- "list_wiki_pages",
1437
- "get_wiki_page",
1438
- "create_wiki_page",
1439
- "update_wiki_page",
1440
- "delete_wiki_page",
1441
- "list_group_wiki_pages",
1442
- "get_group_wiki_page",
1443
- "create_group_wiki_page",
1444
- "update_group_wiki_page",
1445
- "delete_group_wiki_page",
1446
- "upload_wiki_attachment",
1447
- ]);
1448
- // Define which tools are related to milestones and can be toggled by USE_MILESTONE
1449
- const milestoneToolNames = new Set([
1450
- "list_milestones",
1451
- "get_milestone",
1452
- "create_milestone",
1453
- "edit_milestone",
1454
- "delete_milestone",
1455
- "get_milestone_issue",
1456
- "get_milestone_merge_requests",
1457
- "promote_milestone",
1458
- "get_milestone_burndown_events",
1459
- ]);
1460
- // Define which tools are related to pipelines and can be toggled by USE_PIPELINE
1461
- const pipelineToolNames = new Set([
1462
- "list_pipelines",
1463
- "get_pipeline",
1464
- "list_deployments",
1465
- "get_deployment",
1466
- "list_environments",
1467
- "get_environment",
1468
- "list_pipeline_jobs",
1469
- "list_pipeline_trigger_jobs",
1470
- "get_pipeline_job",
1471
- "get_pipeline_job_output",
1472
- "create_pipeline",
1473
- "retry_pipeline",
1474
- "cancel_pipeline",
1475
- "play_pipeline_job",
1476
- "retry_pipeline_job",
1477
- "cancel_pipeline_job",
1478
- "list_job_artifacts",
1479
- "download_job_artifacts",
1480
- "get_job_artifact_file",
1481
- ]);
1482
- const TOOLSET_DEFINITIONS = [
1483
- {
1484
- id: "merge_requests",
1485
- isDefault: true,
1486
- tools: new Set([
1487
- "merge_merge_request",
1488
- "approve_merge_request",
1489
- "unapprove_merge_request",
1490
- "get_merge_request_approval_state",
1491
- "get_merge_request_conflicts",
1492
- "get_merge_request",
1493
- "get_merge_request_diffs",
1494
- "list_merge_request_changed_files",
1495
- "list_merge_request_diffs",
1496
- "get_merge_request_file_diff",
1497
- "list_merge_request_versions",
1498
- "get_merge_request_version",
1499
- "update_merge_request",
1500
- "create_merge_request",
1501
- "list_merge_requests",
1502
- "get_branch_diffs",
1503
- "mr_discussions",
1504
- "create_merge_request_note",
1505
- "update_merge_request_note",
1506
- "delete_merge_request_note",
1507
- "get_merge_request_note",
1508
- "get_merge_request_notes",
1509
- "delete_merge_request_discussion_note",
1510
- "update_merge_request_discussion_note",
1511
- "create_merge_request_discussion_note",
1512
- "get_draft_note",
1513
- "list_draft_notes",
1514
- "create_draft_note",
1515
- "update_draft_note",
1516
- "delete_draft_note",
1517
- "publish_draft_note",
1518
- "bulk_publish_draft_notes",
1519
- "create_merge_request_thread",
1520
- "resolve_merge_request_thread",
1521
- ]),
1522
- },
1523
- {
1524
- id: "issues",
1525
- isDefault: true,
1526
- tools: new Set([
1527
- "create_issue",
1528
- "list_issues",
1529
- "my_issues",
1530
- "get_issue",
1531
- "update_issue",
1532
- "delete_issue",
1533
- "create_issue_note",
1534
- "update_issue_note",
1535
- "list_issue_links",
1536
- "list_issue_discussions",
1537
- "get_issue_link",
1538
- "create_issue_link",
1539
- "delete_issue_link",
1540
- "create_note",
1541
- ]),
1542
- },
1543
- {
1544
- id: "repositories",
1545
- isDefault: true,
1546
- tools: new Set([
1547
- "search_repositories",
1548
- "create_repository",
1549
- "get_file_contents",
1550
- "push_files",
1551
- "create_or_update_file",
1552
- "fork_repository",
1553
- "get_repository_tree",
1554
- ]),
1555
- },
1556
- {
1557
- id: "branches",
1558
- isDefault: true,
1559
- tools: new Set([
1560
- "create_branch",
1561
- "list_commits",
1562
- "get_commit",
1563
- "get_commit_diff",
1564
- ]),
1565
- },
1566
- {
1567
- id: "projects",
1568
- isDefault: true,
1569
- tools: new Set([
1570
- "get_project",
1571
- "list_projects",
1572
- "list_project_members",
1573
- "list_namespaces",
1574
- "get_namespace",
1575
- "verify_namespace",
1576
- "list_group_projects",
1577
- "list_group_iterations",
1578
- ]),
1579
- },
1580
- {
1581
- id: "labels",
1582
- isDefault: true,
1583
- tools: new Set([
1584
- "list_labels",
1585
- "get_label",
1586
- "create_label",
1587
- "update_label",
1588
- "delete_label",
1589
- ]),
1590
- },
1591
- {
1592
- id: "pipelines",
1593
- isDefault: true,
1594
- tools: new Set([
1595
- "list_pipelines",
1596
- "get_pipeline",
1597
- "list_deployments",
1598
- "get_deployment",
1599
- "list_environments",
1600
- "get_environment",
1601
- "list_pipeline_jobs",
1602
- "list_pipeline_trigger_jobs",
1603
- "get_pipeline_job",
1604
- "get_pipeline_job_output",
1605
- "create_pipeline",
1606
- "retry_pipeline",
1607
- "cancel_pipeline",
1608
- "play_pipeline_job",
1609
- "retry_pipeline_job",
1610
- "cancel_pipeline_job",
1611
- "list_job_artifacts",
1612
- "download_job_artifacts",
1613
- "get_job_artifact_file",
1614
- ]),
1615
- },
1616
- {
1617
- id: "milestones",
1618
- isDefault: true,
1619
- tools: new Set([
1620
- "list_milestones",
1621
- "get_milestone",
1622
- "create_milestone",
1623
- "edit_milestone",
1624
- "delete_milestone",
1625
- "get_milestone_issue",
1626
- "get_milestone_merge_requests",
1627
- "promote_milestone",
1628
- "get_milestone_burndown_events",
1629
- ]),
1630
- },
1631
- {
1632
- id: "wiki",
1633
- isDefault: true,
1634
- tools: new Set([
1635
- "list_wiki_pages",
1636
- "get_wiki_page",
1637
- "create_wiki_page",
1638
- "update_wiki_page",
1639
- "delete_wiki_page",
1640
- "list_group_wiki_pages",
1641
- "get_group_wiki_page",
1642
- "create_group_wiki_page",
1643
- "update_group_wiki_page",
1644
- "delete_group_wiki_page",
1645
- ]),
1646
- },
1647
- {
1648
- id: "releases",
1649
- isDefault: true,
1650
- tools: new Set([
1651
- "list_releases",
1652
- "get_release",
1653
- "create_release",
1654
- "update_release",
1655
- "delete_release",
1656
- "create_release_evidence",
1657
- "download_release_asset",
1658
- ]),
1659
- },
1660
- {
1661
- id: "users",
1662
- isDefault: true,
1663
- tools: new Set([
1664
- "get_users",
1665
- "list_events",
1666
- "get_project_events",
1667
- "upload_markdown",
1668
- "download_attachment",
1669
- ]),
1670
- },
1671
- {
1672
- id: "workitems",
1673
- isDefault: false,
1674
- tools: new Set([
1675
- "get_work_item",
1676
- "list_work_items",
1677
- "create_work_item",
1678
- "update_work_item",
1679
- "convert_work_item_type",
1680
- "list_work_item_statuses",
1681
- "list_custom_field_definitions",
1682
- "move_work_item",
1683
- "list_work_item_notes",
1684
- "create_work_item_note",
1685
- "get_timeline_events",
1686
- "create_timeline_event",
1687
- ]),
1688
- },
1689
- {
1690
- id: "webhooks",
1691
- isDefault: false,
1692
- tools: new Set([
1693
- "list_webhooks",
1694
- "list_webhook_events",
1695
- "get_webhook_event",
1696
- ]),
1697
- },
1698
- {
1699
- id: "search",
1700
- isDefault: false,
1701
- tools: new Set(["search_code", "search_project_code", "search_group_code"]),
1702
- },
1703
- ];
1704
- // Derived lookup: tool name → toolset ID
1705
- const TOOLSET_BY_TOOL_NAME = new Map();
1706
- for (const def of TOOLSET_DEFINITIONS) {
1707
- for (const tool of def.tools) {
1708
- if (TOOLSET_BY_TOOL_NAME.has(tool)) {
1709
- logger.warn(`Tool "${tool}" is defined in multiple toolsets: "${TOOLSET_BY_TOOL_NAME.get(tool)}" and "${def.id}"`);
1710
- }
1711
- TOOLSET_BY_TOOL_NAME.set(tool, def.id);
1712
- }
1713
- }
1714
- const DEFAULT_TOOLSET_IDS = new Set(TOOLSET_DEFINITIONS.filter(d => d.isDefault).map(d => d.id));
1715
- const ALL_TOOLSET_IDS = new Set(TOOLSET_DEFINITIONS.map(d => d.id));
1716
- function parseEnabledToolsets(raw) {
1717
- if (!raw || raw.trim() === "") {
1718
- return DEFAULT_TOOLSET_IDS;
1719
- }
1720
- const trimmed = raw.trim().toLowerCase();
1721
- if (trimmed === "all") {
1722
- return ALL_TOOLSET_IDS;
1723
- }
1724
- const selected = new Set(trimmed
1725
- .split(",")
1726
- .map(s => s.trim())
1727
- .filter((s) => ALL_TOOLSET_IDS.has(s)));
1728
- if (selected.size === 0) {
1729
- logger.warn(`No valid toolsets found in configuration (${raw}). Falling back to default toolsets.`);
1730
- return DEFAULT_TOOLSET_IDS;
1731
- }
1732
- return selected;
1733
- }
1734
- function parseIndividualTools(raw) {
1735
- if (!raw || raw.trim() === "") {
1736
- return new Set();
1737
- }
1738
- const allToolNames = new Set(allTools.map((t) => t.name));
1739
- const parsed = raw
1740
- .trim()
1741
- .split(",")
1742
- .map(s => s.trim().toLowerCase())
1743
- .filter(Boolean);
1744
- const unknown = parsed.filter(name => !allToolNames.has(name));
1745
- if (unknown.length > 0) {
1746
- logger.warn(`Unknown tool names in GITLAB_TOOLS (will be ignored): ${unknown.join(", ")}`);
1747
- }
1748
- return new Set(parsed);
1749
- }
1750
- function buildFeatureFlagOverrides() {
1751
- const overrides = new Set();
1752
- if (USE_GITLAB_WIKI) {
1753
- for (const t of wikiToolNames)
1754
- overrides.add(t);
1755
- }
1756
- if (USE_MILESTONE) {
1757
- for (const t of milestoneToolNames)
1758
- overrides.add(t);
1759
- }
1760
- if (USE_PIPELINE) {
1761
- for (const t of pipelineToolNames)
1762
- overrides.add(t);
1763
- }
1764
- return overrides;
1765
- }
1766
- function isToolInEnabledToolset(toolName, enabledToolsets) {
1767
- const toolsetId = TOOLSET_BY_TOOL_NAME.get(toolName);
1768
- // Tools not in any toolset (e.g. execute_graphql) are excluded by default
1769
- if (toolsetId === undefined)
1770
- return false;
1771
- return enabledToolsets.has(toolsetId);
1772
- }
1773
686
  // Compute at startup
1774
687
  const enabledToolsets = parseEnabledToolsets(GITLAB_TOOLSETS_RAW);
1775
688
  const individuallyEnabledTools = parseIndividualTools(GITLAB_TOOLS_RAW);
@@ -1780,25 +693,6 @@ if (GITLAB_TOOLSETS_RAW && (USE_PIPELINE || USE_MILESTONE || USE_GITLAB_WIKI)) {
1780
693
  "Legacy flags add tools additively on top of the toolset selection and may produce unexpected results.");
1781
694
  }
1782
695
  const MERGE_REQUEST_DEPLOYMENT_SUMMARY_LIMIT = 10;
1783
- /**
1784
- * Smart URL handling for GitLab API
1785
- *
1786
- * @param {string | undefined} url - Input GitLab API URL
1787
- * @returns {string} Normalized GitLab API URL with /api/v4 path
1788
- */
1789
- function normalizeGitLabApiUrl(url) {
1790
- if (!url) {
1791
- return "https://gitlab.com/api/v4";
1792
- }
1793
- let normalizedUrl = url.trim();
1794
- if (normalizedUrl.endsWith("/")) {
1795
- normalizedUrl = normalizedUrl.slice(0, -1);
1796
- }
1797
- if (!normalizedUrl.endsWith("/api/v4")) {
1798
- normalizedUrl = `${normalizedUrl}/api/v4`;
1799
- }
1800
- return normalizedUrl;
1801
- }
1802
696
  // Use the normalizeGitLabApiUrl function to handle various URL formats
1803
697
  const GITLAB_API_URLS = (getConfig("api-url", "GITLAB_API_URL") || "https://gitlab.com")
1804
698
  .split(",")
@@ -2311,23 +1205,6 @@ async function convertIssueType(projectId, issueIid, newType) {
2311
1205
  };
2312
1206
  }
2313
1207
  // --- Work item hierarchy ---
2314
- /**
2315
- * Set a parent for a work item (issue hierarchy).
2316
- */
2317
- async function setIssueParent(projectId, issueIid, parentProjectId, parentIssueIid) {
2318
- const { workItemGID } = await resolveWorkItemGID(projectId, issueIid);
2319
- const { workItemGID: parentGID } = await resolveWorkItemGID(parentProjectId, parentIssueIid);
2320
- const data = await executeGraphQL(`mutation($id: WorkItemID!, $parentId: WorkItemID!) {
2321
- workItemUpdate(input: { id: $id, hierarchyWidget: { parentId: $parentId } }) {
2322
- workItem { id }
2323
- errors
2324
- }
2325
- }`, { id: workItemGID, parentId: parentGID });
2326
- if (data.workItemUpdate.errors?.length > 0) {
2327
- throw new Error(`Failed to set parent: ${data.workItemUpdate.errors.join(", ")}`);
2328
- }
2329
- return { id: workItemGID, parentId: parentGID };
2330
- }
2331
1208
  /**
2332
1209
  * Remove the parent from a work item.
2333
1210
  */
@@ -2343,83 +1220,6 @@ async function removeIssueParent(projectId, issueIid) {
2343
1220
  throw new Error(`Failed to remove parent: ${data.workItemUpdate.errors.join(", ")}`);
2344
1221
  }
2345
1222
  }
2346
- /**
2347
- * List children of a work item (hierarchy widget).
2348
- */
2349
- async function listIssueChildren(projectId, issueIid) {
2350
- projectId = decodeURIComponent(projectId);
2351
- const effectiveProjectId = getEffectiveProjectId(projectId);
2352
- // Get project path
2353
- const projectUrl = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}`);
2354
- const projectResponse = await fetch(projectUrl.toString(), {
2355
- ...getFetchConfig(),
2356
- });
2357
- await handleGitLabError(projectResponse);
2358
- const project = await projectResponse.json();
2359
- const data = await executeGraphQL(`query($path: ID!, $iid: String!) {
2360
- namespace(fullPath: $path) {
2361
- workItem(iid: $iid) {
2362
- id
2363
- title
2364
- widgets {
2365
- __typename
2366
- ... on WorkItemWidgetHierarchy {
2367
- parent {
2368
- id
2369
- title
2370
- webUrl
2371
- workItemType { name }
2372
- }
2373
- children {
2374
- nodes {
2375
- id
2376
- title
2377
- state
2378
- webUrl
2379
- workItemType { name }
2380
- }
2381
- }
2382
- }
2383
- }
2384
- }
2385
- }
2386
- }`, { path: project.path_with_namespace, iid: String(issueIid) });
2387
- if (!data.namespace?.workItem) {
2388
- throw new Error(`Work item #${issueIid} not found`);
2389
- }
2390
- // Extract hierarchy widget
2391
- const hierarchyWidget = data.namespace.workItem.widgets?.find((w) => w.__typename === "WorkItemWidgetHierarchy");
2392
- return {
2393
- id: data.namespace.workItem.id,
2394
- title: data.namespace.workItem.title,
2395
- parent: hierarchyWidget?.parent || null,
2396
- children: hierarchyWidget?.children?.nodes || [],
2397
- };
2398
- }
2399
- /**
2400
- * Add a child to a parent work item.
2401
- */
2402
- async function addIssueChild(projectId, issueIid, childProjectId, childIssueIid) {
2403
- const { workItemGID: parentGID } = await resolveWorkItemGID(projectId, issueIid);
2404
- const { workItemGID: childGID } = await resolveWorkItemGID(childProjectId, childIssueIid);
2405
- const data = await executeGraphQL(`mutation($id: WorkItemID!, $childId: WorkItemID!) {
2406
- workItemUpdate(input: { id: $id, hierarchyWidget: { childrenIds: [$childId] } }) {
2407
- workItem { id }
2408
- errors
2409
- }
2410
- }`, { id: parentGID, childId: childGID });
2411
- if (data.workItemUpdate.errors?.length > 0) {
2412
- throw new Error(`Failed to add child: ${data.workItemUpdate.errors.join(", ")}`);
2413
- }
2414
- return { parentId: parentGID, childId: childGID };
2415
- }
2416
- /**
2417
- * Remove a child from a parent work item by setting the child's parent to null.
2418
- */
2419
- async function removeIssueChild(projectId, issueIid, childProjectId, childIssueIid) {
2420
- // Removing a child is done by removing the parent from the child
2421
- await removeIssueParent(childProjectId, childIssueIid);
2422
- }
2423
1223
  // --- Work item status ---
2424
1224
  /**
2425
1225
  * List available statuses for a work item type in a project.
@@ -2815,35 +1615,6 @@ async function updateIncidentEscalationStatus(projectPath, incidentIid, status)
2815
1615
  }
2816
1616
  return data.issueSetEscalationStatus.issue;
2817
1617
  }
2818
- /**
2819
- * Set the status of a work item.
2820
- */
2821
- async function setIssueStatus(projectId, issueIid, status) {
2822
- const { workItemGID } = await resolveWorkItemGID(projectId, issueIid);
2823
- const data = await executeGraphQL(`mutation($id: WorkItemID!, $status: WorkItemsStatusesStatusID!) {
2824
- workItemUpdate(input: { id: $id, statusWidget: { status: $status } }) {
2825
- workItem {
2826
- id
2827
- widgets {
2828
- __typename
2829
- ... on WorkItemWidgetStatus {
2830
- status { id name category color }
2831
- }
2832
- }
2833
- }
2834
- errors
2835
- }
2836
- }`, { id: workItemGID, status });
2837
- if (data.workItemUpdate.errors?.length > 0) {
2838
- throw new Error(`Failed to set status: ${data.workItemUpdate.errors.join(", ")}`);
2839
- }
2840
- // Extract the current status from the response
2841
- const statusWidget = data.workItemUpdate.workItem?.widgets?.find((w) => w.__typename === "WorkItemWidgetStatus");
2842
- return {
2843
- id: data.workItemUpdate.workItem.id,
2844
- status: statusWidget?.status || null,
2845
- };
2846
- }
2847
1618
  /**
2848
1619
  * Resolve a project ID (numeric or path) to its full path_with_namespace.
2849
1620
  */
@@ -3195,7 +1966,8 @@ async function createWorkItem(projectId, options) {
3195
1966
  inputValues.push("labelsWidget: { labelIds: $labelIds }");
3196
1967
  variables.labelIds = labelIds;
3197
1968
  }
3198
- if (options.weight !== undefined) {
1969
+ // Incidents don't support the weight widget
1970
+ if (options.weight !== undefined && typeName !== "incident") {
3199
1971
  inputFields.push("$weight: Int");
3200
1972
  inputValues.push("weightWidget: { weight: $weight }");
3201
1973
  variables.weight = options.weight;
@@ -3454,7 +2226,7 @@ async function updateWorkItem(projectId, iid, options) {
3454
2226
  if (options.children_to_add && options.children_to_add.length > 0) {
3455
2227
  const childGIDs = [];
3456
2228
  for (const child of options.children_to_add) {
3457
- const { workItemGID: childGID } = await resolveWorkItemGID(child.project_id, child.iid);
2229
+ const { workItemGID: childGID } = await resolveWorkItemGID(child.project_id || projectId, child.iid);
3458
2230
  childGIDs.push(childGID);
3459
2231
  }
3460
2232
  const addData = await executeGraphQL(`mutation($id: WorkItemID!, $childrenIds: [WorkItemID!]!) {
@@ -3469,7 +2241,7 @@ async function updateWorkItem(projectId, iid, options) {
3469
2241
  // Handle children_to_remove: remove parent from each child
3470
2242
  if (options.children_to_remove && options.children_to_remove.length > 0) {
3471
2243
  for (const child of options.children_to_remove) {
3472
- await removeIssueParent(child.project_id, child.iid);
2244
+ await removeIssueParent(child.project_id || projectId, child.iid);
3473
2245
  }
3474
2246
  }
3475
2247
  // Handle linked_items_to_add: use workItemAddLinkedItems mutation
@@ -3480,7 +2252,7 @@ async function updateWorkItem(projectId, iid, options) {
3480
2252
  const linkType = item.link_type || "RELATED";
3481
2253
  if (!groupedByType[linkType])
3482
2254
  groupedByType[linkType] = [];
3483
- const { workItemGID: targetGID } = await resolveWorkItemGID(item.project_id, item.iid);
2255
+ const { workItemGID: targetGID } = await resolveWorkItemGID(item.project_id || projectId, item.iid);
3484
2256
  groupedByType[linkType].push(targetGID);
3485
2257
  }
3486
2258
  for (const [linkType, targetGIDs] of Object.entries(groupedByType)) {
@@ -3498,7 +2270,7 @@ async function updateWorkItem(projectId, iid, options) {
3498
2270
  if (options.linked_items_to_remove && options.linked_items_to_remove.length > 0) {
3499
2271
  const targetGIDs = [];
3500
2272
  for (const item of options.linked_items_to_remove) {
3501
- const { workItemGID: targetGID } = await resolveWorkItemGID(item.project_id, item.iid);
2273
+ const { workItemGID: targetGID } = await resolveWorkItemGID(item.project_id || projectId, item.iid);
3502
2274
  targetGIDs.push(targetGID);
3503
2275
  }
3504
2276
  const removeLinkedData = await executeGraphQL(`mutation($id: WorkItemID!, $workItemsIds: [WorkItemID!]!) {
@@ -4052,43 +2824,6 @@ async function createOrUpdateFile(projectId, filePath, content, commitMessage, b
4052
2824
  const data = await response.json();
4053
2825
  return GitLabCreateUpdateFileResponseSchema.parse(data);
4054
2826
  }
4055
- /**
4056
- * Create a tree structure in a GitLab project repository
4057
- * 저장소에 트리 구조 생성
4058
- *
4059
- * @param {string} projectId - The ID or URL-encoded path of the project
4060
- * @param {FileOperation[]} files - Array of file operations
4061
- * @param {string} [ref] - The name of the branch, tag or commit
4062
- * @returns {Promise<GitLabTree>} The created tree
4063
- */
4064
- async function createTree(projectId, files, ref) {
4065
- projectId = decodeURIComponent(projectId); // Decode project ID
4066
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/repository/tree`);
4067
- if (ref) {
4068
- url.searchParams.append("ref", ref);
4069
- }
4070
- const response = await fetch(url.toString(), {
4071
- ...getFetchConfig(),
4072
- method: "POST",
4073
- body: JSON.stringify({
4074
- files: files.map(file => ({
4075
- file_path: file.path,
4076
- content: encodeRepoFilePayloadContent(file.content),
4077
- encoding: GITLAB_REPO_FILE_ENCODING,
4078
- })),
4079
- }),
4080
- });
4081
- if (response.status === 400) {
4082
- const errorBody = await response.text();
4083
- throw new Error(`Invalid request: ${errorBody}`);
4084
- }
4085
- if (!response.ok) {
4086
- const errorBody = await response.text();
4087
- throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
4088
- }
4089
- const data = await response.json();
4090
- return GitLabTreeSchema.parse(data);
4091
- }
4092
2827
  /**
4093
2828
  * Create a commit in a GitLab project repository
4094
2829
  * 저장소에 커밋 생성
@@ -4310,18 +3045,6 @@ async function getProjectMergeMethod(projectId) {
4310
3045
  .parse(data).merge_method;
4311
3046
  return typeof mergeMethod === "string" ? mergeMethod : null;
4312
3047
  }
4313
- function estimateMergeCommitCount(mergeMethod, sourceCommitCount) {
4314
- if (sourceCommitCount === 0) {
4315
- return 0;
4316
- }
4317
- if (mergeMethod === "merge") {
4318
- return 1;
4319
- }
4320
- if (mergeMethod === "ff" || mergeMethod === "rebase_merge") {
4321
- return 0;
4322
- }
4323
- return null;
4324
- }
4325
3048
  async function buildMergeRequestCommitAdditionSummary(projectId, mergeRequest) {
4326
3049
  try {
4327
3050
  const sourceCommitCount = await getMergeRequestSourceCommitCount(projectId, mergeRequest.iid);
@@ -5171,100 +3894,6 @@ async function getMergeRequestVersion(projectId, mergeRequestIid, versionId, uni
5171
3894
  const data = await response.json();
5172
3895
  return GitLabMergeRequestVersionDetailSchema.parse(data);
5173
3896
  }
5174
- /**
5175
- * List all namespaces
5176
- * 사용 가능한 모든 네임스페이스 목록 조회
5177
- *
5178
- * @param {Object} options - Options for listing namespaces
5179
- * @param {string} [options.search] - Search query to filter namespaces
5180
- * @param {boolean} [options.owned_only] - Only return namespaces owned by the authenticated user
5181
- * @param {boolean} [options.top_level_only] - Only return top-level namespaces
5182
- * @returns {Promise<GitLabNamespace[]>} List of namespaces
5183
- */
5184
- async function listNamespaces(options) {
5185
- const url = new URL(`${getEffectiveApiUrl()}/namespaces`);
5186
- if (options.search) {
5187
- url.searchParams.append("search", options.search);
5188
- }
5189
- if (options.owned_only) {
5190
- url.searchParams.append("owned_only", "true");
5191
- }
5192
- if (options.top_level_only) {
5193
- url.searchParams.append("top_level_only", "true");
5194
- }
5195
- const response = await fetch(url.toString(), {
5196
- ...getFetchConfig(),
5197
- });
5198
- await handleGitLabError(response);
5199
- const data = await response.json();
5200
- return z.array(GitLabNamespaceSchema).parse(data);
5201
- }
5202
- /**
5203
- * Get details on a namespace
5204
- * 네임스페이스 상세 정보 조회
5205
- *
5206
- * @param {string} id - The ID or URL-encoded path of the namespace
5207
- * @returns {Promise<GitLabNamespace>} The namespace details
5208
- */
5209
- async function getNamespace(id) {
5210
- const url = new URL(`${getEffectiveApiUrl()}/namespaces/${encodeURIComponent(id)}`);
5211
- const response = await fetch(url.toString(), {
5212
- ...getFetchConfig(),
5213
- });
5214
- await handleGitLabError(response);
5215
- const data = await response.json();
5216
- return GitLabNamespaceSchema.parse(data);
5217
- }
5218
- /**
5219
- * Verify if a namespace exists
5220
- * 네임스페이스 존재 여부 확인
5221
- *
5222
- * @param {string} namespacePath - The path of the namespace to check
5223
- * @param {number} [parentId] - The ID of the parent namespace
5224
- * @returns {Promise<GitLabNamespaceExistsResponse>} The verification result
5225
- */
5226
- async function verifyNamespaceExistence(namespacePath, parentId) {
5227
- const url = new URL(`${getEffectiveApiUrl()}/namespaces/${encodeURIComponent(namespacePath)}/exists`);
5228
- if (parentId) {
5229
- url.searchParams.append("parent_id", parentId.toString());
5230
- }
5231
- const response = await fetch(url.toString(), {
5232
- ...getFetchConfig(),
5233
- });
5234
- await handleGitLabError(response);
5235
- const data = await response.json();
5236
- return GitLabNamespaceExistsResponseSchema.parse(data);
5237
- }
5238
- /**
5239
- * Get a single project
5240
- * 단일 프로젝트 조회
5241
- *
5242
- * @param {string} projectId - The ID or URL-encoded path of the project
5243
- * @param {Object} options - Options for getting project details
5244
- * @param {boolean} [options.license] - Include project license data
5245
- * @param {boolean} [options.statistics] - Include project statistics
5246
- * @param {boolean} [options.with_custom_attributes] - Include custom attributes in response
5247
- * @returns {Promise<GitLabProject>} Project details
5248
- */
5249
- async function getProject(projectId, options = {}) {
5250
- projectId = decodeURIComponent(projectId); // Decode project ID
5251
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}`);
5252
- if (options.license) {
5253
- url.searchParams.append("license", "true");
5254
- }
5255
- if (options.statistics) {
5256
- url.searchParams.append("statistics", "true");
5257
- }
5258
- if (options.with_custom_attributes) {
5259
- url.searchParams.append("with_custom_attributes", "true");
5260
- }
5261
- const response = await fetch(url.toString(), {
5262
- ...getFetchConfig(),
5263
- });
5264
- await handleGitLabError(response);
5265
- const data = await response.json();
5266
- return GitLabRepositorySchema.parse(data);
5267
- }
5268
3897
  /**
5269
3898
  * List projects
5270
3899
  * 프로젝트 목록 조회
@@ -5484,18 +4113,6 @@ async function listWebhooks(options) {
5484
4113
  await handleGitLabError(response);
5485
4114
  return (await response.json());
5486
4115
  }
5487
- /**
5488
- * Summarize webhook events by stripping heavy payload fields
5489
- */
5490
- function summarizeWebhookEvents(events) {
5491
- return events.map(event => ({
5492
- id: event.id,
5493
- url: event.url,
5494
- trigger: event.trigger,
5495
- response_status: event.response_status,
5496
- execution_duration: event.execution_duration,
5497
- }));
5498
- }
5499
4116
  /**
5500
4117
  * Fetch a single page of webhook events
5501
4118
  */
@@ -6880,37 +5497,6 @@ async function downloadReleaseAsset(projectId, tagName, directAssetPath) {
6880
5497
  }
6881
5498
  // Request handlers are now registered inside createServer() factory function
6882
5499
  // to ensure each transport connection gets its own Server instance (GHSA-345p-7cg4-v4c7).
6883
- /**
6884
- * Filter diffs by excluded file patterns
6885
- * Safely handles invalid regex patterns by logging and ignoring them
6886
- *
6887
- * @param diffs - Array of diff objects with new_path property
6888
- * @param excludedFilePatterns - Array of regex patterns to exclude
6889
- * @returns Filtered array of diffs
6890
- */
6891
- function filterDiffsByPatterns(diffs, excludedFilePatterns) {
6892
- if (!excludedFilePatterns?.length)
6893
- return diffs;
6894
- const regexPatterns = excludedFilePatterns
6895
- .map(pattern => {
6896
- try {
6897
- return new RegExp(pattern);
6898
- }
6899
- catch (e) {
6900
- console.warn(`Invalid regex pattern ignored: ${pattern}`);
6901
- return null;
6902
- }
6903
- })
6904
- .filter((regex) => regex !== null);
6905
- if (regexPatterns.length === 0)
6906
- return diffs;
6907
- const matchesAnyPattern = (path) => {
6908
- if (!path)
6909
- return false;
6910
- return regexPatterns.some(regex => regex.test(path));
6911
- };
6912
- return diffs.filter(diff => !matchesAnyPattern(diff.new_path));
6913
- }
6914
5500
  async function handleToolCall(params) {
6915
5501
  try {
6916
5502
  if (!params.arguments) {
@@ -6922,7 +5508,16 @@ async function handleToolCall(params) {
6922
5508
  }
6923
5509
  // Lazy OAuth token refresh: only validate/refresh when a tool is actually called
6924
5510
  await ensureValidOAuthToken();
6925
- logger.info(params.name);
5511
+ // Normalize common parameter aliases that LLMs send
5512
+ const args = params.arguments;
5513
+ if (args) {
5514
+ // work_item_iid -> iid (for work item tools)
5515
+ if (args.work_item_iid !== undefined && args.iid === undefined) {
5516
+ args.iid = args.work_item_iid;
5517
+ delete args.work_item_iid;
5518
+ }
5519
+ }
5520
+ logger.info({ tool: params.name, event: "tool_call_start" }, `tool_call_start: ${params.name}`);
6926
5521
  switch (params.name) {
6927
5522
  case "execute_graphql": {
6928
5523
  const args = ExecuteGraphQLSchema.parse(params.arguments);
@@ -8396,9 +6991,7 @@ async function startSSEServer() {
8396
6991
  });
8397
6992
  });
8398
6993
  const httpServer = app.listen(Number(PORT), HOST, () => {
8399
- logger.info(`GitLab MCP Server running with SSE transport`);
8400
- const colorGreen = "\x1b[32m";
8401
- const colorReset = "\x1b[0m";
6994
+ logger.info("GitLab MCP Server running with SSE transport");
8402
6995
  logger.info(`${colorGreen}Endpoint: http://${HOST}:${PORT}/sse${colorReset}`);
8403
6996
  });
8404
6997
  const shutdown = async (signal) => {