@zereight/mcp-gitlab 2.0.36 → 2.1.1

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