@zereight/mcp-gitlab 2.0.35 → 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 && 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,60 +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 ENABLE_DYNAMIC_API_URL = getConfig("enable-dynamic-api-url", "ENABLE_DYNAMIC_API_URL") === "true";
347
- const SESSION_TIMEOUT_SECONDS = Number.parseInt(getConfig("session-timeout", "SESSION_TIMEOUT_SECONDS", "3600"), 10);
348
- const HOST = getConfig("host", "HOST") || "127.0.0.1";
349
- const PORT = Number.parseInt(getConfig("port", "PORT", "3002"), 10);
350
- // Add proxy configuration
351
- const HTTP_PROXY = getConfig("http-proxy", "HTTP_PROXY");
352
- const HTTPS_PROXY = getConfig("https-proxy", "HTTPS_PROXY");
353
- const NO_PROXY = getConfig("no-proxy", "NO_PROXY");
354
- const NODE_TLS_REJECT_UNAUTHORIZED = getConfig("tls-reject-unauthorized", "NODE_TLS_REJECT_UNAUTHORIZED");
355
- const GITLAB_CA_CERT_PATH = getConfig("ca-cert-path", "GITLAB_CA_CERT_PATH");
356
- const GITLAB_POOL_MAX_SIZE = getConfig("pool-max-size", "GITLAB_POOL_MAX_SIZE")
357
- ? Number.parseInt(getConfig("pool-max-size", "GITLAB_POOL_MAX_SIZE"), 10)
358
- : 100;
359
- let sslOptions = undefined;
360
- if (NODE_TLS_REJECT_UNAUTHORIZED === "0") {
361
- sslOptions = { rejectUnauthorized: false };
362
- }
363
- else if (GITLAB_CA_CERT_PATH) {
364
- const ca = fs.readFileSync(GITLAB_CA_CERT_PATH);
365
- sslOptions = { ca };
366
- }
367
- // Configure proxy agents if proxies are set
368
- let httpAgent = undefined;
369
- let httpsAgent = undefined;
370
- if (HTTP_PROXY) {
371
- if (HTTP_PROXY.startsWith("socks")) {
372
- httpAgent = new SocksProxyAgent(HTTP_PROXY);
373
- }
374
- else {
375
- httpAgent = new HttpProxyAgent(HTTP_PROXY);
376
- }
377
- }
378
- if (HTTPS_PROXY) {
379
- if (HTTPS_PROXY.startsWith("socks")) {
380
- 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
+ }
381
483
  }
382
- else {
383
- 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
+ }
384
488
  }
385
489
  }
386
- httpsAgent = httpsAgent || new HttpsAgent(sslOptions);
387
- httpAgent = httpAgent || new Agent();
388
- // Initialize the client pool for managing multiple GitLab instances
389
490
  const clientPool = new GitLabClientPool({
390
491
  apiUrls: (getConfig("api-url", "GITLAB_API_URL") || "https://gitlab.com")
391
492
  .split(",")
@@ -532,11 +633,10 @@ function buildAuthHeaders() {
532
633
  }
533
634
  return {}; // No auth headers if no session context
534
635
  }
535
- // CI job tokens use a dedicated header (not Bearer/Private-Token)
536
- if (GITLAB_JOB_TOKEN) {
537
- return { "JOB-TOKEN": String(GITLAB_JOB_TOKEN) };
538
- }
539
- // Standard mode: prioritize OAuth token, then fall back to environment token
636
+ // Standard mode: PAT preferred over job token (broader permissions).
637
+ // OAuth token takes priority over PAT when both are set.
638
+ // NOTE: Changed in PR #400 — previously GITLAB_JOB_TOKEN had highest priority.
639
+ // If both GITLAB_PERSONAL_ACCESS_TOKEN and GITLAB_JOB_TOKEN are set, PAT wins.
540
640
  const token = OAUTH_ACCESS_TOKEN || GITLAB_PERSONAL_ACCESS_TOKEN;
541
641
  if (IS_OLD && token) {
542
642
  return { "Private-Token": String(token) };
@@ -544,6 +644,10 @@ function buildAuthHeaders() {
544
644
  if (token) {
545
645
  return { Authorization: `Bearer ${token}` };
546
646
  }
647
+ // Fall back to CI job token
648
+ if (GITLAB_JOB_TOKEN) {
649
+ return { "JOB-TOKEN": String(GITLAB_JOB_TOKEN) };
650
+ }
547
651
  return {};
548
652
  }
549
653
  /**
@@ -579,1190 +683,6 @@ const getFetchConfig = () => {
579
683
  agent: agent,
580
684
  };
581
685
  };
582
- const toJSONSchema = (schema) => {
583
- const jsonSchema = zodToJsonSchema(schema, { $refStrategy: "none" });
584
- // Post-process to fix nullable/optional fields that should truly be optional
585
- function fixNullableOptional(obj) {
586
- if (obj && typeof obj === "object") {
587
- // If this object has properties, process them
588
- if (obj.properties) {
589
- const requiredSet = new Set(obj.required || []);
590
- Object.keys(obj.properties).forEach(key => {
591
- const prop = obj.properties[key];
592
- // Handle fields that can be null or omitted
593
- // If a property has type: ["object", "null"] or anyOf with null, it should not be required
594
- if (prop.anyOf && prop.anyOf.some((t) => t.type === "null")) {
595
- requiredSet.delete(key);
596
- }
597
- else if (Array.isArray(prop.type) && prop.type.includes("null")) {
598
- requiredSet.delete(key);
599
- }
600
- // Recursively process nested objects
601
- obj.properties[key] = fixNullableOptional(prop);
602
- });
603
- // Normalize the required array after processing all properties
604
- if (requiredSet.size > 0) {
605
- obj.required = Array.from(requiredSet);
606
- }
607
- else if (Object.prototype.hasOwnProperty.call(obj, "required")) {
608
- delete obj.required;
609
- }
610
- }
611
- // Process anyOf/allOf/oneOf
612
- ["anyOf", "allOf", "oneOf"].forEach(combiner => {
613
- if (obj[combiner]) {
614
- obj[combiner] = obj[combiner].map(fixNullableOptional);
615
- }
616
- });
617
- }
618
- return obj;
619
- }
620
- return fixNullableOptional(jsonSchema);
621
- };
622
- // Define all available tools
623
- const allTools = [
624
- {
625
- name: "merge_merge_request",
626
- description: "Merge a merge request in a GitLab project",
627
- inputSchema: toJSONSchema(MergeMergeRequestSchema),
628
- },
629
- {
630
- name: "approve_merge_request",
631
- description: "Approve a merge request. Requires appropriate permissions.",
632
- inputSchema: toJSONSchema(ApproveMergeRequestSchema),
633
- },
634
- {
635
- name: "unapprove_merge_request",
636
- description: "Unapprove a previously approved merge request. Requires appropriate permissions.",
637
- inputSchema: toJSONSchema(UnapproveMergeRequestSchema),
638
- },
639
- {
640
- name: "get_merge_request_approval_state",
641
- description: "Get merge request approval details including approvers (uses approval_state when available, falls back to approvals endpoint)",
642
- inputSchema: toJSONSchema(GetMergeRequestApprovalStateSchema),
643
- },
644
- {
645
- name: "get_merge_request_conflicts",
646
- description: "Get the conflicts of a merge request in a GitLab project",
647
- inputSchema: toJSONSchema(GetMergeRequestConflictsSchema),
648
- },
649
- {
650
- name: "execute_graphql",
651
- description: "Execute a GitLab GraphQL query",
652
- inputSchema: zodToJsonSchema(ExecuteGraphQLSchema),
653
- },
654
- {
655
- name: "create_or_update_file",
656
- description: "Create or update a single file in a GitLab project",
657
- inputSchema: toJSONSchema(CreateOrUpdateFileSchema),
658
- },
659
- {
660
- name: "search_repositories",
661
- description: "Search for GitLab projects",
662
- inputSchema: toJSONSchema(SearchRepositoriesSchema),
663
- },
664
- {
665
- name: "create_repository",
666
- description: "Create a new GitLab project",
667
- inputSchema: toJSONSchema(CreateRepositorySchema),
668
- },
669
- {
670
- name: "get_file_contents",
671
- description: "Get the contents of a file or directory from a GitLab project",
672
- inputSchema: toJSONSchema(GetFileContentsSchema),
673
- },
674
- {
675
- name: "push_files",
676
- description: "Push multiple files to a GitLab project in a single commit",
677
- inputSchema: toJSONSchema(PushFilesSchema),
678
- },
679
- {
680
- name: "create_issue",
681
- description: "Create a new issue in a GitLab project",
682
- inputSchema: toJSONSchema(CreateIssueSchema),
683
- },
684
- {
685
- name: "create_merge_request",
686
- description: "Create a new merge request in a GitLab project",
687
- inputSchema: toJSONSchema(CreateMergeRequestSchema),
688
- },
689
- {
690
- name: "fork_repository",
691
- description: "Fork a GitLab project to your account or specified namespace",
692
- inputSchema: toJSONSchema(ForkRepositorySchema),
693
- },
694
- {
695
- name: "create_branch",
696
- description: "Create a new branch in a GitLab project",
697
- inputSchema: toJSONSchema(CreateBranchSchema),
698
- },
699
- {
700
- name: "get_merge_request",
701
- description: "Get details of a merge request with compact deployment, commit addition, and approval summaries (Either mergeRequestIid or branchName must be provided)",
702
- inputSchema: toJSONSchema(GetMergeRequestSchema),
703
- },
704
- {
705
- name: "get_merge_request_diffs",
706
- description: "Get the changes/diffs of a merge request (Either mergeRequestIid or branchName must be provided)",
707
- inputSchema: toJSONSchema(GetMergeRequestDiffsSchema),
708
- },
709
- {
710
- name: "list_merge_request_changed_files",
711
- description: "STEP 1 of code review workflow. " +
712
- "Returns ONLY the list of changed file paths in a merge request — WITHOUT diff content. " +
713
- "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). " +
714
- "This avoids loading the entire diff payload at once and reduces API calls. " +
715
- "Supports excluded_file_patterns filtering using regex. " +
716
- "Returns: new_path, old_path, new_file, deleted_file, renamed_file flags for each file. " +
717
- "(Either mergeRequestIid or branchName must be provided)",
718
- inputSchema: toJSONSchema(ListMergeRequestChangedFilesSchema),
719
- },
720
- {
721
- name: "list_merge_request_diffs",
722
- description: "List merge request diffs with pagination support (Either mergeRequestIid or branchName must be provided)",
723
- inputSchema: toJSONSchema(ListMergeRequestDiffsSchema),
724
- },
725
- {
726
- name: "get_merge_request_file_diff",
727
- description: "STEP 2 of code review workflow. " +
728
- "Get diffs for one or more files from a merge request. " +
729
- "Call list_merge_request_changed_files first to get file paths, then pass them as an array to fetch their diffs efficiently. " +
730
- "Batching multiple files (recommended 3-5) is supported and preferred over individual requests. " +
731
- "Returns an array of results - one per requested file path. Files not found are returned with error messages. " +
732
- "(Either mergeRequestIid or branchName must be provided)",
733
- inputSchema: toJSONSchema(GetMergeRequestFileDiffSchema),
734
- },
735
- {
736
- name: "list_merge_request_versions",
737
- description: "List all versions of a merge request",
738
- inputSchema: toJSONSchema(ListMergeRequestVersionsSchema),
739
- },
740
- {
741
- name: "get_merge_request_version",
742
- description: "Get a specific version of a merge request",
743
- inputSchema: toJSONSchema(GetMergeRequestVersionSchema),
744
- },
745
- {
746
- name: "get_branch_diffs",
747
- description: "Get the changes/diffs between two branches or commits in a GitLab project",
748
- inputSchema: toJSONSchema(GetBranchDiffsSchema),
749
- },
750
- {
751
- name: "update_merge_request",
752
- description: "Update a merge request (Either mergeRequestIid or branchName must be provided)",
753
- inputSchema: toJSONSchema(UpdateMergeRequestSchema),
754
- },
755
- {
756
- name: "create_note",
757
- description: "Create a new note (comment) to an issue or merge request",
758
- inputSchema: toJSONSchema(CreateNoteSchema),
759
- },
760
- {
761
- name: "create_merge_request_thread",
762
- description: "Create a new thread on a merge request",
763
- inputSchema: toJSONSchema(CreateMergeRequestThreadSchema),
764
- },
765
- {
766
- name: "resolve_merge_request_thread",
767
- description: "Resolve a thread on a merge request",
768
- inputSchema: toJSONSchema(ResolveMergeRequestThreadSchema),
769
- },
770
- {
771
- name: "mr_discussions",
772
- description: "List discussion items for a merge request",
773
- inputSchema: toJSONSchema(ListMergeRequestDiscussionsSchema),
774
- },
775
- {
776
- name: "delete_merge_request_discussion_note",
777
- description: "Delete a discussion note on a merge request",
778
- inputSchema: toJSONSchema(DeleteMergeRequestDiscussionNoteSchema),
779
- },
780
- {
781
- name: "update_merge_request_discussion_note",
782
- description: "Update a discussion note on a merge request",
783
- inputSchema: toJSONSchema(UpdateMergeRequestDiscussionNoteSchema),
784
- },
785
- {
786
- name: "create_merge_request_discussion_note",
787
- description: "Add a new discussion note to an existing merge request thread",
788
- inputSchema: toJSONSchema(CreateMergeRequestDiscussionNoteSchema),
789
- },
790
- {
791
- name: "create_merge_request_note",
792
- description: "Add a new note to a merge request",
793
- inputSchema: toJSONSchema(CreateMergeRequestNoteSchema),
794
- },
795
- {
796
- name: "delete_merge_request_note",
797
- description: "Delete an existing merge request note",
798
- inputSchema: toJSONSchema(DeleteMergeRequestNoteSchema),
799
- },
800
- {
801
- name: "get_merge_request_note",
802
- description: "Get a specific note for a merge request",
803
- inputSchema: toJSONSchema(GetMergeRequestNoteSchema),
804
- },
805
- {
806
- name: "get_merge_request_notes",
807
- description: "List notes for a merge request",
808
- inputSchema: toJSONSchema(GetMergeRequestNotesSchema),
809
- },
810
- {
811
- name: "update_merge_request_note",
812
- description: "Modify an existing merge request note",
813
- inputSchema: toJSONSchema(UpdateMergeRequestNoteSchema),
814
- },
815
- {
816
- name: "get_draft_note",
817
- description: "Get a single draft note from a merge request",
818
- inputSchema: toJSONSchema(GetDraftNoteSchema),
819
- },
820
- {
821
- name: "list_draft_notes",
822
- description: "List draft notes for a merge request",
823
- inputSchema: toJSONSchema(ListDraftNotesSchema),
824
- },
825
- {
826
- name: "create_draft_note",
827
- description: "Create a draft note for a merge request",
828
- inputSchema: toJSONSchema(CreateDraftNoteSchema),
829
- },
830
- {
831
- name: "update_draft_note",
832
- description: "Update an existing draft note",
833
- inputSchema: toJSONSchema(UpdateDraftNoteSchema),
834
- },
835
- {
836
- name: "delete_draft_note",
837
- description: "Delete a draft note",
838
- inputSchema: toJSONSchema(DeleteDraftNoteSchema),
839
- },
840
- {
841
- name: "publish_draft_note",
842
- description: "Publish a single draft note",
843
- inputSchema: toJSONSchema(PublishDraftNoteSchema),
844
- },
845
- {
846
- name: "bulk_publish_draft_notes",
847
- description: "Publish all draft notes for a merge request",
848
- inputSchema: toJSONSchema(BulkPublishDraftNotesSchema),
849
- },
850
- {
851
- name: "update_issue_note",
852
- description: "Modify an existing issue thread note",
853
- inputSchema: toJSONSchema(UpdateIssueNoteSchema),
854
- },
855
- {
856
- name: "create_issue_note",
857
- description: "Add a new note to an existing issue thread",
858
- inputSchema: toJSONSchema(CreateIssueNoteSchema),
859
- },
860
- {
861
- name: "list_issues",
862
- description: "List issues (default: created by current user only; use scope='all' for all accessible issues)",
863
- inputSchema: toJSONSchema(ListIssuesSchema),
864
- },
865
- {
866
- name: "my_issues",
867
- description: "List issues assigned to the authenticated user (defaults to open issues)",
868
- inputSchema: toJSONSchema(MyIssuesSchema),
869
- },
870
- {
871
- name: "get_issue",
872
- description: "Get details of a specific issue in a GitLab project",
873
- inputSchema: toJSONSchema(GetIssueSchema),
874
- },
875
- {
876
- name: "update_issue",
877
- description: "Update an issue in a GitLab project",
878
- inputSchema: toJSONSchema(UpdateIssueSchema),
879
- },
880
- {
881
- name: "delete_issue",
882
- description: "Delete an issue from a GitLab project",
883
- inputSchema: toJSONSchema(DeleteIssueSchema),
884
- },
885
- {
886
- name: "list_issue_links",
887
- description: "List all issue links for a specific issue",
888
- inputSchema: toJSONSchema(ListIssueLinksSchema),
889
- },
890
- {
891
- name: "list_issue_discussions",
892
- description: "List discussions for an issue in a GitLab project",
893
- inputSchema: toJSONSchema(ListIssueDiscussionsSchema),
894
- },
895
- {
896
- name: "get_issue_link",
897
- description: "Get a specific issue link",
898
- inputSchema: toJSONSchema(GetIssueLinkSchema),
899
- },
900
- {
901
- name: "create_issue_link",
902
- description: "Create an issue link between two issues",
903
- inputSchema: toJSONSchema(CreateIssueLinkSchema),
904
- },
905
- {
906
- name: "delete_issue_link",
907
- description: "Delete an issue link",
908
- inputSchema: toJSONSchema(DeleteIssueLinkSchema),
909
- },
910
- {
911
- name: "list_namespaces",
912
- description: "List all namespaces available to the current user",
913
- inputSchema: toJSONSchema(ListNamespacesSchema),
914
- },
915
- {
916
- name: "get_namespace",
917
- description: "Get details of a namespace by ID or path",
918
- inputSchema: toJSONSchema(GetNamespaceSchema),
919
- },
920
- {
921
- name: "verify_namespace",
922
- description: "Verify if a namespace path exists",
923
- inputSchema: toJSONSchema(VerifyNamespaceSchema),
924
- },
925
- {
926
- name: "get_project",
927
- description: "Get details of a specific project",
928
- inputSchema: toJSONSchema(GetProjectSchema),
929
- },
930
- {
931
- name: "list_projects",
932
- description: "List projects accessible by the current user",
933
- inputSchema: toJSONSchema(ListProjectsSchema),
934
- },
935
- {
936
- name: "list_project_members",
937
- description: "List members of a GitLab project",
938
- inputSchema: toJSONSchema(ListProjectMembersSchema),
939
- },
940
- {
941
- name: "list_labels",
942
- description: "List labels for a project",
943
- inputSchema: toJSONSchema(ListLabelsSchema),
944
- },
945
- {
946
- name: "get_label",
947
- description: "Get a single label from a project",
948
- inputSchema: toJSONSchema(GetLabelSchema),
949
- },
950
- {
951
- name: "create_label",
952
- description: "Create a new label in a project",
953
- inputSchema: toJSONSchema(CreateLabelSchema),
954
- },
955
- {
956
- name: "update_label",
957
- description: "Update an existing label in a project",
958
- inputSchema: toJSONSchema(UpdateLabelSchema),
959
- },
960
- {
961
- name: "delete_label",
962
- description: "Delete a label from a project",
963
- inputSchema: toJSONSchema(DeleteLabelSchema),
964
- },
965
- {
966
- name: "list_group_projects",
967
- description: "List projects in a GitLab group with filtering options",
968
- inputSchema: toJSONSchema(ListGroupProjectsSchema),
969
- },
970
- {
971
- name: "list_wiki_pages",
972
- description: "List wiki pages in a GitLab project",
973
- inputSchema: toJSONSchema(ListWikiPagesSchema),
974
- },
975
- {
976
- name: "get_wiki_page",
977
- description: "Get details of a specific wiki page",
978
- inputSchema: toJSONSchema(GetWikiPageSchema),
979
- },
980
- {
981
- name: "create_wiki_page",
982
- description: "Create a new wiki page in a GitLab project",
983
- inputSchema: toJSONSchema(CreateWikiPageSchema),
984
- },
985
- {
986
- name: "update_wiki_page",
987
- description: "Update an existing wiki page in a GitLab project",
988
- inputSchema: toJSONSchema(UpdateWikiPageSchema),
989
- },
990
- {
991
- name: "delete_wiki_page",
992
- description: "Delete a wiki page from a GitLab project",
993
- inputSchema: toJSONSchema(DeleteWikiPageSchema),
994
- },
995
- {
996
- name: "list_group_wiki_pages",
997
- description: "List wiki pages in a GitLab group",
998
- inputSchema: toJSONSchema(ListGroupWikiPagesSchema),
999
- },
1000
- {
1001
- name: "get_group_wiki_page",
1002
- description: "Get details of a specific group wiki page",
1003
- inputSchema: toJSONSchema(GetGroupWikiPageSchema),
1004
- },
1005
- {
1006
- name: "create_group_wiki_page",
1007
- description: "Create a new wiki page in a GitLab group",
1008
- inputSchema: toJSONSchema(CreateGroupWikiPageSchema),
1009
- },
1010
- {
1011
- name: "update_group_wiki_page",
1012
- description: "Update an existing wiki page in a GitLab group",
1013
- inputSchema: toJSONSchema(UpdateGroupWikiPageSchema),
1014
- },
1015
- {
1016
- name: "delete_group_wiki_page",
1017
- description: "Delete a wiki page from a GitLab group",
1018
- inputSchema: toJSONSchema(DeleteGroupWikiPageSchema),
1019
- },
1020
- {
1021
- name: "get_repository_tree",
1022
- description: "Get the repository tree for a GitLab project (list files and directories)",
1023
- inputSchema: toJSONSchema(GetRepositoryTreeSchema),
1024
- },
1025
- {
1026
- name: "list_pipelines",
1027
- description: "List pipelines in a GitLab project with filtering options",
1028
- inputSchema: toJSONSchema(ListPipelinesSchema),
1029
- },
1030
- {
1031
- name: "get_pipeline",
1032
- description: "Get details of a specific pipeline in a GitLab project",
1033
- inputSchema: toJSONSchema(GetPipelineSchema),
1034
- },
1035
- {
1036
- name: "list_deployments",
1037
- description: "List deployments in a GitLab project with filtering options",
1038
- inputSchema: toJSONSchema(ListDeploymentsSchema),
1039
- },
1040
- {
1041
- name: "get_deployment",
1042
- description: "Get details of a specific deployment in a GitLab project",
1043
- inputSchema: toJSONSchema(GetDeploymentSchema),
1044
- },
1045
- {
1046
- name: "list_environments",
1047
- description: "List environments in a GitLab project",
1048
- inputSchema: toJSONSchema(ListEnvironmentsSchema),
1049
- },
1050
- {
1051
- name: "get_environment",
1052
- description: "Get details of a specific environment in a GitLab project",
1053
- inputSchema: toJSONSchema(GetEnvironmentSchema),
1054
- },
1055
- {
1056
- name: "list_pipeline_jobs",
1057
- description: "List all jobs in a specific pipeline",
1058
- inputSchema: toJSONSchema(ListPipelineJobsSchema),
1059
- },
1060
- {
1061
- name: "list_pipeline_trigger_jobs",
1062
- description: "List all trigger jobs (bridges) in a specific pipeline that trigger downstream pipelines",
1063
- inputSchema: toJSONSchema(ListPipelineTriggerJobsSchema),
1064
- },
1065
- {
1066
- name: "get_pipeline_job",
1067
- description: "Get details of a GitLab pipeline job number",
1068
- inputSchema: toJSONSchema(GetPipelineJobOutputSchema),
1069
- },
1070
- {
1071
- name: "get_pipeline_job_output",
1072
- description: "Get the output/trace of a GitLab pipeline job with optional pagination to limit context window usage",
1073
- inputSchema: toJSONSchema(GetPipelineJobOutputSchema),
1074
- },
1075
- {
1076
- name: "create_pipeline",
1077
- description: "Create a new pipeline for a branch or tag",
1078
- inputSchema: toJSONSchema(CreatePipelineSchema),
1079
- },
1080
- {
1081
- name: "retry_pipeline",
1082
- description: "Retry a failed or canceled pipeline",
1083
- inputSchema: toJSONSchema(RetryPipelineSchema),
1084
- },
1085
- {
1086
- name: "cancel_pipeline",
1087
- description: "Cancel a running pipeline",
1088
- inputSchema: toJSONSchema(CancelPipelineSchema),
1089
- },
1090
- {
1091
- name: "play_pipeline_job",
1092
- description: "Run a manual pipeline job",
1093
- inputSchema: toJSONSchema(PlayPipelineJobSchema),
1094
- },
1095
- {
1096
- name: "retry_pipeline_job",
1097
- description: "Retry a failed or canceled pipeline job",
1098
- inputSchema: toJSONSchema(RetryPipelineJobSchema),
1099
- },
1100
- {
1101
- name: "cancel_pipeline_job",
1102
- description: "Cancel a running pipeline job",
1103
- inputSchema: toJSONSchema(CancelPipelineJobSchema),
1104
- },
1105
- {
1106
- name: "list_job_artifacts",
1107
- description: "List artifact files in a job's artifacts archive. Returns file names, paths, types, and sizes.",
1108
- inputSchema: toJSONSchema(ListJobArtifactsSchema),
1109
- },
1110
- {
1111
- name: "download_job_artifacts",
1112
- description: "Download the entire artifact archive (zip) for a job to a local path. Returns the saved file path.",
1113
- inputSchema: toJSONSchema(DownloadJobArtifactsSchema),
1114
- },
1115
- {
1116
- name: "get_job_artifact_file",
1117
- description: "Get the content of a single file from a job's artifacts by its path within the archive",
1118
- inputSchema: toJSONSchema(GetJobArtifactFileSchema),
1119
- },
1120
- {
1121
- name: "list_merge_requests",
1122
- 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.",
1123
- inputSchema: toJSONSchema(ListMergeRequestsSchema),
1124
- },
1125
- {
1126
- name: "list_milestones",
1127
- description: "List milestones in a GitLab project with filtering options",
1128
- inputSchema: toJSONSchema(ListProjectMilestonesSchema),
1129
- },
1130
- {
1131
- name: "get_milestone",
1132
- description: "Get details of a specific milestone",
1133
- inputSchema: toJSONSchema(GetProjectMilestoneSchema),
1134
- },
1135
- {
1136
- name: "create_milestone",
1137
- description: "Create a new milestone in a GitLab project",
1138
- inputSchema: toJSONSchema(CreateProjectMilestoneSchema),
1139
- },
1140
- {
1141
- name: "edit_milestone",
1142
- description: "Edit an existing milestone in a GitLab project",
1143
- inputSchema: toJSONSchema(EditProjectMilestoneSchema),
1144
- },
1145
- {
1146
- name: "delete_milestone",
1147
- description: "Delete a milestone from a GitLab project",
1148
- inputSchema: toJSONSchema(DeleteProjectMilestoneSchema),
1149
- },
1150
- {
1151
- name: "get_milestone_issue",
1152
- description: "Get issues associated with a specific milestone",
1153
- inputSchema: toJSONSchema(GetMilestoneIssuesSchema),
1154
- },
1155
- {
1156
- name: "get_milestone_merge_requests",
1157
- description: "Get merge requests associated with a specific milestone",
1158
- inputSchema: toJSONSchema(GetMilestoneMergeRequestsSchema),
1159
- },
1160
- {
1161
- name: "promote_milestone",
1162
- description: "Promote a milestone to the next stage",
1163
- inputSchema: toJSONSchema(PromoteProjectMilestoneSchema),
1164
- },
1165
- {
1166
- name: "get_milestone_burndown_events",
1167
- description: "Get burndown events for a specific milestone",
1168
- inputSchema: toJSONSchema(GetMilestoneBurndownEventsSchema),
1169
- },
1170
- {
1171
- name: "get_users",
1172
- description: "Get GitLab user details by usernames",
1173
- inputSchema: toJSONSchema(GetUsersSchema),
1174
- },
1175
- {
1176
- name: "list_commits",
1177
- description: "List repository commits with filtering options",
1178
- inputSchema: toJSONSchema(ListCommitsSchema),
1179
- },
1180
- {
1181
- name: "get_commit",
1182
- description: "Get details of a specific commit",
1183
- inputSchema: toJSONSchema(GetCommitSchema),
1184
- },
1185
- {
1186
- name: "get_commit_diff",
1187
- description: "Get changes/diffs of a specific commit",
1188
- inputSchema: toJSONSchema(GetCommitDiffSchema),
1189
- },
1190
- {
1191
- name: "list_group_iterations",
1192
- description: "List group iterations with filtering options",
1193
- inputSchema: toJSONSchema(ListGroupIterationsSchema),
1194
- },
1195
- {
1196
- name: "upload_markdown",
1197
- description: "Upload a file to a GitLab project for use in markdown content",
1198
- inputSchema: toJSONSchema(MarkdownUploadSchema),
1199
- },
1200
- {
1201
- name: "download_attachment",
1202
- 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.",
1203
- inputSchema: toJSONSchema(DownloadAttachmentSchema),
1204
- },
1205
- {
1206
- name: "list_events",
1207
- description: "List all events for the currently authenticated user. Note: before/after parameters accept date format YYYY-MM-DD only",
1208
- inputSchema: toJSONSchema(ListEventsSchema),
1209
- },
1210
- {
1211
- name: "get_project_events",
1212
- description: "List all visible events for a specified project. Note: before/after parameters accept date format YYYY-MM-DD only",
1213
- inputSchema: toJSONSchema(GetProjectEventsSchema),
1214
- },
1215
- {
1216
- name: "list_releases",
1217
- description: "List all releases for a project",
1218
- inputSchema: toJSONSchema(ListReleasesSchema),
1219
- },
1220
- {
1221
- name: "get_release",
1222
- description: "Get a release by tag name",
1223
- inputSchema: toJSONSchema(GetReleaseSchema),
1224
- },
1225
- {
1226
- name: "create_release",
1227
- description: "Create a new release in a GitLab project",
1228
- inputSchema: toJSONSchema(CreateReleaseSchema),
1229
- },
1230
- {
1231
- name: "update_release",
1232
- description: "Update an existing release in a GitLab project",
1233
- inputSchema: toJSONSchema(UpdateReleaseSchema),
1234
- },
1235
- {
1236
- name: "delete_release",
1237
- description: "Delete a release from a GitLab project (does not delete the associated tag)",
1238
- inputSchema: toJSONSchema(DeleteReleaseSchema),
1239
- },
1240
- {
1241
- name: "create_release_evidence",
1242
- description: "Create release evidence for an existing release (GitLab Premium/Ultimate only)",
1243
- inputSchema: toJSONSchema(CreateReleaseEvidenceSchema),
1244
- },
1245
- {
1246
- name: "download_release_asset",
1247
- description: "Download a release asset file by direct asset path",
1248
- inputSchema: toJSONSchema(DownloadReleaseAssetSchema),
1249
- },
1250
- // --- Work item tools (GraphQL-based) ---
1251
- {
1252
- name: "get_work_item",
1253
- description: "Get a single work item with full details including status, hierarchy (parent/children), type, labels, assignees, and all widgets.",
1254
- inputSchema: toJSONSchema(GetWorkItemSchema),
1255
- },
1256
- {
1257
- name: "list_work_items",
1258
- description: "List work items in a project with filters (type, state, search, assignees, labels). Returns items with status and hierarchy info.",
1259
- inputSchema: toJSONSchema(ListWorkItemsSchema),
1260
- },
1261
- {
1262
- name: "create_work_item",
1263
- 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.",
1264
- inputSchema: toJSONSchema(CreateWorkItemSchema),
1265
- },
1266
- {
1267
- name: "update_work_item",
1268
- 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.",
1269
- inputSchema: toJSONSchema(UpdateWorkItemSchema),
1270
- },
1271
- {
1272
- name: "convert_work_item_type",
1273
- description: "Convert a work item to a different type (e.g. issue to task, task to incident).",
1274
- inputSchema: toJSONSchema(ConvertWorkItemTypeSchema),
1275
- },
1276
- {
1277
- name: "list_work_item_statuses",
1278
- description: "List available statuses for a work item type in a project. Requires GitLab Premium/Ultimate with configurable statuses.",
1279
- inputSchema: toJSONSchema(ListWorkItemStatusesSchema),
1280
- },
1281
- {
1282
- name: "list_custom_field_definitions",
1283
- 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.",
1284
- inputSchema: toJSONSchema(ListCustomFieldDefinitionsSchema),
1285
- },
1286
- {
1287
- name: "move_work_item",
1288
- description: "Move a work item (issue, task, etc.) to a different project. Uses GitLab GraphQL issueMove mutation.",
1289
- inputSchema: toJSONSchema(MoveWorkItemSchema),
1290
- },
1291
- {
1292
- name: "list_work_item_notes",
1293
- description: "List notes and discussions on a work item. Returns threaded discussions with author, body, timestamps, and system/internal flags.",
1294
- inputSchema: toJSONSchema(ListWorkItemNotesSchema),
1295
- },
1296
- {
1297
- name: "create_work_item_note",
1298
- description: "Add a note/comment to a work item. Supports Markdown, internal notes, and threaded replies.",
1299
- inputSchema: toJSONSchema(CreateWorkItemNoteSchema),
1300
- },
1301
- // --- Incident timeline event tools ---
1302
- {
1303
- name: "get_timeline_events",
1304
- description: "List timeline events for an incident. Returns chronological events with notes, timestamps, and tags (Start time, End time, Impact detected, etc.).",
1305
- inputSchema: toJSONSchema(GetTimelineEventsSchema),
1306
- },
1307
- {
1308
- name: "create_timeline_event",
1309
- description: "Create a timeline event on an incident. Supports tags: 'Start time', 'End time', 'Impact detected', 'Response initiated', 'Impact mitigated', 'Cause identified'.",
1310
- inputSchema: toJSONSchema(CreateTimelineEventSchema),
1311
- },
1312
- {
1313
- name: "list_webhooks",
1314
- description: "List all configured webhooks for a GitLab project or group. Provide either project_id or group_id.",
1315
- inputSchema: toJSONSchema(ListWebhooksSchema),
1316
- },
1317
- {
1318
- name: "list_webhook_events",
1319
- 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.",
1320
- inputSchema: toJSONSchema(ListWebhookEventsSchema),
1321
- },
1322
- {
1323
- name: "get_webhook_event",
1324
- description: "Get full details of a specific webhook event by ID, including request/response payloads. Searches up to 500 most recent events.",
1325
- inputSchema: toJSONSchema(GetWebhookEventSchema),
1326
- },
1327
- {
1328
- name: "search_code",
1329
- 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.",
1330
- inputSchema: toJSONSchema(SearchCodeSchema),
1331
- },
1332
- {
1333
- name: "search_project_code",
1334
- 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.",
1335
- inputSchema: toJSONSchema(SearchProjectCodeSchema),
1336
- },
1337
- {
1338
- name: "search_group_code",
1339
- 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.",
1340
- inputSchema: toJSONSchema(SearchGroupCodeSchema),
1341
- },
1342
- ];
1343
- // Define which tools are read-only
1344
- const readOnlyTools = new Set([
1345
- "search_repositories",
1346
- "search_code",
1347
- "search_project_code",
1348
- "search_group_code",
1349
- "execute_graphql",
1350
- "get_file_contents",
1351
- "get_merge_request",
1352
- "get_merge_request_diffs",
1353
- "list_merge_request_changed_files",
1354
- "list_merge_request_diffs",
1355
- "get_merge_request_file_diff",
1356
- "list_merge_request_versions",
1357
- "get_merge_request_version",
1358
- "get_branch_diffs",
1359
- "get_merge_request_note",
1360
- "get_merge_request_notes",
1361
- "get_draft_note",
1362
- "list_draft_notes",
1363
- "mr_discussions",
1364
- "list_issues",
1365
- "my_issues",
1366
- "list_merge_requests",
1367
- "get_issue",
1368
- "list_issue_links",
1369
- "list_issue_discussions",
1370
- "get_issue_link",
1371
- "list_namespaces",
1372
- "get_namespace",
1373
- "verify_namespace",
1374
- "get_project",
1375
- "list_projects",
1376
- "list_project_members",
1377
- "get_pipeline",
1378
- "list_pipelines",
1379
- "list_deployments",
1380
- "get_deployment",
1381
- "list_environments",
1382
- "get_environment",
1383
- "list_pipeline_jobs",
1384
- "list_pipeline_trigger_jobs",
1385
- "get_pipeline_job",
1386
- "get_pipeline_job_output",
1387
- "list_job_artifacts",
1388
- "download_job_artifacts",
1389
- "get_job_artifact_file",
1390
- "list_labels",
1391
- "get_label",
1392
- "list_group_projects",
1393
- "get_repository_tree",
1394
- "list_milestones",
1395
- "get_milestone",
1396
- "get_milestone_issue",
1397
- "get_milestone_merge_requests",
1398
- "get_milestone_burndown_events",
1399
- "list_wiki_pages",
1400
- "get_wiki_page",
1401
- "list_group_wiki_pages",
1402
- "get_group_wiki_page",
1403
- "get_users",
1404
- "list_commits",
1405
- "get_commit",
1406
- "get_commit_diff",
1407
- "list_group_iterations",
1408
- "get_group_iteration",
1409
- "download_attachment",
1410
- "list_events",
1411
- "get_project_events",
1412
- "list_releases",
1413
- "get_release",
1414
- "download_release_asset",
1415
- "get_merge_request_approval_state",
1416
- "get_work_item",
1417
- "list_work_items",
1418
- "list_work_item_statuses",
1419
- "list_custom_field_definitions",
1420
- "list_work_item_notes",
1421
- "get_timeline_events",
1422
- "get_merge_request_conflicts",
1423
- "list_webhooks",
1424
- "list_webhook_events",
1425
- "get_webhook_event",
1426
- ]);
1427
- // Define which tools are related to wiki and can be toggled by USE_GITLAB_WIKI
1428
- const wikiToolNames = new Set([
1429
- "list_wiki_pages",
1430
- "get_wiki_page",
1431
- "create_wiki_page",
1432
- "update_wiki_page",
1433
- "delete_wiki_page",
1434
- "list_group_wiki_pages",
1435
- "get_group_wiki_page",
1436
- "create_group_wiki_page",
1437
- "update_group_wiki_page",
1438
- "delete_group_wiki_page",
1439
- "upload_wiki_attachment",
1440
- ]);
1441
- // Define which tools are related to milestones and can be toggled by USE_MILESTONE
1442
- const milestoneToolNames = new Set([
1443
- "list_milestones",
1444
- "get_milestone",
1445
- "create_milestone",
1446
- "edit_milestone",
1447
- "delete_milestone",
1448
- "get_milestone_issue",
1449
- "get_milestone_merge_requests",
1450
- "promote_milestone",
1451
- "get_milestone_burndown_events",
1452
- ]);
1453
- // Define which tools are related to pipelines and can be toggled by USE_PIPELINE
1454
- const pipelineToolNames = new Set([
1455
- "list_pipelines",
1456
- "get_pipeline",
1457
- "list_deployments",
1458
- "get_deployment",
1459
- "list_environments",
1460
- "get_environment",
1461
- "list_pipeline_jobs",
1462
- "list_pipeline_trigger_jobs",
1463
- "get_pipeline_job",
1464
- "get_pipeline_job_output",
1465
- "create_pipeline",
1466
- "retry_pipeline",
1467
- "cancel_pipeline",
1468
- "play_pipeline_job",
1469
- "retry_pipeline_job",
1470
- "cancel_pipeline_job",
1471
- "list_job_artifacts",
1472
- "download_job_artifacts",
1473
- "get_job_artifact_file",
1474
- ]);
1475
- const TOOLSET_DEFINITIONS = [
1476
- {
1477
- id: "merge_requests",
1478
- isDefault: true,
1479
- tools: new Set([
1480
- "merge_merge_request",
1481
- "approve_merge_request",
1482
- "unapprove_merge_request",
1483
- "get_merge_request_approval_state",
1484
- "get_merge_request_conflicts",
1485
- "get_merge_request",
1486
- "get_merge_request_diffs",
1487
- "list_merge_request_changed_files",
1488
- "list_merge_request_diffs",
1489
- "get_merge_request_file_diff",
1490
- "list_merge_request_versions",
1491
- "get_merge_request_version",
1492
- "update_merge_request",
1493
- "create_merge_request",
1494
- "list_merge_requests",
1495
- "get_branch_diffs",
1496
- "mr_discussions",
1497
- "create_merge_request_note",
1498
- "update_merge_request_note",
1499
- "delete_merge_request_note",
1500
- "get_merge_request_note",
1501
- "get_merge_request_notes",
1502
- "delete_merge_request_discussion_note",
1503
- "update_merge_request_discussion_note",
1504
- "create_merge_request_discussion_note",
1505
- "get_draft_note",
1506
- "list_draft_notes",
1507
- "create_draft_note",
1508
- "update_draft_note",
1509
- "delete_draft_note",
1510
- "publish_draft_note",
1511
- "bulk_publish_draft_notes",
1512
- "create_merge_request_thread",
1513
- "resolve_merge_request_thread",
1514
- ]),
1515
- },
1516
- {
1517
- id: "issues",
1518
- isDefault: true,
1519
- tools: new Set([
1520
- "create_issue",
1521
- "list_issues",
1522
- "my_issues",
1523
- "get_issue",
1524
- "update_issue",
1525
- "delete_issue",
1526
- "create_issue_note",
1527
- "update_issue_note",
1528
- "list_issue_links",
1529
- "list_issue_discussions",
1530
- "get_issue_link",
1531
- "create_issue_link",
1532
- "delete_issue_link",
1533
- "create_note",
1534
- ]),
1535
- },
1536
- {
1537
- id: "repositories",
1538
- isDefault: true,
1539
- tools: new Set([
1540
- "search_repositories",
1541
- "create_repository",
1542
- "get_file_contents",
1543
- "push_files",
1544
- "create_or_update_file",
1545
- "fork_repository",
1546
- "get_repository_tree",
1547
- ]),
1548
- },
1549
- {
1550
- id: "branches",
1551
- isDefault: true,
1552
- tools: new Set([
1553
- "create_branch",
1554
- "list_commits",
1555
- "get_commit",
1556
- "get_commit_diff",
1557
- ]),
1558
- },
1559
- {
1560
- id: "projects",
1561
- isDefault: true,
1562
- tools: new Set([
1563
- "get_project",
1564
- "list_projects",
1565
- "list_project_members",
1566
- "list_namespaces",
1567
- "get_namespace",
1568
- "verify_namespace",
1569
- "list_group_projects",
1570
- "list_group_iterations",
1571
- ]),
1572
- },
1573
- {
1574
- id: "labels",
1575
- isDefault: true,
1576
- tools: new Set([
1577
- "list_labels",
1578
- "get_label",
1579
- "create_label",
1580
- "update_label",
1581
- "delete_label",
1582
- ]),
1583
- },
1584
- {
1585
- id: "pipelines",
1586
- isDefault: true,
1587
- tools: new Set([
1588
- "list_pipelines",
1589
- "get_pipeline",
1590
- "list_deployments",
1591
- "get_deployment",
1592
- "list_environments",
1593
- "get_environment",
1594
- "list_pipeline_jobs",
1595
- "list_pipeline_trigger_jobs",
1596
- "get_pipeline_job",
1597
- "get_pipeline_job_output",
1598
- "create_pipeline",
1599
- "retry_pipeline",
1600
- "cancel_pipeline",
1601
- "play_pipeline_job",
1602
- "retry_pipeline_job",
1603
- "cancel_pipeline_job",
1604
- "list_job_artifacts",
1605
- "download_job_artifacts",
1606
- "get_job_artifact_file",
1607
- ]),
1608
- },
1609
- {
1610
- id: "milestones",
1611
- isDefault: true,
1612
- tools: new Set([
1613
- "list_milestones",
1614
- "get_milestone",
1615
- "create_milestone",
1616
- "edit_milestone",
1617
- "delete_milestone",
1618
- "get_milestone_issue",
1619
- "get_milestone_merge_requests",
1620
- "promote_milestone",
1621
- "get_milestone_burndown_events",
1622
- ]),
1623
- },
1624
- {
1625
- id: "wiki",
1626
- isDefault: true,
1627
- tools: new Set([
1628
- "list_wiki_pages",
1629
- "get_wiki_page",
1630
- "create_wiki_page",
1631
- "update_wiki_page",
1632
- "delete_wiki_page",
1633
- "list_group_wiki_pages",
1634
- "get_group_wiki_page",
1635
- "create_group_wiki_page",
1636
- "update_group_wiki_page",
1637
- "delete_group_wiki_page",
1638
- ]),
1639
- },
1640
- {
1641
- id: "releases",
1642
- isDefault: true,
1643
- tools: new Set([
1644
- "list_releases",
1645
- "get_release",
1646
- "create_release",
1647
- "update_release",
1648
- "delete_release",
1649
- "create_release_evidence",
1650
- "download_release_asset",
1651
- ]),
1652
- },
1653
- {
1654
- id: "users",
1655
- isDefault: true,
1656
- tools: new Set([
1657
- "get_users",
1658
- "list_events",
1659
- "get_project_events",
1660
- "upload_markdown",
1661
- "download_attachment",
1662
- ]),
1663
- },
1664
- {
1665
- id: "workitems",
1666
- isDefault: false,
1667
- tools: new Set([
1668
- "get_work_item",
1669
- "list_work_items",
1670
- "create_work_item",
1671
- "update_work_item",
1672
- "convert_work_item_type",
1673
- "list_work_item_statuses",
1674
- "list_custom_field_definitions",
1675
- "move_work_item",
1676
- "list_work_item_notes",
1677
- "create_work_item_note",
1678
- "get_timeline_events",
1679
- "create_timeline_event",
1680
- ]),
1681
- },
1682
- {
1683
- id: "webhooks",
1684
- isDefault: false,
1685
- tools: new Set([
1686
- "list_webhooks",
1687
- "list_webhook_events",
1688
- "get_webhook_event",
1689
- ]),
1690
- },
1691
- {
1692
- id: "search",
1693
- isDefault: false,
1694
- tools: new Set(["search_code", "search_project_code", "search_group_code"]),
1695
- },
1696
- ];
1697
- // Derived lookup: tool name → toolset ID
1698
- const TOOLSET_BY_TOOL_NAME = new Map();
1699
- for (const def of TOOLSET_DEFINITIONS) {
1700
- for (const tool of def.tools) {
1701
- if (TOOLSET_BY_TOOL_NAME.has(tool)) {
1702
- logger.warn(`Tool "${tool}" is defined in multiple toolsets: "${TOOLSET_BY_TOOL_NAME.get(tool)}" and "${def.id}"`);
1703
- }
1704
- TOOLSET_BY_TOOL_NAME.set(tool, def.id);
1705
- }
1706
- }
1707
- const DEFAULT_TOOLSET_IDS = new Set(TOOLSET_DEFINITIONS.filter(d => d.isDefault).map(d => d.id));
1708
- const ALL_TOOLSET_IDS = new Set(TOOLSET_DEFINITIONS.map(d => d.id));
1709
- function parseEnabledToolsets(raw) {
1710
- if (!raw || raw.trim() === "") {
1711
- return DEFAULT_TOOLSET_IDS;
1712
- }
1713
- const trimmed = raw.trim().toLowerCase();
1714
- if (trimmed === "all") {
1715
- return ALL_TOOLSET_IDS;
1716
- }
1717
- const selected = new Set(trimmed
1718
- .split(",")
1719
- .map(s => s.trim())
1720
- .filter((s) => ALL_TOOLSET_IDS.has(s)));
1721
- if (selected.size === 0) {
1722
- logger.warn(`No valid toolsets found in configuration (${raw}). Falling back to default toolsets.`);
1723
- return DEFAULT_TOOLSET_IDS;
1724
- }
1725
- return selected;
1726
- }
1727
- function parseIndividualTools(raw) {
1728
- if (!raw || raw.trim() === "") {
1729
- return new Set();
1730
- }
1731
- const allToolNames = new Set(allTools.map((t) => t.name));
1732
- const parsed = raw
1733
- .trim()
1734
- .split(",")
1735
- .map(s => s.trim().toLowerCase())
1736
- .filter(Boolean);
1737
- const unknown = parsed.filter(name => !allToolNames.has(name));
1738
- if (unknown.length > 0) {
1739
- logger.warn(`Unknown tool names in GITLAB_TOOLS (will be ignored): ${unknown.join(", ")}`);
1740
- }
1741
- return new Set(parsed);
1742
- }
1743
- function buildFeatureFlagOverrides() {
1744
- const overrides = new Set();
1745
- if (USE_GITLAB_WIKI) {
1746
- for (const t of wikiToolNames)
1747
- overrides.add(t);
1748
- }
1749
- if (USE_MILESTONE) {
1750
- for (const t of milestoneToolNames)
1751
- overrides.add(t);
1752
- }
1753
- if (USE_PIPELINE) {
1754
- for (const t of pipelineToolNames)
1755
- overrides.add(t);
1756
- }
1757
- return overrides;
1758
- }
1759
- function isToolInEnabledToolset(toolName, enabledToolsets) {
1760
- const toolsetId = TOOLSET_BY_TOOL_NAME.get(toolName);
1761
- // Tools not in any toolset (e.g. execute_graphql) are excluded by default
1762
- if (toolsetId === undefined)
1763
- return false;
1764
- return enabledToolsets.has(toolsetId);
1765
- }
1766
686
  // Compute at startup
1767
687
  const enabledToolsets = parseEnabledToolsets(GITLAB_TOOLSETS_RAW);
1768
688
  const individuallyEnabledTools = parseIndividualTools(GITLAB_TOOLS_RAW);
@@ -1773,25 +693,6 @@ if (GITLAB_TOOLSETS_RAW && (USE_PIPELINE || USE_MILESTONE || USE_GITLAB_WIKI)) {
1773
693
  "Legacy flags add tools additively on top of the toolset selection and may produce unexpected results.");
1774
694
  }
1775
695
  const MERGE_REQUEST_DEPLOYMENT_SUMMARY_LIMIT = 10;
1776
- /**
1777
- * Smart URL handling for GitLab API
1778
- *
1779
- * @param {string | undefined} url - Input GitLab API URL
1780
- * @returns {string} Normalized GitLab API URL with /api/v4 path
1781
- */
1782
- function normalizeGitLabApiUrl(url) {
1783
- if (!url) {
1784
- return "https://gitlab.com/api/v4";
1785
- }
1786
- let normalizedUrl = url.trim();
1787
- if (normalizedUrl.endsWith("/")) {
1788
- normalizedUrl = normalizedUrl.slice(0, -1);
1789
- }
1790
- if (!normalizedUrl.endsWith("/api/v4")) {
1791
- normalizedUrl = `${normalizedUrl}/api/v4`;
1792
- }
1793
- return normalizedUrl;
1794
- }
1795
696
  // Use the normalizeGitLabApiUrl function to handle various URL formats
1796
697
  const GITLAB_API_URLS = (getConfig("api-url", "GITLAB_API_URL") || "https://gitlab.com")
1797
698
  .split(",")
@@ -1833,7 +734,7 @@ if (GITLAB_MCP_OAUTH) {
1833
734
  logger.error("Set STREAMABLE_HTTP=true to enable MCP OAuth");
1834
735
  process.exit(1);
1835
736
  }
1836
- logger.info("MCP OAuth enabled: GitLab OAuth proxy active");
737
+ logger.info("MCP OAuth enabled: GitLab OAuth proxy active (Private-Token/JOB-TOKEN headers bypass OAuth)");
1837
738
  }
1838
739
  if (!REMOTE_AUTHORIZATION && !GITLAB_MCP_OAUTH && !USE_OAUTH && !GITLAB_PERSONAL_ACCESS_TOKEN && !GITLAB_JOB_TOKEN && !GITLAB_AUTH_COOKIE_PATH) {
1839
740
  // Standard mode: token must be in environment (unless using OAuth)
@@ -2304,23 +1205,6 @@ async function convertIssueType(projectId, issueIid, newType) {
2304
1205
  };
2305
1206
  }
2306
1207
  // --- Work item hierarchy ---
2307
- /**
2308
- * Set a parent for a work item (issue hierarchy).
2309
- */
2310
- async function setIssueParent(projectId, issueIid, parentProjectId, parentIssueIid) {
2311
- const { workItemGID } = await resolveWorkItemGID(projectId, issueIid);
2312
- const { workItemGID: parentGID } = await resolveWorkItemGID(parentProjectId, parentIssueIid);
2313
- const data = await executeGraphQL(`mutation($id: WorkItemID!, $parentId: WorkItemID!) {
2314
- workItemUpdate(input: { id: $id, hierarchyWidget: { parentId: $parentId } }) {
2315
- workItem { id }
2316
- errors
2317
- }
2318
- }`, { id: workItemGID, parentId: parentGID });
2319
- if (data.workItemUpdate.errors?.length > 0) {
2320
- throw new Error(`Failed to set parent: ${data.workItemUpdate.errors.join(", ")}`);
2321
- }
2322
- return { id: workItemGID, parentId: parentGID };
2323
- }
2324
1208
  /**
2325
1209
  * Remove the parent from a work item.
2326
1210
  */
@@ -2336,83 +1220,6 @@ async function removeIssueParent(projectId, issueIid) {
2336
1220
  throw new Error(`Failed to remove parent: ${data.workItemUpdate.errors.join(", ")}`);
2337
1221
  }
2338
1222
  }
2339
- /**
2340
- * List children of a work item (hierarchy widget).
2341
- */
2342
- async function listIssueChildren(projectId, issueIid) {
2343
- projectId = decodeURIComponent(projectId);
2344
- const effectiveProjectId = getEffectiveProjectId(projectId);
2345
- // Get project path
2346
- const projectUrl = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}`);
2347
- const projectResponse = await fetch(projectUrl.toString(), {
2348
- ...getFetchConfig(),
2349
- });
2350
- await handleGitLabError(projectResponse);
2351
- const project = await projectResponse.json();
2352
- const data = await executeGraphQL(`query($path: ID!, $iid: String!) {
2353
- namespace(fullPath: $path) {
2354
- workItem(iid: $iid) {
2355
- id
2356
- title
2357
- widgets {
2358
- __typename
2359
- ... on WorkItemWidgetHierarchy {
2360
- parent {
2361
- id
2362
- title
2363
- webUrl
2364
- workItemType { name }
2365
- }
2366
- children {
2367
- nodes {
2368
- id
2369
- title
2370
- state
2371
- webUrl
2372
- workItemType { name }
2373
- }
2374
- }
2375
- }
2376
- }
2377
- }
2378
- }
2379
- }`, { path: project.path_with_namespace, iid: String(issueIid) });
2380
- if (!data.namespace?.workItem) {
2381
- throw new Error(`Work item #${issueIid} not found`);
2382
- }
2383
- // Extract hierarchy widget
2384
- const hierarchyWidget = data.namespace.workItem.widgets?.find((w) => w.__typename === "WorkItemWidgetHierarchy");
2385
- return {
2386
- id: data.namespace.workItem.id,
2387
- title: data.namespace.workItem.title,
2388
- parent: hierarchyWidget?.parent || null,
2389
- children: hierarchyWidget?.children?.nodes || [],
2390
- };
2391
- }
2392
- /**
2393
- * Add a child to a parent work item.
2394
- */
2395
- async function addIssueChild(projectId, issueIid, childProjectId, childIssueIid) {
2396
- const { workItemGID: parentGID } = await resolveWorkItemGID(projectId, issueIid);
2397
- const { workItemGID: childGID } = await resolveWorkItemGID(childProjectId, childIssueIid);
2398
- const data = await executeGraphQL(`mutation($id: WorkItemID!, $childId: WorkItemID!) {
2399
- workItemUpdate(input: { id: $id, hierarchyWidget: { childrenIds: [$childId] } }) {
2400
- workItem { id }
2401
- errors
2402
- }
2403
- }`, { id: parentGID, childId: childGID });
2404
- if (data.workItemUpdate.errors?.length > 0) {
2405
- throw new Error(`Failed to add child: ${data.workItemUpdate.errors.join(", ")}`);
2406
- }
2407
- return { parentId: parentGID, childId: childGID };
2408
- }
2409
- /**
2410
- * Remove a child from a parent work item by setting the child's parent to null.
2411
- */
2412
- async function removeIssueChild(projectId, issueIid, childProjectId, childIssueIid) {
2413
- // Removing a child is done by removing the parent from the child
2414
- await removeIssueParent(childProjectId, childIssueIid);
2415
- }
2416
1223
  // --- Work item status ---
2417
1224
  /**
2418
1225
  * List available statuses for a work item type in a project.
@@ -2808,35 +1615,6 @@ async function updateIncidentEscalationStatus(projectPath, incidentIid, status)
2808
1615
  }
2809
1616
  return data.issueSetEscalationStatus.issue;
2810
1617
  }
2811
- /**
2812
- * Set the status of a work item.
2813
- */
2814
- async function setIssueStatus(projectId, issueIid, status) {
2815
- const { workItemGID } = await resolveWorkItemGID(projectId, issueIid);
2816
- const data = await executeGraphQL(`mutation($id: WorkItemID!, $status: WorkItemsStatusesStatusID!) {
2817
- workItemUpdate(input: { id: $id, statusWidget: { status: $status } }) {
2818
- workItem {
2819
- id
2820
- widgets {
2821
- __typename
2822
- ... on WorkItemWidgetStatus {
2823
- status { id name category color }
2824
- }
2825
- }
2826
- }
2827
- errors
2828
- }
2829
- }`, { id: workItemGID, status });
2830
- if (data.workItemUpdate.errors?.length > 0) {
2831
- throw new Error(`Failed to set status: ${data.workItemUpdate.errors.join(", ")}`);
2832
- }
2833
- // Extract the current status from the response
2834
- const statusWidget = data.workItemUpdate.workItem?.widgets?.find((w) => w.__typename === "WorkItemWidgetStatus");
2835
- return {
2836
- id: data.workItemUpdate.workItem.id,
2837
- status: statusWidget?.status || null,
2838
- };
2839
- }
2840
1618
  /**
2841
1619
  * Resolve a project ID (numeric or path) to its full path_with_namespace.
2842
1620
  */
@@ -3188,7 +1966,8 @@ async function createWorkItem(projectId, options) {
3188
1966
  inputValues.push("labelsWidget: { labelIds: $labelIds }");
3189
1967
  variables.labelIds = labelIds;
3190
1968
  }
3191
- if (options.weight !== undefined) {
1969
+ // Incidents don't support the weight widget
1970
+ if (options.weight !== undefined && typeName !== "incident") {
3192
1971
  inputFields.push("$weight: Int");
3193
1972
  inputValues.push("weightWidget: { weight: $weight }");
3194
1973
  variables.weight = options.weight;
@@ -3447,7 +2226,7 @@ async function updateWorkItem(projectId, iid, options) {
3447
2226
  if (options.children_to_add && options.children_to_add.length > 0) {
3448
2227
  const childGIDs = [];
3449
2228
  for (const child of options.children_to_add) {
3450
- const { workItemGID: childGID } = await resolveWorkItemGID(child.project_id, child.iid);
2229
+ const { workItemGID: childGID } = await resolveWorkItemGID(child.project_id || projectId, child.iid);
3451
2230
  childGIDs.push(childGID);
3452
2231
  }
3453
2232
  const addData = await executeGraphQL(`mutation($id: WorkItemID!, $childrenIds: [WorkItemID!]!) {
@@ -3462,7 +2241,7 @@ async function updateWorkItem(projectId, iid, options) {
3462
2241
  // Handle children_to_remove: remove parent from each child
3463
2242
  if (options.children_to_remove && options.children_to_remove.length > 0) {
3464
2243
  for (const child of options.children_to_remove) {
3465
- await removeIssueParent(child.project_id, child.iid);
2244
+ await removeIssueParent(child.project_id || projectId, child.iid);
3466
2245
  }
3467
2246
  }
3468
2247
  // Handle linked_items_to_add: use workItemAddLinkedItems mutation
@@ -3473,7 +2252,7 @@ async function updateWorkItem(projectId, iid, options) {
3473
2252
  const linkType = item.link_type || "RELATED";
3474
2253
  if (!groupedByType[linkType])
3475
2254
  groupedByType[linkType] = [];
3476
- const { workItemGID: targetGID } = await resolveWorkItemGID(item.project_id, item.iid);
2255
+ const { workItemGID: targetGID } = await resolveWorkItemGID(item.project_id || projectId, item.iid);
3477
2256
  groupedByType[linkType].push(targetGID);
3478
2257
  }
3479
2258
  for (const [linkType, targetGIDs] of Object.entries(groupedByType)) {
@@ -3491,7 +2270,7 @@ async function updateWorkItem(projectId, iid, options) {
3491
2270
  if (options.linked_items_to_remove && options.linked_items_to_remove.length > 0) {
3492
2271
  const targetGIDs = [];
3493
2272
  for (const item of options.linked_items_to_remove) {
3494
- const { workItemGID: targetGID } = await resolveWorkItemGID(item.project_id, item.iid);
2273
+ const { workItemGID: targetGID } = await resolveWorkItemGID(item.project_id || projectId, item.iid);
3495
2274
  targetGIDs.push(targetGID);
3496
2275
  }
3497
2276
  const removeLinkedData = await executeGraphQL(`mutation($id: WorkItemID!, $workItemsIds: [WorkItemID!]!) {
@@ -3836,14 +2615,17 @@ async function updateIssueNote(projectId, issueIid, discussionId, noteId, body,
3836
2615
  * Create a note in an issue discussion
3837
2616
  * @param {string} projectId - The ID or URL-encoded path of the project
3838
2617
  * @param {number} issueIid - The IID of an issue
3839
- * @param {string} discussionId - The ID of a thread
2618
+ * @param {string} [discussionId] - The ID of a thread (omit for top-level note)
3840
2619
  * @param {string} body - The content of the new note
3841
2620
  * @param {string} [createdAt] - The creation date of the note (ISO 8601 format)
3842
2621
  * @returns {Promise<GitLabDiscussionNote>} The created note
3843
2622
  */
3844
2623
  async function createIssueNote(projectId, issueIid, discussionId, body, createdAt) {
3845
2624
  projectId = decodeURIComponent(projectId); // Decode project ID
3846
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${issueIid}/discussions/${discussionId}/notes`);
2625
+ const basePath = `${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${issueIid}`;
2626
+ const url = new URL(discussionId
2627
+ ? `${basePath}/discussions/${discussionId}/notes`
2628
+ : `${basePath}/notes`);
3847
2629
  const payload = { body };
3848
2630
  if (createdAt) {
3849
2631
  payload.created_at = createdAt;
@@ -4042,43 +2824,6 @@ async function createOrUpdateFile(projectId, filePath, content, commitMessage, b
4042
2824
  const data = await response.json();
4043
2825
  return GitLabCreateUpdateFileResponseSchema.parse(data);
4044
2826
  }
4045
- /**
4046
- * Create a tree structure in a GitLab project repository
4047
- * 저장소에 트리 구조 생성
4048
- *
4049
- * @param {string} projectId - The ID or URL-encoded path of the project
4050
- * @param {FileOperation[]} files - Array of file operations
4051
- * @param {string} [ref] - The name of the branch, tag or commit
4052
- * @returns {Promise<GitLabTree>} The created tree
4053
- */
4054
- async function createTree(projectId, files, ref) {
4055
- projectId = decodeURIComponent(projectId); // Decode project ID
4056
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/repository/tree`);
4057
- if (ref) {
4058
- url.searchParams.append("ref", ref);
4059
- }
4060
- const response = await fetch(url.toString(), {
4061
- ...getFetchConfig(),
4062
- method: "POST",
4063
- body: JSON.stringify({
4064
- files: files.map(file => ({
4065
- file_path: file.path,
4066
- content: encodeRepoFilePayloadContent(file.content),
4067
- encoding: GITLAB_REPO_FILE_ENCODING,
4068
- })),
4069
- }),
4070
- });
4071
- if (response.status === 400) {
4072
- const errorBody = await response.text();
4073
- throw new Error(`Invalid request: ${errorBody}`);
4074
- }
4075
- if (!response.ok) {
4076
- const errorBody = await response.text();
4077
- throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
4078
- }
4079
- const data = await response.json();
4080
- return GitLabTreeSchema.parse(data);
4081
- }
4082
2827
  /**
4083
2828
  * Create a commit in a GitLab project repository
4084
2829
  * 저장소에 커밋 생성
@@ -4300,18 +3045,6 @@ async function getProjectMergeMethod(projectId) {
4300
3045
  .parse(data).merge_method;
4301
3046
  return typeof mergeMethod === "string" ? mergeMethod : null;
4302
3047
  }
4303
- function estimateMergeCommitCount(mergeMethod, sourceCommitCount) {
4304
- if (sourceCommitCount === 0) {
4305
- return 0;
4306
- }
4307
- if (mergeMethod === "merge") {
4308
- return 1;
4309
- }
4310
- if (mergeMethod === "ff" || mergeMethod === "rebase_merge") {
4311
- return 0;
4312
- }
4313
- return null;
4314
- }
4315
3048
  async function buildMergeRequestCommitAdditionSummary(projectId, mergeRequest) {
4316
3049
  try {
4317
3050
  const sourceCommitCount = await getMergeRequestSourceCommitCount(projectId, mergeRequest.iid);
@@ -5161,100 +3894,6 @@ async function getMergeRequestVersion(projectId, mergeRequestIid, versionId, uni
5161
3894
  const data = await response.json();
5162
3895
  return GitLabMergeRequestVersionDetailSchema.parse(data);
5163
3896
  }
5164
- /**
5165
- * List all namespaces
5166
- * 사용 가능한 모든 네임스페이스 목록 조회
5167
- *
5168
- * @param {Object} options - Options for listing namespaces
5169
- * @param {string} [options.search] - Search query to filter namespaces
5170
- * @param {boolean} [options.owned_only] - Only return namespaces owned by the authenticated user
5171
- * @param {boolean} [options.top_level_only] - Only return top-level namespaces
5172
- * @returns {Promise<GitLabNamespace[]>} List of namespaces
5173
- */
5174
- async function listNamespaces(options) {
5175
- const url = new URL(`${getEffectiveApiUrl()}/namespaces`);
5176
- if (options.search) {
5177
- url.searchParams.append("search", options.search);
5178
- }
5179
- if (options.owned_only) {
5180
- url.searchParams.append("owned_only", "true");
5181
- }
5182
- if (options.top_level_only) {
5183
- url.searchParams.append("top_level_only", "true");
5184
- }
5185
- const response = await fetch(url.toString(), {
5186
- ...getFetchConfig(),
5187
- });
5188
- await handleGitLabError(response);
5189
- const data = await response.json();
5190
- return z.array(GitLabNamespaceSchema).parse(data);
5191
- }
5192
- /**
5193
- * Get details on a namespace
5194
- * 네임스페이스 상세 정보 조회
5195
- *
5196
- * @param {string} id - The ID or URL-encoded path of the namespace
5197
- * @returns {Promise<GitLabNamespace>} The namespace details
5198
- */
5199
- async function getNamespace(id) {
5200
- const url = new URL(`${getEffectiveApiUrl()}/namespaces/${encodeURIComponent(id)}`);
5201
- const response = await fetch(url.toString(), {
5202
- ...getFetchConfig(),
5203
- });
5204
- await handleGitLabError(response);
5205
- const data = await response.json();
5206
- return GitLabNamespaceSchema.parse(data);
5207
- }
5208
- /**
5209
- * Verify if a namespace exists
5210
- * 네임스페이스 존재 여부 확인
5211
- *
5212
- * @param {string} namespacePath - The path of the namespace to check
5213
- * @param {number} [parentId] - The ID of the parent namespace
5214
- * @returns {Promise<GitLabNamespaceExistsResponse>} The verification result
5215
- */
5216
- async function verifyNamespaceExistence(namespacePath, parentId) {
5217
- const url = new URL(`${getEffectiveApiUrl()}/namespaces/${encodeURIComponent(namespacePath)}/exists`);
5218
- if (parentId) {
5219
- url.searchParams.append("parent_id", parentId.toString());
5220
- }
5221
- const response = await fetch(url.toString(), {
5222
- ...getFetchConfig(),
5223
- });
5224
- await handleGitLabError(response);
5225
- const data = await response.json();
5226
- return GitLabNamespaceExistsResponseSchema.parse(data);
5227
- }
5228
- /**
5229
- * Get a single project
5230
- * 단일 프로젝트 조회
5231
- *
5232
- * @param {string} projectId - The ID or URL-encoded path of the project
5233
- * @param {Object} options - Options for getting project details
5234
- * @param {boolean} [options.license] - Include project license data
5235
- * @param {boolean} [options.statistics] - Include project statistics
5236
- * @param {boolean} [options.with_custom_attributes] - Include custom attributes in response
5237
- * @returns {Promise<GitLabProject>} Project details
5238
- */
5239
- async function getProject(projectId, options = {}) {
5240
- projectId = decodeURIComponent(projectId); // Decode project ID
5241
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}`);
5242
- if (options.license) {
5243
- url.searchParams.append("license", "true");
5244
- }
5245
- if (options.statistics) {
5246
- url.searchParams.append("statistics", "true");
5247
- }
5248
- if (options.with_custom_attributes) {
5249
- url.searchParams.append("with_custom_attributes", "true");
5250
- }
5251
- const response = await fetch(url.toString(), {
5252
- ...getFetchConfig(),
5253
- });
5254
- await handleGitLabError(response);
5255
- const data = await response.json();
5256
- return GitLabRepositorySchema.parse(data);
5257
- }
5258
3897
  /**
5259
3898
  * List projects
5260
3899
  * 프로젝트 목록 조회
@@ -5474,18 +4113,6 @@ async function listWebhooks(options) {
5474
4113
  await handleGitLabError(response);
5475
4114
  return (await response.json());
5476
4115
  }
5477
- /**
5478
- * Summarize webhook events by stripping heavy payload fields
5479
- */
5480
- function summarizeWebhookEvents(events) {
5481
- return events.map(event => ({
5482
- id: event.id,
5483
- url: event.url,
5484
- trigger: event.trigger,
5485
- response_status: event.response_status,
5486
- execution_duration: event.execution_duration,
5487
- }));
5488
- }
5489
4116
  /**
5490
4117
  * Fetch a single page of webhook events
5491
4118
  */
@@ -6870,37 +5497,6 @@ async function downloadReleaseAsset(projectId, tagName, directAssetPath) {
6870
5497
  }
6871
5498
  // Request handlers are now registered inside createServer() factory function
6872
5499
  // to ensure each transport connection gets its own Server instance (GHSA-345p-7cg4-v4c7).
6873
- /**
6874
- * Filter diffs by excluded file patterns
6875
- * Safely handles invalid regex patterns by logging and ignoring them
6876
- *
6877
- * @param diffs - Array of diff objects with new_path property
6878
- * @param excludedFilePatterns - Array of regex patterns to exclude
6879
- * @returns Filtered array of diffs
6880
- */
6881
- function filterDiffsByPatterns(diffs, excludedFilePatterns) {
6882
- if (!excludedFilePatterns?.length)
6883
- return diffs;
6884
- const regexPatterns = excludedFilePatterns
6885
- .map(pattern => {
6886
- try {
6887
- return new RegExp(pattern);
6888
- }
6889
- catch (e) {
6890
- console.warn(`Invalid regex pattern ignored: ${pattern}`);
6891
- return null;
6892
- }
6893
- })
6894
- .filter((regex) => regex !== null);
6895
- if (regexPatterns.length === 0)
6896
- return diffs;
6897
- const matchesAnyPattern = (path) => {
6898
- if (!path)
6899
- return false;
6900
- return regexPatterns.some(regex => regex.test(path));
6901
- };
6902
- return diffs.filter(diff => !matchesAnyPattern(diff.new_path));
6903
- }
6904
5500
  async function handleToolCall(params) {
6905
5501
  try {
6906
5502
  if (!params.arguments) {
@@ -6912,7 +5508,16 @@ async function handleToolCall(params) {
6912
5508
  }
6913
5509
  // Lazy OAuth token refresh: only validate/refresh when a tool is actually called
6914
5510
  await ensureValidOAuthToken();
6915
- 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}`);
6916
5521
  switch (params.name) {
6917
5522
  case "execute_graphql": {
6918
5523
  const args = ExecuteGraphQLSchema.parse(params.arguments);
@@ -8386,9 +6991,7 @@ async function startSSEServer() {
8386
6991
  });
8387
6992
  });
8388
6993
  const httpServer = app.listen(Number(PORT), HOST, () => {
8389
- logger.info(`GitLab MCP Server running with SSE transport`);
8390
- const colorGreen = "\x1b[32m";
8391
- const colorReset = "\x1b[0m";
6994
+ logger.info("GitLab MCP Server running with SSE transport");
8392
6995
  logger.info(`${colorGreen}Endpoint: http://${HOST}:${PORT}/sse${colorReset}`);
8393
6996
  });
8394
6997
  const shutdown = async (signal) => {
@@ -8464,10 +7067,19 @@ async function startStreamableHTTPServer() {
8464
7067
  session.count++;
8465
7068
  return true;
8466
7069
  };
7070
+ /**
7071
+ * Check whether the request carries a raw header auth token (Private-Token or JOB-TOKEN).
7072
+ * Used to decide whether to bypass OAuth validation.
7073
+ */
7074
+ const hasHeaderAuth = (req) => {
7075
+ return !!(req.headers["private-token"] ||
7076
+ req.headers["job-token"]);
7077
+ };
8467
7078
  /**
8468
7079
  * Parse authentication from request headers
8469
7080
  * Returns null if no auth found or invalid format
8470
- * Supports: JOB-TOKEN header, Private-Token header, Authorization Bearer header
7081
+ * Supports: Private-Token header, JOB-TOKEN header, Authorization Bearer header
7082
+ * Priority: Private-Token > JOB-TOKEN > Authorization Bearer
8471
7083
  */
8472
7084
  const parseAuthHeaders = (req) => {
8473
7085
  const authHeader = req.headers["authorization"] || "";
@@ -8486,17 +7098,18 @@ async function startStreamableHTTPServer() {
8486
7098
  return null; // Reject if URL is malformed
8487
7099
  }
8488
7100
  }
8489
- // Extract token
7101
+ // Extract token — priority: Private-Token > JOB-TOKEN > Authorization Bearer
7102
+ // PATs are preferred over job tokens because they carry broader permissions.
8490
7103
  let token = null;
8491
7104
  let header = null;
8492
- if (jobToken) {
8493
- token = jobToken.trim();
8494
- header = "JOB-TOKEN";
8495
- }
8496
- else if (privateToken) {
7105
+ if (privateToken) {
8497
7106
  token = privateToken.trim();
8498
7107
  header = "Private-Token";
8499
7108
  }
7109
+ else if (jobToken) {
7110
+ token = jobToken.trim();
7111
+ header = "JOB-TOKEN";
7112
+ }
8500
7113
  else if (authHeader) {
8501
7114
  // Use \S+ instead of .+ to prevent ReDoS attacks
8502
7115
  // \S+ only matches non-whitespace, so trim() is technically unnecessary,
@@ -8557,7 +7170,7 @@ async function startStreamableHTTPServer() {
8557
7170
  app.set("trust proxy", 1);
8558
7171
  const gitlabBaseUrl = GITLAB_API_URL.replace(/\/api\/v4\/?$/, "").replace(/\/$/, "");
8559
7172
  const issuerUrl = new URL(MCP_SERVER_URL);
8560
- const oauthProvider = createGitLabOAuthProvider(gitlabBaseUrl, GITLAB_OAUTH_APP_ID, "GitLab MCP Server", GITLAB_READ_ONLY_MODE);
7173
+ const oauthProvider = createGitLabOAuthProvider(gitlabBaseUrl, GITLAB_OAUTH_APP_ID, "GitLab MCP Server", GITLAB_READ_ONLY_MODE, GITLAB_OAUTH_SCOPES);
8561
7174
  // Mounts /.well-known/oauth-authorization-server,
8562
7175
  // /.well-known/oauth-protected-resource,
8563
7176
  // /authorize, /token, /register, /revoke
@@ -8565,7 +7178,7 @@ async function startStreamableHTTPServer() {
8565
7178
  provider: oauthProvider,
8566
7179
  issuerUrl,
8567
7180
  baseUrl: issuerUrl,
8568
- scopesSupported: ["api", "read_api", "read_user"],
7181
+ scopesSupported: GITLAB_OAUTH_SCOPES ?? ["api", "read_api", "read_user"],
8569
7182
  resourceName: "GitLab MCP Server",
8570
7183
  }));
8571
7184
  // Expose provider so the /mcp route middleware can reference it
@@ -8574,11 +7187,36 @@ async function startStreamableHTTPServer() {
8574
7187
  // Build bearer-auth middleware — no-op unless GITLAB_MCP_OAUTH is enabled.
8575
7188
  // Unauthenticated requests receive 401 + WWW-Authenticate header, which is
8576
7189
  // exactly what Claude.ai needs to trigger the OAuth browser flow.
8577
- const mcpBearerAuth = GITLAB_MCP_OAUTH
7190
+ //
7191
+ // Header auth fallback: if Private-Token or JOB-TOKEN headers are present,
7192
+ // OAuth validation is skipped and the raw token is used directly per-session.
7193
+ // Note: Authorization: Bearer is always treated as an OAuth token and goes
7194
+ // through OAuth validation — use Private-Token for PAT-based header auth.
7195
+ const oauthBearerAuth = GITLAB_MCP_OAUTH
8578
7196
  ? requireBearerAuth({
8579
7197
  verifier: app._mcpOAuthProvider,
8580
7198
  requiredScopes: [],
8581
7199
  })
7200
+ : undefined;
7201
+ const mcpBearerAuth = GITLAB_MCP_OAUTH
7202
+ ? (req, res, next) => {
7203
+ const privateToken = req.headers["private-token"] || "";
7204
+ const jobToken = req.headers["job-token"] || "";
7205
+ if (privateToken || jobToken) {
7206
+ // Validate the raw token before bypassing OAuth
7207
+ const authData = parseAuthHeaders(req);
7208
+ if (authData) {
7209
+ next();
7210
+ return;
7211
+ }
7212
+ res.status(401).json({
7213
+ error: "Invalid Private-Token or JOB-TOKEN header",
7214
+ message: "The provided token failed validation. Check the token value and format.",
7215
+ });
7216
+ return;
7217
+ }
7218
+ oauthBearerAuth(req, res, next);
7219
+ }
8582
7220
  : (_req, _res, next) => next();
8583
7221
  // Streamable HTTP endpoint - handles both session creation and message handling
8584
7222
  app.post("/mcp", mcpBearerAuth, async (req, res) => {
@@ -8611,8 +7249,8 @@ async function startStreamableHTTPServer() {
8611
7249
  if (!authData) {
8612
7250
  metrics.authFailures++;
8613
7251
  res.status(401).json({
8614
- error: "Missing Authorization, Private-Token, or JOB-TOKEN header",
8615
- message: "Remote authorization is enabled. Please provide Authorization, Private-Token, or JOB-TOKEN header.",
7252
+ error: "Missing Private-Token, JOB-TOKEN, or Authorization header",
7253
+ message: "Remote authorization is enabled. Please provide Private-Token, JOB-TOKEN, or Authorization header.",
8616
7254
  });
8617
7255
  return;
8618
7256
  }
@@ -8636,28 +7274,49 @@ async function startStreamableHTTPServer() {
8636
7274
  // First request without session - will fail in initialization
8637
7275
  }
8638
7276
  }
8639
- // MCP OAuth mode — token already validated by requireBearerAuth middleware.
8640
- // req.auth is populated by the middleware; store/refresh per session so that
8641
- // buildAuthHeaders() can pick it up via AsyncLocalStorage, exactly like the
8642
- // REMOTE_AUTHORIZATION path.
7277
+ // MCP OAuth mode — either header auth (PAT/job token) or OAuth Bearer token.
7278
+ // Header auth takes precedence: if Private-Token or JOB-TOKEN is present the
7279
+ // OAuth middleware was bypassed and we store the raw token per-session.
7280
+ // Otherwise req.auth is populated by requireBearerAuth; store the OAuth token.
8643
7281
  if (GITLAB_MCP_OAUTH) {
8644
- const authInfo = req.auth;
8645
- if (authInfo?.token && sessionId) {
8646
- if (!authBySession[sessionId]) {
8647
- authBySession[sessionId] = {
8648
- header: "Authorization",
8649
- token: authInfo.token,
8650
- lastUsed: Date.now(),
8651
- apiUrl: GITLAB_API_URL,
8652
- };
8653
- logger.info(`Session ${sessionId}: stored OAuth token (client: ${authInfo.clientId})`);
8654
- setAuthTimeout(sessionId);
7282
+ const headerAuthData = hasHeaderAuth(req) ? parseAuthHeaders(req) : null;
7283
+ if (headerAuthData) {
7284
+ if (headerAuthData && sessionId) {
7285
+ if (!authBySession[sessionId]) {
7286
+ authBySession[sessionId] = headerAuthData;
7287
+ logger.info(`Session ${sessionId}: stored ${headerAuthData.header} header (header auth)`);
7288
+ setAuthTimeout(sessionId);
7289
+ }
7290
+ else {
7291
+ authBySession[sessionId] = {
7292
+ ...authBySession[sessionId],
7293
+ header: headerAuthData.header,
7294
+ token: headerAuthData.token,
7295
+ lastUsed: Date.now(),
7296
+ };
7297
+ setAuthTimeout(sessionId);
7298
+ }
8655
7299
  }
8656
- else {
8657
- // Update token on every request — the client may have refreshed it
8658
- authBySession[sessionId].token = authInfo.token;
8659
- authBySession[sessionId].lastUsed = Date.now();
8660
- setAuthTimeout(sessionId);
7300
+ }
7301
+ else {
7302
+ const authInfo = req.auth;
7303
+ if (authInfo?.token && sessionId) {
7304
+ if (!authBySession[sessionId]) {
7305
+ authBySession[sessionId] = {
7306
+ header: "Authorization",
7307
+ token: authInfo.token,
7308
+ lastUsed: Date.now(),
7309
+ apiUrl: GITLAB_API_URL,
7310
+ };
7311
+ logger.info(`Session ${sessionId}: stored OAuth token (client: ${authInfo.clientId})`);
7312
+ setAuthTimeout(sessionId);
7313
+ }
7314
+ else {
7315
+ // Update token on every request — the client may have refreshed it
7316
+ authBySession[sessionId].token = authInfo.token;
7317
+ authBySession[sessionId].lastUsed = Date.now();
7318
+ setAuthTimeout(sessionId);
7319
+ }
8661
7320
  }
8662
7321
  }
8663
7322
  }
@@ -8688,18 +7347,29 @@ async function startStreamableHTTPServer() {
8688
7347
  setAuthTimeout(newSessionId);
8689
7348
  }
8690
7349
  }
8691
- // Store OAuth token for newly created session in MCP OAuth mode
7350
+ // Store OAuth token for newly created session in MCP OAuth mode.
7351
+ // If Private-Token or JOB-TOKEN headers are present, prefer them.
8692
7352
  if (GITLAB_MCP_OAUTH && !authBySession[newSessionId]) {
8693
- const authInfo = req.auth;
8694
- if (authInfo?.token) {
8695
- authBySession[newSessionId] = {
8696
- header: "Authorization",
8697
- token: authInfo.token,
8698
- lastUsed: Date.now(),
8699
- apiUrl: GITLAB_API_URL,
8700
- };
8701
- logger.info(`Session ${newSessionId}: stored OAuth token (client: ${authInfo.clientId})`);
8702
- setAuthTimeout(newSessionId);
7353
+ if (hasHeaderAuth(req)) {
7354
+ const authData = parseAuthHeaders(req);
7355
+ if (authData) {
7356
+ authBySession[newSessionId] = authData;
7357
+ logger.info(`Session ${newSessionId}: stored ${authData.header} header (header auth)`);
7358
+ setAuthTimeout(newSessionId);
7359
+ }
7360
+ }
7361
+ else {
7362
+ const authInfo = req.auth;
7363
+ if (authInfo?.token) {
7364
+ authBySession[newSessionId] = {
7365
+ header: "Authorization",
7366
+ token: authInfo.token,
7367
+ lastUsed: Date.now(),
7368
+ apiUrl: GITLAB_API_URL,
7369
+ };
7370
+ logger.info(`Session ${newSessionId}: stored OAuth token (client: ${authInfo.clientId})`);
7371
+ setAuthTimeout(newSessionId);
7372
+ }
8703
7373
  }
8704
7374
  }
8705
7375
  },