@zereight/mcp-gitlab 2.1.25 → 2.1.26

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,5 +1,5 @@
1
1
  #!/usr/bin/env node
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_OAUTH_CALLBACK_PROXY, 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, OAUTH_STATELESS_CLIENT_TTL_SECONDS, OAUTH_STATELESS_MODE, OAUTH_STATELESS_PENDING_TTL_SECONDS, OAUTH_STATELESS_SESSION_TTL_SECONDS, OAUTH_STATELESS_STORED_TTL_SECONDS, PORT, REMOTE_AUTHORIZATION, SESSION_TIMEOUT_SECONDS, SSE, STREAMABLE_HTTP, MCP_TRUST_PROXY, USE_GITLAB_WIKI, USE_MILESTONE, USE_OAUTH, USE_PIPELINE, GITLAB_TOOL_POLICY_APPROVE_RAW, GITLAB_TOOL_POLICY_HIDDEN_RAW, GITLAB_OAUTH_ALLOWED_GROUPS_RAW, GITLAB_ALLOWED_GROUPS_RAW, GITLAB_OAUTH_ALLOWED_GROUPS, } from "./config.js";
2
+ import { getConfig, ENABLE_DYNAMIC_API_URL, GITLAB_AUTH_COOKIE_PATH, GITLAB_ALLOW_UNAUTHENTICATED_TOOL_DISCOVERY, GITLAB_CA_CERT_PATH, GITLAB_JOB_TOKEN, GITLAB_MCP_OAUTH, GITLAB_OAUTH_APP_ID, GITLAB_OAUTH_SCOPES, GITLAB_OAUTH_CALLBACK_PROXY, 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, OAUTH_STATELESS_CLIENT_TTL_SECONDS, OAUTH_STATELESS_MODE, OAUTH_STATELESS_PENDING_TTL_SECONDS, OAUTH_STATELESS_SESSION_TTL_SECONDS, OAUTH_STATELESS_STORED_TTL_SECONDS, PORT, REMOTE_AUTHORIZATION, SESSION_TIMEOUT_SECONDS, SSE, STREAMABLE_HTTP, MCP_TRUST_PROXY, USE_GITLAB_WIKI, USE_MILESTONE, USE_OAUTH, USE_PIPELINE, GITLAB_TOOL_POLICY_APPROVE_RAW, GITLAB_TOOL_POLICY_HIDDEN_RAW, GITLAB_OAUTH_ALLOWED_GROUPS_RAW, GITLAB_ALLOWED_GROUPS_RAW, GITLAB_OAUTH_ALLOWED_GROUPS, } from "./config.js";
3
3
  /** True when the server is running in remote/network mode (SSE or StreamableHTTP transport). */
4
4
  const IS_REMOTE = SSE || STREAMABLE_HTTP;
5
5
  /**
@@ -59,62 +59,6 @@ function decryptDownloadToken(tokenStr) {
59
59
  return null;
60
60
  }
61
61
  }
62
- function getLastHeaderValue(value) {
63
- const raw = Array.isArray(value) ? value[value.length - 1] : value;
64
- if (!raw)
65
- return undefined;
66
- const parts = raw.split(",").map(part => part.trim()).filter(Boolean);
67
- return parts.length > 0 ? parts[parts.length - 1] : undefined;
68
- }
69
- function unquoteHeaderValue(value) {
70
- if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
71
- return value.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, "\\");
72
- }
73
- return value;
74
- }
75
- function getForwardedPublicBaseUrl(req) {
76
- if (!MCP_TRUST_PROXY)
77
- return undefined;
78
- const forwarded = getLastHeaderValue(req.headers.forwarded);
79
- const forwardedValues = {};
80
- if (forwarded) {
81
- for (const part of forwarded.split(";")) {
82
- const separator = part.indexOf("=");
83
- if (separator <= 0)
84
- continue;
85
- const key = part.slice(0, separator).trim().toLowerCase();
86
- const value = unquoteHeaderValue(part.slice(separator + 1).trim());
87
- if (key && value)
88
- forwardedValues[key] = value;
89
- }
90
- }
91
- const proto = (forwardedValues.proto || getLastHeaderValue(req.headers["x-forwarded-proto"]))?.toLowerCase();
92
- const host = forwardedValues.host || getLastHeaderValue(req.headers["x-forwarded-host"]);
93
- if (!proto || !host || !/^https?$/.test(proto))
94
- return undefined;
95
- if (/[\s/\\]/.test(host))
96
- return undefined;
97
- const prefix = getLastHeaderValue(req.headers["x-forwarded-prefix"]);
98
- const safePrefix = prefix &&
99
- prefix.startsWith("/") &&
100
- !prefix.startsWith("//") &&
101
- !prefix.includes("://") &&
102
- !/[\s\\]/.test(prefix)
103
- ? prefix.replace(/\/+$/, "")
104
- : undefined;
105
- try {
106
- const baseUrl = new URL(`${proto}://${host}`);
107
- if (baseUrl.username || baseUrl.password || baseUrl.pathname !== "/" || baseUrl.search || baseUrl.hash) {
108
- return undefined;
109
- }
110
- if (safePrefix)
111
- baseUrl.pathname = safePrefix;
112
- return baseUrl.toString().replace(/\/$/, "");
113
- }
114
- catch {
115
- return undefined;
116
- }
117
- }
118
62
  /**
119
63
  * Build a URL pointing to the download proxy endpoint.
120
64
  * Embeds an encrypted auth token (and API URL for dynamic routing)
@@ -185,8 +129,9 @@ import { createGitLabOAuthProvider } from "./oauth-proxy.js";
185
129
  import { mcpAuthRouter } from "@modelcontextprotocol/sdk/server/auth/router.js";
186
130
  import { ipKeyGenerator } from "express-rate-limit";
187
131
  import { normalizeProxyClientIpForRateLimit } from "./utils/proxy-client-ip.js";
132
+ import { getForwardedPublicBaseUrl } from "./utils/forwarded-public-base-url.js";
188
133
  import { normalizeGitLabApiUrl } from "./utils/url.js";
189
- import { estimateMergeCommitCount, filterDiffsByPatterns, summarizeWebhookEvents } from "./utils/helpers.js";
134
+ import { estimateMergeCommitCount, filterDiffsByPatterns, summarizeWebhookEvents, } from "./utils/helpers.js";
190
135
  import { graphqlQueryContainsWriteOperation } from "./utils/graphql-query.js";
191
136
  import { resolveNestedWikiUpdateTitle } from "./utils/wiki-title.js";
192
137
  import { cleanMutuallyExclusiveIdUsernameOptions, LIST_MERGE_REQUESTS_ID_USERNAME_PAIRS, sanitizeToolArguments, } from "./utils/tool-args.js";
@@ -201,7 +146,7 @@ GitLabDiscussionNoteSchema, // Added
201
146
  GitLabDiscussionSchema,
202
147
  // Draft Notes Schemas
203
148
  GitLabDraftNoteSchema, GitLabForkSchema, GitLabBranchSchema, GitLabProtectedBranchSchema, GitLabGroupSchema, GitLabIssueLinkSchema, GitLabIssueSchema, GitLabIssueWithLinkDetailsSchema, GitLabMarkdownUploadSchema, GitLabMergeRequestPipelineSchema, GitLabMergeRequestSchema, GitLabMilestonesSchema, GitLabNamespaceExistsResponseSchema, GitLabNamespaceSchema, GitLabPipelineJobSchema, GitLabDeploymentSchema, GitLabEnvironmentSchema, GitLabPipelineSchema, GitLabPipelineTriggerJobSchema, GitLabProjectMemberSchema, GitLabProjectSchema, GitLabTodoSchema, GitLabReferenceSchema, GitLabRepositorySchema, GitLabSearchBlobResultSchema, GitLabSearchResponseSchema, GitLabTreeItemSchema, GitLabUserSchema, GitLabUsersResponseSchema, GitLabWikiPageSchema, GroupIteration, ListCommitStatusesSchema, ListBranchesSchema, ListCommitsSchema, ListDraftNotesSchema, ListGroupIterationsSchema, ListGroupProjectsSchema, GitLabCiVariableSchema, ListProjectVariablesSchema, GetProjectVariableSchema, CreateProjectVariableSchema, UpdateProjectVariableSchema, DeleteProjectVariableSchema, ListGroupVariablesSchema, GetGroupVariableSchema, CreateGroupVariableSchema, UpdateGroupVariableSchema, DeleteGroupVariableSchema, GitLabDependencyProxySchema, GitLabDependencyProxyBlobSchema, GetDependencyProxySettingsSchema, UpdateDependencyProxySettingsSchema, ListDependencyProxyBlobsSchema, PurgeDependencyProxyCacheSchema, ListIssueDiscussionsSchema, ListIssueLinksSchema, ListIssuesSchema, ListTodosSchema, ListLabelsSchema, ListMergeRequestDiffsSchema, // Added
204
- GetMergeRequestFileDiffSchema, ListMergeRequestChangedFilesSchema, ListMergeRequestDiscussionsSchema, ListMergeRequestPipelinesSchema, ListMergeRequestsSchema, ListMergeRequestVersionsSchema, GetMergeRequestVersionSchema, GitLabMergeRequestVersionSchema, GitLabMergeRequestVersionDetailSchema, ListNamespacesSchema, ListPipelineJobsSchema, ListPipelinesSchema, ListDeploymentsSchema, ListEnvironmentsSchema, ListPipelineTriggerJobsSchema, ValidateCiLintSchema, ValidateProjectCiLintSchema, ListProjectMembersSchema, ListProjectMilestonesSchema, ListProjectsSchema, ListWikiPagesSchema, GetGroupWikiPageSchema, ListGroupWikiPagesSchema, UpdateGroupWikiPageSchema, MarkdownUploadSchema, MarkdownUploadRemoteSchema, DownloadAttachmentSchema, DownloadJobArtifactsSchema, GetJobArtifactFileSchema, GitLabArtifactEntrySchema, ListJobArtifactsSchema, MergeMergeRequestSchema, ApproveMergeRequestSchema, UnapproveMergeRequestSchema, GetMergeRequestApprovalStateSchema, GetMergeRequestConflictsSchema, GitLabMergeRequestApprovalsResponseSchema, GitLabMergeRequestApprovalStateSchema, MyIssuesSchema, MarkAllTodosDoneSchema, MarkTodoDoneSchema, PaginatedDiscussionsResponseSchema, PromoteProjectMilestoneSchema, PublishDraftNoteSchema, PlayPipelineJobSchema, PushFilesSchema, RetryPipelineJobSchema, RetryPipelineSchema, SearchCodeSchema, SearchGroupCodeSchema, SearchProjectCodeSchema, SearchRepositoriesSchema, UpdateDraftNoteSchema, UpdateIssueNoteSchema, UpdateIssueSchema, UpdateIssueDescriptionPatchSchema, UpdateLabelSchema, UpdateMergeRequestNoteSchema, UpdateMergeRequestDiscussionNoteSchema, UpdateMergeRequestSchema, UpdateWikiPageSchema, VerifyNamespaceSchema, GitLabEventSchema, ListEventsSchema, GetProjectEventsSchema, ExecuteGraphQLSchema, GitLabReleaseSchema, ListReleasesSchema, GetReleaseSchema, CreateReleaseSchema, UpdateReleaseSchema, DeleteReleaseSchema, CreateReleaseEvidenceSchema, DownloadReleaseAssetSchema, ListTagsSchema, GetTagSchema, CreateTagSchema, DeleteTagSchema, GetTagSignatureSchema, GitLabTagSchema, GitLabTagSignatureSchema, GetMergeRequestNotesSchema, GetMergeRequestNoteSchema, DeleteMergeRequestDiscussionNoteSchema, ResolveMergeRequestThreadSchema, GetWorkItemSchema, ListWorkItemsSchema, CreateWorkItemSchema, UpdateWorkItemSchema, ConvertWorkItemTypeSchema, ListWorkItemStatusesSchema, ListWorkItemNotesSchema, CreateWorkItemNoteSchema, CreateWorkItemEmojiReactionSchema, CreateWorkItemNoteEmojiReactionSchema, ListWorkItemEmojiReactionsSchema, ListWorkItemNoteEmojiReactionsSchema, DeleteWorkItemEmojiReactionSchema, DeleteWorkItemNoteEmojiReactionSchema, MoveWorkItemSchema, ListCustomFieldDefinitionsSchema, GetTimelineEventsSchema, CreateTimelineEventSchema, ListWebhooksSchema, ListWebhookEventsSchema, GetWebhookEventSchema, HealthCheckSchema, } from "./schemas.js";
149
+ GetMergeRequestFileDiffSchema, ListMergeRequestChangedFilesSchema, ListMergeRequestDiscussionsSchema, ListMergeRequestPipelinesSchema, ListMergeRequestsSchema, ListMergeRequestVersionsSchema, GetMergeRequestVersionSchema, GitLabMergeRequestVersionSchema, GitLabMergeRequestVersionDetailSchema, ListNamespacesSchema, ListPipelineJobsSchema, ListPipelinesSchema, ListDeploymentsSchema, ListEnvironmentsSchema, ListPipelineTriggerJobsSchema, ValidateCiLintSchema, ValidateProjectCiLintSchema, ListCiCatalogResourcesSchema, GetCiCatalogResourceSchema, ListProjectMembersSchema, ListProjectMilestonesSchema, ListProjectsSchema, ListWikiPagesSchema, GetGroupWikiPageSchema, ListGroupWikiPagesSchema, UpdateGroupWikiPageSchema, MarkdownUploadSchema, MarkdownUploadRemoteSchema, DownloadAttachmentSchema, DownloadJobArtifactsSchema, GetJobArtifactFileSchema, GitLabArtifactEntrySchema, ListJobArtifactsSchema, MergeMergeRequestSchema, ApproveMergeRequestSchema, UnapproveMergeRequestSchema, GetMergeRequestApprovalStateSchema, GetMergeRequestConflictsSchema, GitLabMergeRequestApprovalsResponseSchema, GitLabMergeRequestApprovalStateSchema, MyIssuesSchema, MarkAllTodosDoneSchema, MarkTodoDoneSchema, PaginatedDiscussionsResponseSchema, PromoteProjectMilestoneSchema, PublishDraftNoteSchema, PlayPipelineJobSchema, PushFilesSchema, RetryPipelineJobSchema, RetryPipelineSchema, SearchCodeSchema, SearchGroupCodeSchema, SearchProjectCodeSchema, SearchRepositoriesSchema, UpdateDraftNoteSchema, UpdateIssueNoteSchema, UpdateIssueSchema, UpdateIssueDescriptionPatchSchema, UpdateLabelSchema, UpdateProjectSchema, UpdateMergeRequestNoteSchema, UpdateMergeRequestDiscussionNoteSchema, UpdateMergeRequestSchema, UpdateWikiPageSchema, VerifyNamespaceSchema, GitLabEventSchema, ListEventsSchema, GetProjectEventsSchema, ExecuteGraphQLSchema, GitLabReleaseSchema, ListReleasesSchema, GetReleaseSchema, CreateReleaseSchema, UpdateReleaseSchema, DeleteReleaseSchema, CreateReleaseEvidenceSchema, DownloadReleaseAssetSchema, ListTagsSchema, GetTagSchema, CreateTagSchema, DeleteTagSchema, GetTagSignatureSchema, GitLabTagSchema, GitLabTagSignatureSchema, GetMergeRequestNotesSchema, GetMergeRequestNoteSchema, DeleteMergeRequestDiscussionNoteSchema, ResolveMergeRequestThreadSchema, GetWorkItemSchema, ListWorkItemsSchema, CreateWorkItemSchema, UpdateWorkItemSchema, ConvertWorkItemTypeSchema, ListWorkItemStatusesSchema, ListWorkItemNotesSchema, CreateWorkItemNoteSchema, CreateWorkItemEmojiReactionSchema, CreateWorkItemNoteEmojiReactionSchema, ListWorkItemEmojiReactionsSchema, ListWorkItemNoteEmojiReactionsSchema, DeleteWorkItemEmojiReactionSchema, DeleteWorkItemNoteEmojiReactionSchema, MoveWorkItemSchema, ListCustomFieldDefinitionsSchema, GetTimelineEventsSchema, CreateTimelineEventSchema, ListWebhooksSchema, ListWebhookEventsSchema, GetWebhookEventSchema, HealthCheckSchema, } from "./schemas.js";
205
150
  import { randomUUID, createCipheriv, createDecipheriv, randomBytes, createHash } from "node:crypto";
206
151
  import { pino } from "pino";
207
152
  const logger = pino({
@@ -325,7 +270,9 @@ function createServer() {
325
270
  const modified = { ...tool };
326
271
  // Safety net: remove $schema if present (toJSONSchema strips it for zod schemas,
327
272
  // but manually-defined schemas like discover_tools may still have it)
328
- if (modified.inputSchema && typeof modified.inputSchema === "object" && modified.inputSchema !== null) {
273
+ if (modified.inputSchema &&
274
+ typeof modified.inputSchema === "object" &&
275
+ modified.inputSchema !== null) {
329
276
  if ("$schema" in modified.inputSchema) {
330
277
  modified.inputSchema = { ...modified.inputSchema };
331
278
  delete modified.inputSchema.$schema;
@@ -371,7 +318,12 @@ function createServer() {
371
318
  };
372
319
  const logError = (error) => {
373
320
  const durationMs = Date.now() - start;
374
- logger.error({ tool: toolName, event: "tool_call_error", durationMs, error: error instanceof Error ? error.message : String(error) }, `tool_call_error: ${toolName} (${durationMs}ms)`);
321
+ logger.error({
322
+ tool: toolName,
323
+ event: "tool_call_error",
324
+ durationMs,
325
+ error: error instanceof Error ? error.message : String(error),
326
+ }, `tool_call_error: ${toolName} (${durationMs}ms)`);
375
327
  throw error;
376
328
  };
377
329
  try {
@@ -390,16 +342,18 @@ function createServer() {
390
342
  return logCompletion({
391
343
  content: [{
392
344
  type: "text",
393
- text: JSON.stringify({ categories, hint: "Call discover_tools with a category name to activate it" }, null, 2),
345
+ text: JSON.stringify({ categories, hint: "Call discover_tools with a category name to activate it" }),
394
346
  }],
395
347
  });
396
348
  }
397
349
  if (!ALL_TOOLSET_IDS.has(category)) {
398
350
  return logCompletion({
399
- content: [{
351
+ content: [
352
+ {
400
353
  type: "text",
401
354
  text: `Unknown category "${category}". Available: ${[...ALL_TOOLSET_IDS].join(", ")}`,
402
- }],
355
+ },
356
+ ],
403
357
  isError: true,
404
358
  });
405
359
  }
@@ -414,10 +368,12 @@ function createServer() {
414
368
  const alreadyActive = [...toolsetDef.tools].every(t => currentToolNames.has(t));
415
369
  if (alreadyActive) {
416
370
  return logCompletion({
417
- content: [{
371
+ content: [
372
+ {
418
373
  type: "text",
419
374
  text: `Category "${category}" is already active (${toolsetDef.tools.size} tools).`,
420
- }],
375
+ },
376
+ ],
421
377
  });
422
378
  }
423
379
  // Add tools from this toolset, respecting all filtering policies
@@ -437,10 +393,12 @@ function createServer() {
437
393
  }
438
394
  if (newTools.length === 0) {
439
395
  return logCompletion({
440
- content: [{
396
+ content: [
397
+ {
441
398
  type: "text",
442
399
  text: `Category "${category}" has no additional tools to activate (all already active or filtered).`,
443
- }],
400
+ },
401
+ ],
444
402
  });
445
403
  }
446
404
  filteredTools.push(...newTools);
@@ -460,7 +418,7 @@ function createServer() {
460
418
  activated: category,
461
419
  addedTools: addedNames,
462
420
  totalTools: filteredTools.length,
463
- }, null, 2),
421
+ }),
464
422
  }],
465
423
  });
466
424
  }
@@ -470,10 +428,12 @@ function createServer() {
470
428
  if (!confirmed) {
471
429
  logger.info({ tool: toolName, event: "tool_call_approval_required" }, `Approval required: ${toolName}`);
472
430
  return logCompletion({
473
- content: [{
431
+ content: [
432
+ {
474
433
  type: "text",
475
434
  text: `Tool "${toolName}" requires confirmation. This tool is marked as requiring approval before execution. Re-call with _confirmed: true to proceed.`,
476
- }],
435
+ },
436
+ ],
477
437
  });
478
438
  }
479
439
  // Strip _confirmed from args before forwarding to handler
@@ -636,13 +596,24 @@ function redactSessionIdForLog(sid) {
636
596
  function isInitializationRequestBody(body) {
637
597
  if (!body)
638
598
  return false;
639
- const isInitObj = (m) => typeof m === "object" &&
640
- m !== null &&
641
- m.method === "initialize";
599
+ const isInitObj = (m) => typeof m === "object" && m !== null && m.method === "initialize";
642
600
  if (Array.isArray(body))
643
601
  return body.some(isInitObj);
644
602
  return isInitObj(body);
645
603
  }
604
+ function isUnauthenticatedDiscoveryRequestBody(body) {
605
+ if (!body)
606
+ return false;
607
+ const isDiscoveryMethod = (m) => {
608
+ if (typeof m !== "object" || m === null)
609
+ return false;
610
+ const method = m.method;
611
+ return (method === "initialize" || method === "notifications/initialized" || method === "tools/list");
612
+ };
613
+ if (Array.isArray(body))
614
+ return body.every(isDiscoveryMethod);
615
+ return isDiscoveryMethod(body);
616
+ }
646
617
  /**
647
618
  * Normalize an `Mcp-Session-Id` header value.
648
619
  *
@@ -699,9 +670,7 @@ catch (err) {
699
670
  * a 401 from the OAuth bearer middleware.
700
671
  */
701
672
  export function hasStatelessSessionId(req) {
702
- return Boolean(OAUTH_STATELESS_MODE &&
703
- STATELESS_MATERIAL &&
704
- readMcpSessionIdHeader(req));
673
+ return Boolean(OAUTH_STATELESS_MODE && STATELESS_MATERIAL && readMcpSessionIdHeader(req));
705
674
  }
706
675
  /**
707
676
  * Ensure the OAuth token is valid before making an API call.
@@ -845,7 +814,9 @@ function defaultAuthRetryConfig() {
845
814
  return {
846
815
  isOAuthEnabled: () => USE_OAUTH && oauthClient != null,
847
816
  refreshToken: (force) => oauthClient.getAccessToken(force),
848
- onTokenRefreshed: (token) => { OAUTH_ACCESS_TOKEN = token; },
817
+ onTokenRefreshed: (token) => {
818
+ OAUTH_ACCESS_TOKEN = token;
819
+ },
849
820
  buildAuthHeaders,
850
821
  logger,
851
822
  };
@@ -1058,7 +1029,12 @@ if (GITLAB_MCP_OAUTH) {
1058
1029
  }
1059
1030
  logger.info("MCP OAuth enabled: GitLab OAuth proxy active (Private-Token/JOB-TOKEN headers bypass OAuth)");
1060
1031
  }
1061
- if (!REMOTE_AUTHORIZATION && !GITLAB_MCP_OAUTH && !USE_OAUTH && !GITLAB_PERSONAL_ACCESS_TOKEN && !GITLAB_JOB_TOKEN && !GITLAB_AUTH_COOKIE_PATH) {
1032
+ if (!REMOTE_AUTHORIZATION &&
1033
+ !GITLAB_MCP_OAUTH &&
1034
+ !USE_OAUTH &&
1035
+ !GITLAB_PERSONAL_ACCESS_TOKEN &&
1036
+ !GITLAB_JOB_TOKEN &&
1037
+ !GITLAB_AUTH_COOKIE_PATH) {
1062
1038
  // Standard mode: token must be in environment (unless using OAuth)
1063
1039
  logger.error("GITLAB_PERSONAL_ACCESS_TOKEN environment variable is not set");
1064
1040
  logger.info("Either set GITLAB_PERSONAL_ACCESS_TOKEN or enable OAuth with GITLAB_USE_OAUTH=true");
@@ -1483,18 +1459,28 @@ async function resolveNamesToIds(projectPath, labelNames, usernames) {
1483
1459
  if (!labelNames?.length && !usernames?.length) {
1484
1460
  return { labelIds: [], userIds: [] };
1485
1461
  }
1486
- const data = await executeGraphQL(`query($path: ID!, $usernames: [String!]!) {
1487
- project(fullPath: $path) { labels(includeAncestorGroups: true, first: 250) { nodes { id title } } }
1462
+ labelNames ??= [];
1463
+ usernames ??= [];
1464
+ const labelVars = Object.fromEntries(labelNames.map((name, i) => [`l${i}`, name]));
1465
+ // One alias per label — exact title match via the `title` argument, includes ancestor
1466
+ // group labels, single round trip with no pagination needed.
1467
+ const varDefs = labelNames.map((_, i) => `$l${i}: String!`).join(", ");
1468
+ const aliases = labelNames.map((_, i) => `l${i}: labels(title: $l${i}, includeAncestorGroups: true, first: 1) { nodes { id } }`).join(" ");
1469
+ const { project, users } = await executeGraphQL(`query($path: ID!, $usernames: [String!]!${varDefs ? `, ${varDefs}` : ""}) {
1470
+ project(fullPath: $path) { ${aliases || "__typename"} }
1488
1471
  users(usernames: $usernames) { nodes { id username } }
1489
- }`, { path: projectPath, usernames: usernames || [] });
1490
- const labelIds = (labelNames || []).map(name => {
1491
- const label = data.project.labels.nodes.find(l => l.title === name);
1492
- if (!label)
1472
+ }`, { path: projectPath, usernames, ...labelVars });
1473
+ if (!project) {
1474
+ throw new Error(`Project '${projectPath}' not found or inaccessible`);
1475
+ }
1476
+ const labelIds = labelNames.map((name, i) => {
1477
+ const nodes = project[`l${i}`]?.nodes;
1478
+ if (!nodes?.length)
1493
1479
  throw new Error(`Label '${name}' not found in project`);
1494
- return label.id;
1480
+ return nodes[0].id;
1495
1481
  });
1496
- const userIds = (usernames || []).map(name => {
1497
- const user = data.users.nodes.find(u => u.username === name);
1482
+ const userIds = usernames.map(name => {
1483
+ const user = users.nodes.find(u => u.username === name);
1498
1484
  if (!user)
1499
1485
  throw new Error(`User '${name}' not found`);
1500
1486
  return user.id;
@@ -1534,7 +1520,7 @@ async function resolveWorkItemTypeGID(projectPath, typeName) {
1534
1520
  }
1535
1521
  }
1536
1522
  }`, { path: projectPath });
1537
- const typeNode = data.namespace?.workItemTypes?.nodes?.find((n) => n.name === targetName);
1523
+ const typeNode = data.namespace?.workItemTypes?.nodes?.find(n => n.name === targetName);
1538
1524
  if (!typeNode) {
1539
1525
  throw new Error(`Work item type '${targetName}' not found in project ${projectPath}`);
1540
1526
  }
@@ -2140,7 +2126,11 @@ async function getWorkItem(projectId, iid) {
2140
2126
  if (wi.closedAt)
2141
2127
  result.closedAt = wi.closedAt;
2142
2128
  if (statusWidget?.status)
2143
- result.status = { name: statusWidget.status.name, id: statusWidget.status.id, category: statusWidget.status.category };
2129
+ result.status = {
2130
+ name: statusWidget.status.name,
2131
+ id: statusWidget.status.id,
2132
+ category: statusWidget.status.category,
2133
+ };
2144
2134
  const labels = (labelsWidget?.labels?.nodes || []).map((l) => l.title);
2145
2135
  if (labels.length > 0)
2146
2136
  result.labels = labels;
@@ -2178,10 +2168,23 @@ async function getWorkItem(projectId, iid) {
2178
2168
  if (colorWidget?.color)
2179
2169
  result.color = colorWidget.color;
2180
2170
  if (hierarchyWidget?.parent)
2181
- result.parent = { iid: hierarchyWidget.parent.iid, title: hierarchyWidget.parent.title, type: hierarchyWidget.parent.workItemType?.name, project: hierarchyWidget.parent.namespace?.fullPath, webUrl: hierarchyWidget.parent.webUrl };
2171
+ result.parent = {
2172
+ iid: hierarchyWidget.parent.iid,
2173
+ title: hierarchyWidget.parent.title,
2174
+ type: hierarchyWidget.parent.workItemType?.name,
2175
+ project: hierarchyWidget.parent.namespace?.fullPath,
2176
+ webUrl: hierarchyWidget.parent.webUrl,
2177
+ };
2182
2178
  const children = hierarchyWidget?.children?.nodes || [];
2183
2179
  if (children.length > 0)
2184
- result.children = children.map((c) => ({ iid: c.iid, title: c.title, state: c.state, type: c.workItemType?.name, project: c.namespace?.fullPath, webUrl: c.webUrl }));
2180
+ result.children = children.map((c) => ({
2181
+ iid: c.iid,
2182
+ title: c.title,
2183
+ state: c.state,
2184
+ type: c.workItemType?.name,
2185
+ project: c.namespace?.fullPath,
2186
+ webUrl: c.webUrl,
2187
+ }));
2185
2188
  if (linkedItemsWidget?.blocked)
2186
2189
  result.blocked = true;
2187
2190
  if (linkedItemsWidget?.blockedByCount > 0)
@@ -2253,7 +2256,7 @@ async function listWorkItems(projectId, options) {
2253
2256
  first: options.first || 20,
2254
2257
  };
2255
2258
  if (options.types && options.types.length > 0) {
2256
- variables.types = options.types.map((t) => typeMap[t] || t.replace(/ /g, "_").toUpperCase());
2259
+ variables.types = options.types.map(t => typeMap[t] || t.replace(/ /g, "_").toUpperCase());
2257
2260
  }
2258
2261
  if (options.state) {
2259
2262
  variables.state = options.state === "opened" ? "opened" : "closed";
@@ -2721,7 +2724,9 @@ async function updateWorkItem(projectId, iid, options) {
2721
2724
  linked_items_added: options.linked_items_to_add?.length || 0,
2722
2725
  linked_items_removed: options.linked_items_to_remove?.length || 0,
2723
2726
  ...(options.severity !== undefined && { severity: options.severity }),
2724
- ...(options.escalation_status !== undefined && { escalation_status: options.escalation_status }),
2727
+ ...(options.escalation_status !== undefined && {
2728
+ escalation_status: options.escalation_status,
2729
+ }),
2725
2730
  };
2726
2731
  }
2727
2732
  /**
@@ -3023,9 +3028,7 @@ async function updateIssueNote(projectId, issueIid, discussionId, noteId, body,
3023
3028
  async function createIssueNote(projectId, issueIid, discussionId, body, createdAt) {
3024
3029
  projectId = decodeURIComponent(projectId); // Decode project ID
3025
3030
  const basePath = `${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${issueIid}`;
3026
- const url = new URL(discussionId
3027
- ? `${basePath}/discussions/${discussionId}/notes`
3028
- : `${basePath}/notes`);
3031
+ const url = new URL(discussionId ? `${basePath}/discussions/${discussionId}/notes` : `${basePath}/notes`);
3029
3032
  const payload = { body };
3030
3033
  if (createdAt) {
3031
3034
  payload.created_at = createdAt;
@@ -3392,6 +3395,7 @@ async function createRepository(options) {
3392
3395
  method: "POST",
3393
3396
  body: JSON.stringify({
3394
3397
  name: options.name,
3398
+ ...(options.namespace_id !== undefined ? { namespace_id: options.namespace_id } : {}),
3395
3399
  description: options.description,
3396
3400
  visibility: options.visibility,
3397
3401
  initialize_with_readme: options.initialize_with_readme,
@@ -5718,7 +5722,7 @@ async function getCurrentUser() {
5718
5722
  if ((response.status === 401 || response.status === 403) && usesJobTokenHeader()) {
5719
5723
  const jobResponse = await fetch(`${getEffectiveApiUrl()}/job`, getFetchConfig());
5720
5724
  if (jobResponse.ok) {
5721
- const jobData = await jobResponse.json();
5725
+ const jobData = (await jobResponse.json());
5722
5726
  if (jobData.user) {
5723
5727
  return GitLabUserSchema.parse(jobData.user);
5724
5728
  }
@@ -5742,7 +5746,8 @@ async function myIssues(options = {}) {
5742
5746
  effectiveProjectId = getEffectiveProjectId(options.project_id || "");
5743
5747
  }
5744
5748
  catch (err) {
5745
- if (err instanceof Error && err.message.includes("No project ID provided and GITLAB_PROJECT_ID is not set")) {
5749
+ if (err instanceof Error &&
5750
+ err.message.includes("No project ID provided and GITLAB_PROJECT_ID is not set")) {
5746
5751
  effectiveProjectId = "";
5747
5752
  }
5748
5753
  else {
@@ -5956,7 +5961,11 @@ async function updateGroupVariable(groupId, key, options) {
5956
5961
  if (filter?.environment_scope) {
5957
5962
  url.searchParams.append("filter[environment_scope]", filter.environment_scope);
5958
5963
  }
5959
- const response = await fetch(url.toString(), { ...getFetchConfig(), method: "PUT", body: JSON.stringify(body) });
5964
+ const response = await fetch(url.toString(), {
5965
+ ...getFetchConfig(),
5966
+ method: "PUT",
5967
+ body: JSON.stringify(body),
5968
+ });
5960
5969
  await handleGitLabError(response);
5961
5970
  const data = await response.json();
5962
5971
  return GitLabCiVariableSchema.parse(data);
@@ -6004,7 +6013,9 @@ async function getDependencyProxySettings(groupPath) {
6004
6013
  });
6005
6014
  }
6006
6015
  async function updateDependencyProxySettings(groupPath, options) {
6007
- if (options.enabled === undefined && options.identity === undefined && options.secret === undefined) {
6016
+ if (options.enabled === undefined &&
6017
+ options.identity === undefined &&
6018
+ options.secret === undefined) {
6008
6019
  throw new Error("At least one of enabled, identity, or secret must be provided");
6009
6020
  }
6010
6021
  const fullPath = await resolveGroupFullPath(groupPath);
@@ -6038,7 +6049,11 @@ async function listDependencyProxyBlobs(groupPath, options = {}) {
6038
6049
  if (!conn)
6039
6050
  throw new Error(`Group not found or dependency proxy not enabled: ${fullPath}`);
6040
6051
  return {
6041
- blobs: conn.nodes.map(n => GitLabDependencyProxyBlobSchema.parse({ file_name: n.fileName, size: n.size, created_at: n.createdAt })),
6052
+ blobs: conn.nodes.map(n => GitLabDependencyProxyBlobSchema.parse({
6053
+ file_name: n.fileName,
6054
+ size: n.size,
6055
+ created_at: n.createdAt,
6056
+ })),
6042
6057
  pageInfo: conn.pageInfo,
6043
6058
  };
6044
6059
  }
@@ -6098,7 +6113,7 @@ async function markdownUpload(projectId, filePath, content, filename) {
6098
6113
  const response = await fetch(url.toString(), {
6099
6114
  ...defaultFetchConfig,
6100
6115
  method: "POST",
6101
- body: form
6116
+ body: form,
6102
6117
  });
6103
6118
  if (!response.ok) {
6104
6119
  await handleGitLabError(response);
@@ -6419,6 +6434,34 @@ async function getTagSignature(projectId, tagName) {
6419
6434
  const data = await response.json();
6420
6435
  return GitLabTagSignatureSchema.parse(data);
6421
6436
  }
6437
+ async function executeGitLabGraphQL(query, variables = {}) {
6438
+ const apiUrl = new URL(getEffectiveApiUrl());
6439
+ const restPath = apiUrl.pathname || "";
6440
+ const idx = restPath.lastIndexOf("/api/v4");
6441
+ const prefix = idx >= 0 ? restPath.slice(0, idx) : "";
6442
+ const graphqlUrl = process.env.GITLAB_GRAPHQL_URL || `${apiUrl.origin}${prefix}/api/graphql`;
6443
+ const controller = new AbortController();
6444
+ const timeout = setTimeout(() => controller.abort(), 45000);
6445
+ try {
6446
+ const response = await fetch(graphqlUrl, {
6447
+ ...getFetchConfig(),
6448
+ method: "POST",
6449
+ headers: {
6450
+ ...BASE_HEADERS,
6451
+ ...buildAuthHeaders(),
6452
+ },
6453
+ body: JSON.stringify({ query, variables }),
6454
+ signal: controller.signal,
6455
+ });
6456
+ if (!response.ok) {
6457
+ await handleGitLabError(response);
6458
+ }
6459
+ return await response.json();
6460
+ }
6461
+ finally {
6462
+ clearTimeout(timeout);
6463
+ }
6464
+ }
6422
6465
  // Request handlers are now registered inside createServer() factory function
6423
6466
  // to ensure each transport connection gets its own Server instance (GHSA-345p-7cg4-v4c7).
6424
6467
  async function handleToolCall(params) {
@@ -6482,7 +6525,7 @@ async function handleToolCall(params) {
6482
6525
  }
6483
6526
  const json = await response.json();
6484
6527
  return {
6485
- content: [{ type: "text", text: JSON.stringify(json, null, 2) }],
6528
+ content: [{ type: "text", text: JSON.stringify(json) }],
6486
6529
  };
6487
6530
  }
6488
6531
  catch (err) {
@@ -6491,7 +6534,7 @@ async function handleToolCall(params) {
6491
6534
  content: [
6492
6535
  {
6493
6536
  type: "text",
6494
- text: JSON.stringify({ error: `GraphQL request failed: ${message}` }, null, 2),
6537
+ text: JSON.stringify({ error: `GraphQL request failed: ${message}` }),
6495
6538
  },
6496
6539
  ],
6497
6540
  };
@@ -6506,7 +6549,7 @@ async function handleToolCall(params) {
6506
6549
  try {
6507
6550
  const forkedProject = await forkProject(forkArgs.project_id, forkArgs.namespace);
6508
6551
  return {
6509
- content: [{ type: "text", text: JSON.stringify(forkedProject, null, 2) }],
6552
+ content: [{ type: "text", text: JSON.stringify(forkedProject) }],
6510
6553
  };
6511
6554
  }
6512
6555
  catch (forkError) {
@@ -6519,7 +6562,7 @@ async function handleToolCall(params) {
6519
6562
  content: [
6520
6563
  {
6521
6564
  type: "text",
6522
- text: JSON.stringify({ error: forkErrorMessage }, null, 2),
6565
+ text: JSON.stringify({ error: forkErrorMessage }),
6523
6566
  },
6524
6567
  ],
6525
6568
  };
@@ -6536,7 +6579,7 @@ async function handleToolCall(params) {
6536
6579
  ref,
6537
6580
  });
6538
6581
  return {
6539
- content: [{ type: "text", text: JSON.stringify(branch, null, 2) }],
6582
+ content: [{ type: "text", text: JSON.stringify(branch) }],
6540
6583
  };
6541
6584
  }
6542
6585
  case "get_branch_diffs": {
@@ -6544,14 +6587,14 @@ async function handleToolCall(params) {
6544
6587
  const diffResp = await getBranchDiffs(args.project_id, args.from, args.to, args.straight);
6545
6588
  diffResp.diffs = filterDiffsByPatterns(diffResp.diffs, args.excluded_file_patterns);
6546
6589
  return {
6547
- content: [{ type: "text", text: JSON.stringify(diffResp, null, 2) }],
6590
+ content: [{ type: "text", text: JSON.stringify(diffResp) }],
6548
6591
  };
6549
6592
  }
6550
6593
  case "search_repositories": {
6551
6594
  const args = SearchRepositoriesSchema.parse(params.arguments);
6552
6595
  const results = await searchProjects(args.search, args.page, args.per_page);
6553
6596
  return {
6554
- content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
6597
+ content: [{ type: "text", text: JSON.stringify(results) }],
6555
6598
  };
6556
6599
  }
6557
6600
  case "search_code": {
@@ -6565,7 +6608,7 @@ async function handleToolCall(params) {
6565
6608
  per_page: args.per_page,
6566
6609
  });
6567
6610
  return {
6568
- content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
6611
+ content: [{ type: "text", text: JSON.stringify(results) }],
6569
6612
  };
6570
6613
  }
6571
6614
  case "search_project_code": {
@@ -6581,7 +6624,7 @@ async function handleToolCall(params) {
6581
6624
  per_page: args.per_page,
6582
6625
  });
6583
6626
  return {
6584
- content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
6627
+ content: [{ type: "text", text: JSON.stringify(results) }],
6585
6628
  };
6586
6629
  }
6587
6630
  case "search_group_code": {
@@ -6596,7 +6639,7 @@ async function handleToolCall(params) {
6596
6639
  per_page: args.per_page,
6597
6640
  });
6598
6641
  return {
6599
- content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
6642
+ content: [{ type: "text", text: JSON.stringify(results) }],
6600
6643
  };
6601
6644
  }
6602
6645
  case "create_repository": {
@@ -6604,7 +6647,7 @@ async function handleToolCall(params) {
6604
6647
  const args = CreateRepositorySchema.parse(params.arguments);
6605
6648
  const repository = await createRepository(args);
6606
6649
  return {
6607
- content: [{ type: "text", text: JSON.stringify(repository, null, 2) }],
6650
+ content: [{ type: "text", text: JSON.stringify(repository) }],
6608
6651
  };
6609
6652
  }
6610
6653
  case "create_group": {
@@ -6631,28 +6674,28 @@ async function handleToolCall(params) {
6631
6674
  const data = await response.json();
6632
6675
  const group = GitLabGroupSchema.parse(data);
6633
6676
  return {
6634
- content: [{ type: "text", text: JSON.stringify(group, null, 2) }],
6677
+ content: [{ type: "text", text: JSON.stringify(group) }],
6635
6678
  };
6636
6679
  }
6637
6680
  case "get_file_contents": {
6638
6681
  const args = GetFileContentsSchema.parse(params.arguments);
6639
6682
  const contents = await getFileContents(args.project_id, args.file_path, args.ref);
6640
6683
  return {
6641
- content: [{ type: "text", text: JSON.stringify(contents, null, 2) }],
6684
+ content: [{ type: "text", text: JSON.stringify(contents) }],
6642
6685
  };
6643
6686
  }
6644
6687
  case "create_or_update_file": {
6645
6688
  const args = CreateOrUpdateFileSchema.parse(params.arguments);
6646
6689
  const result = await createOrUpdateFile(args.project_id, args.file_path, args.content, args.commit_message, args.branch, args.previous_path, args.last_commit_id, args.commit_id);
6647
6690
  return {
6648
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
6691
+ content: [{ type: "text", text: JSON.stringify(result) }],
6649
6692
  };
6650
6693
  }
6651
6694
  case "push_files": {
6652
6695
  const args = PushFilesSchema.parse(params.arguments);
6653
6696
  const result = await createCommit(args.project_id, args.commit_message, args.branch, args.files.map(f => ({ path: f.file_path, content: f.content })));
6654
6697
  return {
6655
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
6698
+ content: [{ type: "text", text: JSON.stringify(result) }],
6656
6699
  };
6657
6700
  }
6658
6701
  case "create_issue": {
@@ -6660,7 +6703,7 @@ async function handleToolCall(params) {
6660
6703
  const { project_id, ...options } = args;
6661
6704
  const issue = await createIssue(project_id, options);
6662
6705
  return {
6663
- content: [{ type: "text", text: JSON.stringify(issue, null, 2) }],
6706
+ content: [{ type: "text", text: JSON.stringify(issue) }],
6664
6707
  };
6665
6708
  }
6666
6709
  case "create_merge_request": {
@@ -6668,7 +6711,7 @@ async function handleToolCall(params) {
6668
6711
  const { project_id, ...options } = args;
6669
6712
  const mergeRequest = await createMergeRequest(project_id, options);
6670
6713
  return {
6671
- content: [{ type: "text", text: JSON.stringify(mergeRequest, null, 2) }],
6714
+ content: [{ type: "text", text: JSON.stringify(mergeRequest) }],
6672
6715
  };
6673
6716
  }
6674
6717
  case "delete_merge_request_discussion_note": {
@@ -6685,21 +6728,21 @@ async function handleToolCall(params) {
6685
6728
  args.resolved // Now one of body or resolved must be provided, not both
6686
6729
  );
6687
6730
  return {
6688
- content: [{ type: "text", text: JSON.stringify(note, null, 2) }],
6731
+ content: [{ type: "text", text: JSON.stringify(note) }],
6689
6732
  };
6690
6733
  }
6691
6734
  case "create_merge_request_discussion_note": {
6692
6735
  const args = CreateMergeRequestDiscussionNoteSchema.parse(params.arguments);
6693
6736
  const note = await createMergeRequestDiscussionNote(args.project_id, args.merge_request_iid, args.discussion_id, args.body, args.created_at);
6694
6737
  return {
6695
- content: [{ type: "text", text: JSON.stringify(note, null, 2) }],
6738
+ content: [{ type: "text", text: JSON.stringify(note) }],
6696
6739
  };
6697
6740
  }
6698
6741
  case "create_merge_request_note": {
6699
6742
  const args = CreateMergeRequestNoteSchema.parse(params.arguments);
6700
6743
  const note = await createMergeRequestNote(args.project_id, args.merge_request_iid, args.body);
6701
6744
  return {
6702
- content: [{ type: "text", text: JSON.stringify(note, null, 2) }],
6745
+ content: [{ type: "text", text: JSON.stringify(note) }],
6703
6746
  };
6704
6747
  }
6705
6748
  case "delete_merge_request_note": {
@@ -6713,121 +6756,141 @@ async function handleToolCall(params) {
6713
6756
  const args = GetMergeRequestNoteSchema.parse(params.arguments);
6714
6757
  const note = await getMergeRequestNote(args.project_id, args.merge_request_iid, args.note_id);
6715
6758
  return {
6716
- content: [{ type: "text", text: JSON.stringify(note, null, 2) }],
6759
+ content: [{ type: "text", text: JSON.stringify(note) }],
6717
6760
  };
6718
6761
  }
6719
6762
  case "get_merge_request_notes": {
6720
6763
  const args = GetMergeRequestNotesSchema.parse(params.arguments);
6721
6764
  const notes = await getMergeRequestNotes(args.project_id, args.merge_request_iid, args.sort, args.order_by, args.per_page, args.page);
6722
6765
  return {
6723
- content: [{ type: "text", text: JSON.stringify(notes, null, 2) }],
6766
+ content: [{ type: "text", text: JSON.stringify(notes) }],
6724
6767
  };
6725
6768
  }
6726
6769
  case "update_merge_request_note": {
6727
6770
  const args = UpdateMergeRequestNoteSchema.parse(params.arguments);
6728
6771
  const note = await updateMergeRequestNote(args.project_id, args.merge_request_iid, args.note_id, args.body);
6729
6772
  return {
6730
- content: [{ type: "text", text: JSON.stringify(note, null, 2) }],
6773
+ content: [{ type: "text", text: JSON.stringify(note) }],
6731
6774
  };
6732
6775
  }
6733
6776
  case "list_merge_request_emoji_reactions": {
6734
6777
  const args = ListMergeRequestEmojiReactionsSchema.parse(params.arguments);
6735
6778
  const path = buildAwardEmojiPath("merge_requests", args.project_id, args.merge_request_iid);
6736
6779
  const result = await listRestAwardEmoji(path);
6737
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
6780
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
6738
6781
  }
6739
6782
  case "list_merge_request_note_emoji_reactions": {
6740
6783
  const args = ListMergeRequestNoteEmojiReactionsSchema.parse(params.arguments);
6741
6784
  const path = buildAwardEmojiPath("merge_requests", args.project_id, args.merge_request_iid, { noteId: args.note_id, discussionId: args.discussion_id });
6742
6785
  const result = await listRestAwardEmoji(path);
6743
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
6786
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
6744
6787
  }
6745
6788
  case "create_merge_request_emoji_reaction": {
6746
6789
  const args = CreateMergeRequestEmojiReactionSchema.parse(params.arguments);
6747
6790
  const path = buildAwardEmojiPath("merge_requests", args.project_id, args.merge_request_iid);
6748
6791
  const result = await createRestAwardEmoji(path, args.name);
6749
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
6792
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
6750
6793
  }
6751
6794
  case "delete_merge_request_emoji_reaction": {
6752
6795
  const args = DeleteMergeRequestEmojiReactionSchema.parse(params.arguments);
6753
6796
  const path = buildAwardEmojiPath("merge_requests", args.project_id, args.merge_request_iid, { awardId: args.award_id });
6754
6797
  await deleteRestAwardEmoji(path);
6755
- return { content: [{ type: "text", text: "Merge request emoji reaction deleted successfully" }] };
6798
+ return {
6799
+ content: [{ type: "text", text: "Merge request emoji reaction deleted successfully" }],
6800
+ };
6756
6801
  }
6757
6802
  case "create_merge_request_note_emoji_reaction": {
6758
6803
  const args = CreateMergeRequestNoteEmojiReactionSchema.parse(params.arguments);
6759
6804
  const path = buildAwardEmojiPath("merge_requests", args.project_id, args.merge_request_iid, { noteId: args.note_id, discussionId: args.discussion_id });
6760
6805
  const result = await createRestAwardEmoji(path, args.name);
6761
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
6806
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
6762
6807
  }
6763
6808
  case "delete_merge_request_note_emoji_reaction": {
6764
6809
  const args = DeleteMergeRequestNoteEmojiReactionSchema.parse(params.arguments);
6765
6810
  const path = buildAwardEmojiPath("merge_requests", args.project_id, args.merge_request_iid, { noteId: args.note_id, discussionId: args.discussion_id, awardId: args.award_id });
6766
6811
  await deleteRestAwardEmoji(path);
6767
- return { content: [{ type: "text", text: "Merge request note emoji reaction deleted successfully" }] };
6812
+ return {
6813
+ content: [
6814
+ { type: "text", text: "Merge request note emoji reaction deleted successfully" },
6815
+ ],
6816
+ };
6768
6817
  }
6769
6818
  case "update_issue_note": {
6770
6819
  const args = UpdateIssueNoteSchema.parse(params.arguments);
6771
6820
  const note = await updateIssueNote(args.project_id, args.issue_iid, args.discussion_id, args.note_id, args.body, args.resolved);
6772
6821
  return {
6773
- content: [{ type: "text", text: JSON.stringify(note, null, 2) }],
6822
+ content: [{ type: "text", text: JSON.stringify(note) }],
6774
6823
  };
6775
6824
  }
6776
6825
  case "create_issue_note": {
6777
6826
  const args = CreateIssueNoteSchema.parse(params.arguments);
6778
6827
  const note = await createIssueNote(args.project_id, args.issue_iid, args.discussion_id, args.body, args.created_at);
6779
6828
  return {
6780
- content: [{ type: "text", text: JSON.stringify(note, null, 2) }],
6829
+ content: [{ type: "text", text: JSON.stringify(note) }],
6781
6830
  };
6782
6831
  }
6783
6832
  case "list_issue_emoji_reactions": {
6784
6833
  const args = ListIssueEmojiReactionsSchema.parse(params.arguments);
6785
6834
  const path = buildAwardEmojiPath("issues", args.project_id, args.issue_iid);
6786
6835
  const result = await listRestAwardEmoji(path);
6787
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
6836
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
6788
6837
  }
6789
6838
  case "list_issue_note_emoji_reactions": {
6790
6839
  const args = ListIssueNoteEmojiReactionsSchema.parse(params.arguments);
6791
- const path = buildAwardEmojiPath("issues", args.project_id, args.issue_iid, { noteId: args.note_id, discussionId: args.discussion_id });
6840
+ const path = buildAwardEmojiPath("issues", args.project_id, args.issue_iid, {
6841
+ noteId: args.note_id,
6842
+ discussionId: args.discussion_id,
6843
+ });
6792
6844
  const result = await listRestAwardEmoji(path);
6793
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
6845
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
6794
6846
  }
6795
6847
  case "create_issue_emoji_reaction": {
6796
6848
  const args = CreateIssueEmojiReactionSchema.parse(params.arguments);
6797
6849
  const path = buildAwardEmojiPath("issues", args.project_id, args.issue_iid);
6798
6850
  const result = await createRestAwardEmoji(path, args.name);
6799
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
6851
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
6800
6852
  }
6801
6853
  case "delete_issue_emoji_reaction": {
6802
6854
  const args = DeleteIssueEmojiReactionSchema.parse(params.arguments);
6803
- const path = buildAwardEmojiPath("issues", args.project_id, args.issue_iid, { awardId: args.award_id });
6855
+ const path = buildAwardEmojiPath("issues", args.project_id, args.issue_iid, {
6856
+ awardId: args.award_id,
6857
+ });
6804
6858
  await deleteRestAwardEmoji(path);
6805
6859
  return { content: [{ type: "text", text: "Issue emoji reaction deleted successfully" }] };
6806
6860
  }
6807
6861
  case "create_issue_note_emoji_reaction": {
6808
6862
  const args = CreateIssueNoteEmojiReactionSchema.parse(params.arguments);
6809
- const path = buildAwardEmojiPath("issues", args.project_id, args.issue_iid, { noteId: args.note_id, discussionId: args.discussion_id });
6863
+ const path = buildAwardEmojiPath("issues", args.project_id, args.issue_iid, {
6864
+ noteId: args.note_id,
6865
+ discussionId: args.discussion_id,
6866
+ });
6810
6867
  const result = await createRestAwardEmoji(path, args.name);
6811
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
6868
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
6812
6869
  }
6813
6870
  case "delete_issue_note_emoji_reaction": {
6814
6871
  const args = DeleteIssueNoteEmojiReactionSchema.parse(params.arguments);
6815
- const path = buildAwardEmojiPath("issues", args.project_id, args.issue_iid, { noteId: args.note_id, discussionId: args.discussion_id, awardId: args.award_id });
6872
+ const path = buildAwardEmojiPath("issues", args.project_id, args.issue_iid, {
6873
+ noteId: args.note_id,
6874
+ discussionId: args.discussion_id,
6875
+ awardId: args.award_id,
6876
+ });
6816
6877
  await deleteRestAwardEmoji(path);
6817
- return { content: [{ type: "text", text: "Issue note emoji reaction deleted successfully" }] };
6878
+ return {
6879
+ content: [{ type: "text", text: "Issue note emoji reaction deleted successfully" }],
6880
+ };
6818
6881
  }
6819
6882
  case "list_todos": {
6820
6883
  const args = ListTodosSchema.parse(params.arguments);
6821
6884
  const todos = await listTodos(args);
6822
6885
  return {
6823
- content: [{ type: "text", text: JSON.stringify(todos, null, 2) }],
6886
+ content: [{ type: "text", text: JSON.stringify(todos) }],
6824
6887
  };
6825
6888
  }
6826
6889
  case "mark_todo_done": {
6827
6890
  const args = MarkTodoDoneSchema.parse(params.arguments);
6828
6891
  const todo = await markTodoDone(args.id);
6829
6892
  return {
6830
- content: [{ type: "text", text: JSON.stringify(todo, null, 2) }],
6893
+ content: [{ type: "text", text: JSON.stringify(todo) }],
6831
6894
  };
6832
6895
  }
6833
6896
  case "mark_all_todos_done": {
@@ -6861,7 +6924,7 @@ async function handleToolCall(params) {
6861
6924
  content: [
6862
6925
  {
6863
6926
  type: "text",
6864
- text: JSON.stringify(mergeRequestWithDeploymentSummary, null, 2),
6927
+ text: JSON.stringify(mergeRequestWithDeploymentSummary),
6865
6928
  },
6866
6929
  ],
6867
6930
  };
@@ -6871,14 +6934,14 @@ async function handleToolCall(params) {
6871
6934
  const diffs = await getMergeRequestDiffs(args.project_id, args.merge_request_iid, args.source_branch, args.view);
6872
6935
  const filteredDiffs = filterDiffsByPatterns(diffs, args.excluded_file_patterns);
6873
6936
  return {
6874
- content: [{ type: "text", text: JSON.stringify(filteredDiffs, null, 2) }],
6937
+ content: [{ type: "text", text: JSON.stringify(filteredDiffs) }],
6875
6938
  };
6876
6939
  }
6877
6940
  case "list_merge_request_changed_files": {
6878
6941
  const args = ListMergeRequestChangedFilesSchema.parse(params.arguments);
6879
6942
  const files = await listMergeRequestChangedFiles(args.project_id, args.merge_request_iid, args.source_branch, args.excluded_file_patterns);
6880
6943
  return {
6881
- content: [{ type: "text", text: JSON.stringify(files, null, 2) }],
6944
+ content: [{ type: "text", text: JSON.stringify(files) }],
6882
6945
  };
6883
6946
  }
6884
6947
  case "list_merge_request_pipelines": {
@@ -6886,35 +6949,35 @@ async function handleToolCall(params) {
6886
6949
  const { project_id, merge_request_iid, ...options } = args;
6887
6950
  const pipelines = await listMergeRequestPipelines(project_id, merge_request_iid, options);
6888
6951
  return {
6889
- content: [{ type: "text", text: JSON.stringify(pipelines, null, 2) }],
6952
+ content: [{ type: "text", text: JSON.stringify(pipelines) }],
6890
6953
  };
6891
6954
  }
6892
6955
  case "list_merge_request_diffs": {
6893
6956
  const args = ListMergeRequestDiffsSchema.parse(params.arguments);
6894
6957
  const changes = await listMergeRequestDiffs(args.project_id, args.merge_request_iid, args.source_branch, args.page, args.per_page, args.unidiff);
6895
6958
  return {
6896
- content: [{ type: "text", text: JSON.stringify(changes, null, 2) }],
6959
+ content: [{ type: "text", text: JSON.stringify(changes) }],
6897
6960
  };
6898
6961
  }
6899
6962
  case "get_merge_request_file_diff": {
6900
6963
  const args = GetMergeRequestFileDiffSchema.parse(params.arguments);
6901
6964
  const fileDiff = await getMergeRequestFileDiff(args.project_id, args.file_paths, args.merge_request_iid, args.source_branch, args.unidiff);
6902
6965
  return {
6903
- content: [{ type: "text", text: JSON.stringify(fileDiff, null, 2) }],
6966
+ content: [{ type: "text", text: JSON.stringify(fileDiff) }],
6904
6967
  };
6905
6968
  }
6906
6969
  case "list_merge_request_versions": {
6907
6970
  const args = ListMergeRequestVersionsSchema.parse(params.arguments);
6908
6971
  const versions = await listMergeRequestVersions(args.project_id, args.merge_request_iid);
6909
6972
  return {
6910
- content: [{ type: "text", text: JSON.stringify(versions, null, 2) }],
6973
+ content: [{ type: "text", text: JSON.stringify(versions) }],
6911
6974
  };
6912
6975
  }
6913
6976
  case "get_merge_request_version": {
6914
6977
  const args = GetMergeRequestVersionSchema.parse(params.arguments);
6915
6978
  const version = await getMergeRequestVersion(args.project_id, args.merge_request_iid, args.version_id, args.unidiff);
6916
6979
  return {
6917
- content: [{ type: "text", text: JSON.stringify(version, null, 2) }],
6980
+ content: [{ type: "text", text: JSON.stringify(version) }],
6918
6981
  };
6919
6982
  }
6920
6983
  case "update_merge_request": {
@@ -6922,7 +6985,7 @@ async function handleToolCall(params) {
6922
6985
  const { project_id, merge_request_iid, source_branch, ...options } = args;
6923
6986
  const mergeRequest = await updateMergeRequest(project_id, options, merge_request_iid, source_branch);
6924
6987
  return {
6925
- content: [{ type: "text", text: JSON.stringify(mergeRequest, null, 2) }],
6988
+ content: [{ type: "text", text: JSON.stringify(mergeRequest) }],
6926
6989
  };
6927
6990
  }
6928
6991
  case "merge_merge_request": {
@@ -6930,35 +6993,35 @@ async function handleToolCall(params) {
6930
6993
  const { project_id, merge_request_iid, ...options } = args;
6931
6994
  const mergeRequest = await mergeMergeRequest(project_id, options, merge_request_iid);
6932
6995
  return {
6933
- content: [{ type: "text", text: JSON.stringify(mergeRequest, null, 2) }],
6996
+ content: [{ type: "text", text: JSON.stringify(mergeRequest) }],
6934
6997
  };
6935
6998
  }
6936
6999
  case "approve_merge_request": {
6937
7000
  const args = ApproveMergeRequestSchema.parse(params.arguments);
6938
7001
  const approvalState = await approveMergeRequest(args.project_id, args.merge_request_iid, args.sha, args.approval_password);
6939
7002
  return {
6940
- content: [{ type: "text", text: JSON.stringify(approvalState, null, 2) }],
7003
+ content: [{ type: "text", text: JSON.stringify(approvalState) }],
6941
7004
  };
6942
7005
  }
6943
7006
  case "unapprove_merge_request": {
6944
7007
  const args = UnapproveMergeRequestSchema.parse(params.arguments);
6945
7008
  const approvalState = await unapproveMergeRequest(args.project_id, args.merge_request_iid);
6946
7009
  return {
6947
- content: [{ type: "text", text: JSON.stringify(approvalState, null, 2) }],
7010
+ content: [{ type: "text", text: JSON.stringify(approvalState) }],
6948
7011
  };
6949
7012
  }
6950
7013
  case "get_merge_request_approval_state": {
6951
7014
  const args = GetMergeRequestApprovalStateSchema.parse(params.arguments);
6952
7015
  const approvalState = await getMergeRequestApprovalState(args.project_id, args.merge_request_iid);
6953
7016
  return {
6954
- content: [{ type: "text", text: JSON.stringify(approvalState, null, 2) }],
7017
+ content: [{ type: "text", text: JSON.stringify(approvalState) }],
6955
7018
  };
6956
7019
  }
6957
7020
  case "get_merge_request_conflicts": {
6958
7021
  const args = GetMergeRequestConflictsSchema.parse(params.arguments);
6959
7022
  const conflicts = await getMergeRequestConflicts(args.project_id, args.merge_request_iid);
6960
7023
  return {
6961
- content: [{ type: "text", text: JSON.stringify(conflicts, null, 2) }],
7024
+ content: [{ type: "text", text: JSON.stringify(conflicts) }],
6962
7025
  };
6963
7026
  }
6964
7027
  case "mr_discussions": {
@@ -6966,7 +7029,7 @@ async function handleToolCall(params) {
6966
7029
  const { project_id, merge_request_iid, ...options } = args;
6967
7030
  const discussions = await listMergeRequestDiscussions(project_id, merge_request_iid, options);
6968
7031
  return {
6969
- content: [{ type: "text", text: JSON.stringify(discussions, null, 2) }],
7032
+ content: [{ type: "text", text: JSON.stringify(discussions) }],
6970
7033
  };
6971
7034
  }
6972
7035
  case "list_namespaces": {
@@ -6991,7 +7054,7 @@ async function handleToolCall(params) {
6991
7054
  const data = await response.json();
6992
7055
  const namespaces = z.array(GitLabNamespaceSchema).parse(data);
6993
7056
  return {
6994
- content: [{ type: "text", text: JSON.stringify(namespaces, null, 2) }],
7057
+ content: [{ type: "text", text: JSON.stringify(namespaces) }],
6995
7058
  };
6996
7059
  }
6997
7060
  case "get_namespace": {
@@ -7004,7 +7067,7 @@ async function handleToolCall(params) {
7004
7067
  const data = await response.json();
7005
7068
  const namespace = GitLabNamespaceSchema.parse(data);
7006
7069
  return {
7007
- content: [{ type: "text", text: JSON.stringify(namespace, null, 2) }],
7070
+ content: [{ type: "text", text: JSON.stringify(namespace) }],
7008
7071
  };
7009
7072
  }
7010
7073
  case "verify_namespace": {
@@ -7019,7 +7082,7 @@ async function handleToolCall(params) {
7019
7082
  const data = await response.json();
7020
7083
  const namespaceExists = GitLabNamespaceExistsResponseSchema.parse(data);
7021
7084
  return {
7022
- content: [{ type: "text", text: JSON.stringify(namespaceExists, null, 2) }],
7085
+ content: [{ type: "text", text: JSON.stringify(namespaceExists) }],
7023
7086
  };
7024
7087
  }
7025
7088
  case "get_project": {
@@ -7040,14 +7103,29 @@ async function handleToolCall(params) {
7040
7103
  const data = await response.json();
7041
7104
  // Return raw data without parsing through our schema to avoid type mismatches in tests
7042
7105
  return {
7043
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
7106
+ content: [{ type: "text", text: JSON.stringify(data) }],
7044
7107
  };
7045
7108
  }
7046
7109
  case "list_projects": {
7047
7110
  const args = ListProjectsSchema.parse(params.arguments);
7048
7111
  const projects = await listProjects(args);
7049
7112
  return {
7050
- content: [{ type: "text", text: JSON.stringify(projects, null, 2) }],
7113
+ content: [{ type: "text", text: JSON.stringify(projects) }],
7114
+ };
7115
+ }
7116
+ case "update_project": {
7117
+ const { project_id, ...updates } = UpdateProjectSchema.parse(params.arguments);
7118
+ const effectiveProjectId = getEffectiveProjectId(project_id);
7119
+ const body = Object.fromEntries(Object.entries(updates).filter(([, value]) => value !== undefined));
7120
+ const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}`, {
7121
+ ...getFetchConfig(),
7122
+ method: "PUT",
7123
+ body: JSON.stringify(body),
7124
+ });
7125
+ await handleGitLabError(response);
7126
+ const data = await response.json();
7127
+ return {
7128
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
7051
7129
  };
7052
7130
  }
7053
7131
  case "list_project_members": {
@@ -7055,14 +7133,14 @@ async function handleToolCall(params) {
7055
7133
  const { project_id, ...options } = args;
7056
7134
  const members = await listProjectMembers(project_id, options);
7057
7135
  return {
7058
- content: [{ type: "text", text: JSON.stringify(members, null, 2) }],
7136
+ content: [{ type: "text", text: JSON.stringify(members) }],
7059
7137
  };
7060
7138
  }
7061
7139
  case "get_users": {
7062
7140
  const args = GetUsersSchema.parse(params.arguments);
7063
7141
  const usersMap = await getUsers(args.usernames);
7064
7142
  return {
7065
- content: [{ type: "text", text: JSON.stringify(usersMap, null, 2) }],
7143
+ content: [{ type: "text", text: JSON.stringify(usersMap) }],
7066
7144
  };
7067
7145
  }
7068
7146
  case "get_user": {
@@ -7075,7 +7153,7 @@ async function handleToolCall(params) {
7075
7153
  const data = await response.json();
7076
7154
  const user = GitLabUserFullSchema.parse(data);
7077
7155
  return {
7078
- content: [{ type: "text", text: JSON.stringify(user, null, 2) }],
7156
+ content: [{ type: "text", text: JSON.stringify(user) }],
7079
7157
  };
7080
7158
  }
7081
7159
  case "whoami": {
@@ -7088,7 +7166,7 @@ async function handleToolCall(params) {
7088
7166
  const data = await response.json();
7089
7167
  const user = GitLabCurrentUserSchema.parse(data);
7090
7168
  return {
7091
- content: [{ type: "text", text: JSON.stringify(user, null, 2) }],
7169
+ content: [{ type: "text", text: JSON.stringify(user) }],
7092
7170
  };
7093
7171
  }
7094
7172
  case "create_note": {
@@ -7096,7 +7174,7 @@ async function handleToolCall(params) {
7096
7174
  const { project_id, noteable_type, noteable_iid, body } = args;
7097
7175
  const note = await createNote(project_id, noteable_type, noteable_iid, body);
7098
7176
  return {
7099
- content: [{ type: "text", text: JSON.stringify(note, null, 2) }],
7177
+ content: [{ type: "text", text: JSON.stringify(note) }],
7100
7178
  };
7101
7179
  }
7102
7180
  case "get_draft_note": {
@@ -7104,7 +7182,7 @@ async function handleToolCall(params) {
7104
7182
  const { project_id, merge_request_iid, draft_note_id } = args;
7105
7183
  const draftNote = await getDraftNote(project_id, merge_request_iid, draft_note_id);
7106
7184
  return {
7107
- content: [{ type: "text", text: JSON.stringify(draftNote, null, 2) }],
7185
+ content: [{ type: "text", text: JSON.stringify(draftNote) }],
7108
7186
  };
7109
7187
  }
7110
7188
  case "list_draft_notes": {
@@ -7112,15 +7190,15 @@ async function handleToolCall(params) {
7112
7190
  const { project_id, merge_request_iid } = args;
7113
7191
  const draftNotes = await listDraftNotes(project_id, merge_request_iid);
7114
7192
  return {
7115
- content: [{ type: "text", text: JSON.stringify(draftNotes, null, 2) }],
7193
+ content: [{ type: "text", text: JSON.stringify(draftNotes) }],
7116
7194
  };
7117
7195
  }
7118
7196
  case "create_draft_note": {
7119
7197
  const args = CreateDraftNoteSchema.parse(params.arguments);
7120
- const { project_id, merge_request_iid, body, in_reply_to_discussion_id, position, resolve_discussion } = args;
7198
+ const { project_id, merge_request_iid, body, in_reply_to_discussion_id, position, resolve_discussion, } = args;
7121
7199
  const draftNote = await createDraftNote(project_id, merge_request_iid, body, in_reply_to_discussion_id, position, resolve_discussion);
7122
7200
  return {
7123
- content: [{ type: "text", text: JSON.stringify(draftNote, null, 2) }],
7201
+ content: [{ type: "text", text: JSON.stringify(draftNote) }],
7124
7202
  };
7125
7203
  }
7126
7204
  case "update_draft_note": {
@@ -7128,7 +7206,7 @@ async function handleToolCall(params) {
7128
7206
  const { project_id, merge_request_iid, draft_note_id, body, position, resolve_discussion } = args;
7129
7207
  const draftNote = await updateDraftNote(project_id, merge_request_iid, draft_note_id, body, position, resolve_discussion);
7130
7208
  return {
7131
- content: [{ type: "text", text: JSON.stringify(draftNote, null, 2) }],
7209
+ content: [{ type: "text", text: JSON.stringify(draftNote) }],
7132
7210
  };
7133
7211
  }
7134
7212
  case "delete_draft_note": {
@@ -7144,7 +7222,7 @@ async function handleToolCall(params) {
7144
7222
  const { project_id, merge_request_iid, draft_note_id } = args;
7145
7223
  const publishedNote = await publishDraftNote(project_id, merge_request_iid, draft_note_id);
7146
7224
  return {
7147
- content: [{ type: "text", text: JSON.stringify(publishedNote, null, 2) }],
7225
+ content: [{ type: "text", text: JSON.stringify(publishedNote) }],
7148
7226
  };
7149
7227
  }
7150
7228
  case "bulk_publish_draft_notes": {
@@ -7152,7 +7230,7 @@ async function handleToolCall(params) {
7152
7230
  const { project_id, merge_request_iid } = args;
7153
7231
  const publishedNotes = await bulkPublishDraftNotes(project_id, merge_request_iid);
7154
7232
  return {
7155
- content: [{ type: "text", text: JSON.stringify(publishedNotes, null, 2) }],
7233
+ content: [{ type: "text", text: JSON.stringify(publishedNotes) }],
7156
7234
  };
7157
7235
  }
7158
7236
  case "create_merge_request_thread": {
@@ -7160,7 +7238,7 @@ async function handleToolCall(params) {
7160
7238
  const { project_id, merge_request_iid, body, position, created_at } = args;
7161
7239
  const thread = await createMergeRequestThread(project_id, merge_request_iid, body, position, created_at);
7162
7240
  return {
7163
- content: [{ type: "text", text: JSON.stringify(thread, null, 2) }],
7241
+ content: [{ type: "text", text: JSON.stringify(thread) }],
7164
7242
  };
7165
7243
  }
7166
7244
  case "resolve_merge_request_thread": {
@@ -7177,21 +7255,21 @@ async function handleToolCall(params) {
7177
7255
  const cleanedOptions = cleanMutuallyExclusiveIdUsernameOptions(options);
7178
7256
  const issues = await listIssues(project_id, cleanedOptions);
7179
7257
  return {
7180
- content: [{ type: "text", text: JSON.stringify(issues, null, 2) }],
7258
+ content: [{ type: "text", text: JSON.stringify(issues) }],
7181
7259
  };
7182
7260
  }
7183
7261
  case "my_issues": {
7184
7262
  const args = MyIssuesSchema.parse(params.arguments);
7185
7263
  const issues = await myIssues(args);
7186
7264
  return {
7187
- content: [{ type: "text", text: JSON.stringify(issues, null, 2) }],
7265
+ content: [{ type: "text", text: JSON.stringify(issues) }],
7188
7266
  };
7189
7267
  }
7190
7268
  case "get_issue": {
7191
7269
  const args = GetIssueSchema.parse(params.arguments);
7192
7270
  const issue = await getIssue(args.project_id, args.issue_iid);
7193
7271
  return {
7194
- content: [{ type: "text", text: JSON.stringify(issue, null, 2) }],
7272
+ content: [{ type: "text", text: JSON.stringify(issue) }],
7195
7273
  };
7196
7274
  }
7197
7275
  case "update_issue": {
@@ -7199,7 +7277,7 @@ async function handleToolCall(params) {
7199
7277
  const { project_id, issue_iid, ...options } = args;
7200
7278
  const issue = await updateIssue(project_id, issue_iid, options);
7201
7279
  return {
7202
- content: [{ type: "text", text: JSON.stringify(issue, null, 2) }],
7280
+ content: [{ type: "text", text: JSON.stringify(issue) }],
7203
7281
  };
7204
7282
  }
7205
7283
  case "update_issue_description_patch": {
@@ -7293,7 +7371,7 @@ async function handleToolCall(params) {
7293
7371
  const args = ListIssueLinksSchema.parse(params.arguments);
7294
7372
  const links = await listIssueLinks(args.project_id, args.issue_iid);
7295
7373
  return {
7296
- content: [{ type: "text", text: JSON.stringify(links, null, 2) }],
7374
+ content: [{ type: "text", text: JSON.stringify(links) }],
7297
7375
  };
7298
7376
  }
7299
7377
  case "list_issue_discussions": {
@@ -7301,21 +7379,21 @@ async function handleToolCall(params) {
7301
7379
  const { project_id, issue_iid, ...options } = args;
7302
7380
  const discussions = await listIssueDiscussions(project_id, issue_iid, options);
7303
7381
  return {
7304
- content: [{ type: "text", text: JSON.stringify(discussions, null, 2) }],
7382
+ content: [{ type: "text", text: JSON.stringify(discussions) }],
7305
7383
  };
7306
7384
  }
7307
7385
  case "get_issue_link": {
7308
7386
  const args = GetIssueLinkSchema.parse(params.arguments);
7309
7387
  const link = await getIssueLink(args.project_id, args.issue_iid, args.issue_link_id);
7310
7388
  return {
7311
- content: [{ type: "text", text: JSON.stringify(link, null, 2) }],
7389
+ content: [{ type: "text", text: JSON.stringify(link) }],
7312
7390
  };
7313
7391
  }
7314
7392
  case "create_issue_link": {
7315
7393
  const args = CreateIssueLinkSchema.parse(params.arguments);
7316
7394
  const link = await createIssueLink(args.project_id, args.issue_iid, args.target_project_id, args.target_issue_iid, args.link_type);
7317
7395
  return {
7318
- content: [{ type: "text", text: JSON.stringify(link, null, 2) }],
7396
+ content: [{ type: "text", text: JSON.stringify(link) }],
7319
7397
  };
7320
7398
  }
7321
7399
  case "delete_issue_link": {
@@ -7337,7 +7415,7 @@ async function handleToolCall(params) {
7337
7415
  const args = GetWorkItemSchema.parse(params.arguments);
7338
7416
  const result = await getWorkItem(args.project_id, args.iid);
7339
7417
  return {
7340
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
7418
+ content: [{ type: "text", text: JSON.stringify(result) }],
7341
7419
  };
7342
7420
  }
7343
7421
  case "list_work_items": {
@@ -7345,7 +7423,7 @@ async function handleToolCall(params) {
7345
7423
  const { project_id, ...options } = args;
7346
7424
  const result = await listWorkItems(project_id, options);
7347
7425
  return {
7348
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
7426
+ content: [{ type: "text", text: JSON.stringify(result) }],
7349
7427
  };
7350
7428
  }
7351
7429
  case "create_work_item": {
@@ -7353,7 +7431,7 @@ async function handleToolCall(params) {
7353
7431
  const { project_id, ...options } = args;
7354
7432
  const result = await createWorkItem(project_id, options);
7355
7433
  return {
7356
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
7434
+ content: [{ type: "text", text: JSON.stringify(result) }],
7357
7435
  };
7358
7436
  }
7359
7437
  case "update_work_item": {
@@ -7361,117 +7439,117 @@ async function handleToolCall(params) {
7361
7439
  const { project_id, iid, ...options } = args;
7362
7440
  const result = await updateWorkItem(project_id, iid, options);
7363
7441
  return {
7364
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
7442
+ content: [{ type: "text", text: JSON.stringify(result) }],
7365
7443
  };
7366
7444
  }
7367
7445
  case "convert_work_item_type": {
7368
7446
  const args = ConvertWorkItemTypeSchema.parse(params.arguments);
7369
7447
  const result = await convertIssueType(args.project_id, args.iid, args.new_type);
7370
7448
  return {
7371
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
7449
+ content: [{ type: "text", text: JSON.stringify(result) }],
7372
7450
  };
7373
7451
  }
7374
7452
  case "list_work_item_statuses": {
7375
7453
  const args = ListWorkItemStatusesSchema.parse(params.arguments);
7376
7454
  const result = await listIssueStatuses(args.project_id, args.work_item_type);
7377
7455
  return {
7378
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
7456
+ content: [{ type: "text", text: JSON.stringify(result) }],
7379
7457
  };
7380
7458
  }
7381
7459
  case "list_custom_field_definitions": {
7382
7460
  const args = ListCustomFieldDefinitionsSchema.parse(params.arguments);
7383
7461
  const result = await listCustomFieldDefinitions(args.project_id, args.work_item_type);
7384
7462
  return {
7385
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
7463
+ content: [{ type: "text", text: JSON.stringify(result) }],
7386
7464
  };
7387
7465
  }
7388
7466
  case "move_work_item": {
7389
7467
  const args = MoveWorkItemSchema.parse(params.arguments);
7390
7468
  const result = await moveWorkItem(args.project_id, args.iid, args.target_project_id);
7391
7469
  return {
7392
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
7470
+ content: [{ type: "text", text: JSON.stringify(result) }],
7393
7471
  };
7394
7472
  }
7395
7473
  case "list_work_item_notes": {
7396
7474
  const args = ListWorkItemNotesSchema.parse(params.arguments);
7397
7475
  const result = await listWorkItemNotes(args.project_id, args.iid, args);
7398
7476
  return {
7399
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
7477
+ content: [{ type: "text", text: JSON.stringify(result) }],
7400
7478
  };
7401
7479
  }
7402
7480
  case "create_work_item_note": {
7403
7481
  const args = CreateWorkItemNoteSchema.parse(params.arguments);
7404
7482
  const result = await createWorkItemNote(args.project_id, args.iid, args.body, args);
7405
7483
  return {
7406
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
7484
+ content: [{ type: "text", text: JSON.stringify(result) }],
7407
7485
  };
7408
7486
  }
7409
7487
  case "list_work_item_emoji_reactions": {
7410
7488
  const args = ListWorkItemEmojiReactionsSchema.parse(params.arguments);
7411
7489
  const { workItemGID } = await resolveWorkItemGID(args.project_id, args.iid);
7412
7490
  const result = await listGraphQLAwardEmoji(workItemGID);
7413
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
7491
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
7414
7492
  }
7415
7493
  case "list_work_item_note_emoji_reactions": {
7416
7494
  const args = ListWorkItemNoteEmojiReactionsSchema.parse(params.arguments);
7417
7495
  const result = await listGraphQLAwardEmoji(args.note_id);
7418
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
7496
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
7419
7497
  }
7420
7498
  case "create_work_item_emoji_reaction": {
7421
7499
  const args = CreateWorkItemEmojiReactionSchema.parse(params.arguments);
7422
7500
  const { workItemGID } = await resolveWorkItemGID(args.project_id, args.iid);
7423
7501
  const result = await addGraphQLAwardEmoji(workItemGID, args.name);
7424
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
7502
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
7425
7503
  }
7426
7504
  case "delete_work_item_emoji_reaction": {
7427
7505
  const args = DeleteWorkItemEmojiReactionSchema.parse(params.arguments);
7428
7506
  const { workItemGID } = await resolveWorkItemGID(args.project_id, args.iid);
7429
7507
  const result = await removeGraphQLAwardEmoji(workItemGID, args.name);
7430
- return { content: [{ type: "text", text: JSON.stringify(result ?? { status: "success", message: "Work item emoji reaction removed" }, null, 2) }] };
7508
+ return { content: [{ type: "text", text: JSON.stringify(result ?? { status: "success", message: "Work item emoji reaction removed" }) }] };
7431
7509
  }
7432
7510
  case "create_work_item_note_emoji_reaction": {
7433
7511
  const args = CreateWorkItemNoteEmojiReactionSchema.parse(params.arguments);
7434
7512
  const result = await addGraphQLAwardEmoji(args.note_id, args.name);
7435
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
7513
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
7436
7514
  }
7437
7515
  case "delete_work_item_note_emoji_reaction": {
7438
7516
  const args = DeleteWorkItemNoteEmojiReactionSchema.parse(params.arguments);
7439
7517
  const result = await removeGraphQLAwardEmoji(args.note_id, args.name);
7440
- return { content: [{ type: "text", text: JSON.stringify(result ?? { status: "success", message: "Work item note emoji reaction removed" }, null, 2) }] };
7518
+ return { content: [{ type: "text", text: JSON.stringify(result ?? { status: "success", message: "Work item note emoji reaction removed" }) }] };
7441
7519
  }
7442
7520
  case "get_timeline_events": {
7443
7521
  const args = GetTimelineEventsSchema.parse(params.arguments);
7444
7522
  const result = await getTimelineEvents(args.project_id, args.incident_iid);
7445
7523
  return {
7446
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
7524
+ content: [{ type: "text", text: JSON.stringify(result) }],
7447
7525
  };
7448
7526
  }
7449
7527
  case "create_timeline_event": {
7450
7528
  const args = CreateTimelineEventSchema.parse(params.arguments);
7451
7529
  const result = await createTimelineEvent(args.project_id, args.incident_iid, args.note, args.occurred_at, args.tag_names);
7452
7530
  return {
7453
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
7531
+ content: [{ type: "text", text: JSON.stringify(result) }],
7454
7532
  };
7455
7533
  }
7456
7534
  case "list_labels": {
7457
7535
  const args = ListLabelsSchema.parse(params.arguments);
7458
7536
  const labels = await listLabels(args.project_id, args);
7459
7537
  return {
7460
- content: [{ type: "text", text: JSON.stringify(labels, null, 2) }],
7538
+ content: [{ type: "text", text: JSON.stringify(labels) }],
7461
7539
  };
7462
7540
  }
7463
7541
  case "get_label": {
7464
7542
  const args = GetLabelSchema.parse(params.arguments);
7465
7543
  const label = await getLabel(args.project_id, args.label_id, args.include_ancestor_groups);
7466
7544
  return {
7467
- content: [{ type: "text", text: JSON.stringify(label, null, 2) }],
7545
+ content: [{ type: "text", text: JSON.stringify(label) }],
7468
7546
  };
7469
7547
  }
7470
7548
  case "create_label": {
7471
7549
  const args = CreateLabelSchema.parse(params.arguments);
7472
7550
  const label = await createLabel(args.project_id, args);
7473
7551
  return {
7474
- content: [{ type: "text", text: JSON.stringify(label, null, 2) }],
7552
+ content: [{ type: "text", text: JSON.stringify(label) }],
7475
7553
  };
7476
7554
  }
7477
7555
  case "update_label": {
@@ -7479,7 +7557,7 @@ async function handleToolCall(params) {
7479
7557
  const { project_id, label_id, ...options } = args;
7480
7558
  const label = await updateLabel(project_id, label_id, options);
7481
7559
  return {
7482
- content: [{ type: "text", text: JSON.stringify(label, null, 2) }],
7560
+ content: [{ type: "text", text: JSON.stringify(label) }],
7483
7561
  };
7484
7562
  }
7485
7563
  case "delete_label": {
@@ -7498,7 +7576,7 @@ async function handleToolCall(params) {
7498
7576
  const args = ListGroupProjectsSchema.parse(params.arguments);
7499
7577
  const projects = await listGroupProjects(args);
7500
7578
  return {
7501
- content: [{ type: "text", text: JSON.stringify(projects, null, 2) }],
7579
+ content: [{ type: "text", text: JSON.stringify(projects) }],
7502
7580
  };
7503
7581
  }
7504
7582
  case "list_wiki_pages": {
@@ -7509,28 +7587,28 @@ async function handleToolCall(params) {
7509
7587
  with_content,
7510
7588
  });
7511
7589
  return {
7512
- content: [{ type: "text", text: JSON.stringify(wikiPages, null, 2) }],
7590
+ content: [{ type: "text", text: JSON.stringify(wikiPages) }],
7513
7591
  };
7514
7592
  }
7515
7593
  case "get_wiki_page": {
7516
7594
  const { project_id, slug } = GetWikiPageSchema.parse(params.arguments);
7517
7595
  const wikiPage = await getWikiPage(project_id, slug);
7518
7596
  return {
7519
- content: [{ type: "text", text: JSON.stringify(wikiPage, null, 2) }],
7597
+ content: [{ type: "text", text: JSON.stringify(wikiPage) }],
7520
7598
  };
7521
7599
  }
7522
7600
  case "create_wiki_page": {
7523
7601
  const { project_id, title, content, format } = CreateWikiPageSchema.parse(params.arguments);
7524
7602
  const wikiPage = await createWikiPage(project_id, title, content, format);
7525
7603
  return {
7526
- content: [{ type: "text", text: JSON.stringify(wikiPage, null, 2) }],
7604
+ content: [{ type: "text", text: JSON.stringify(wikiPage) }],
7527
7605
  };
7528
7606
  }
7529
7607
  case "update_wiki_page": {
7530
7608
  const { project_id, slug, title, content, format } = UpdateWikiPageSchema.parse(params.arguments);
7531
7609
  const wikiPage = await updateWikiPage(project_id, slug, title, content, format);
7532
7610
  return {
7533
- content: [{ type: "text", text: JSON.stringify(wikiPage, null, 2) }],
7611
+ content: [{ type: "text", text: JSON.stringify(wikiPage) }],
7534
7612
  };
7535
7613
  }
7536
7614
  case "delete_wiki_page": {
@@ -7556,28 +7634,28 @@ async function handleToolCall(params) {
7556
7634
  with_content,
7557
7635
  });
7558
7636
  return {
7559
- content: [{ type: "text", text: JSON.stringify(wikiPages, null, 2) }],
7637
+ content: [{ type: "text", text: JSON.stringify(wikiPages) }],
7560
7638
  };
7561
7639
  }
7562
7640
  case "get_group_wiki_page": {
7563
7641
  const { group_id, slug } = GetGroupWikiPageSchema.parse(params.arguments);
7564
7642
  const wikiPage = await getGroupWikiPage(group_id, slug);
7565
7643
  return {
7566
- content: [{ type: "text", text: JSON.stringify(wikiPage, null, 2) }],
7644
+ content: [{ type: "text", text: JSON.stringify(wikiPage) }],
7567
7645
  };
7568
7646
  }
7569
7647
  case "create_group_wiki_page": {
7570
7648
  const { group_id, title, content, format } = CreateGroupWikiPageSchema.parse(params.arguments);
7571
7649
  const wikiPage = await createGroupWikiPage(group_id, title, content, format);
7572
7650
  return {
7573
- content: [{ type: "text", text: JSON.stringify(wikiPage, null, 2) }],
7651
+ content: [{ type: "text", text: JSON.stringify(wikiPage) }],
7574
7652
  };
7575
7653
  }
7576
7654
  case "update_group_wiki_page": {
7577
7655
  const { group_id, slug, title, content, format } = UpdateGroupWikiPageSchema.parse(params.arguments);
7578
7656
  const wikiPage = await updateGroupWikiPage(group_id, slug, title, content, format);
7579
7657
  return {
7580
- content: [{ type: "text", text: JSON.stringify(wikiPage, null, 2) }],
7658
+ content: [{ type: "text", text: JSON.stringify(wikiPage) }],
7581
7659
  };
7582
7660
  }
7583
7661
  case "delete_group_wiki_page": {
@@ -7608,7 +7686,7 @@ async function handleToolCall(params) {
7608
7686
  }
7609
7687
  : items;
7610
7688
  return {
7611
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
7689
+ content: [{ type: "text", text: JSON.stringify(result) }],
7612
7690
  };
7613
7691
  }
7614
7692
  case "list_pipelines": {
@@ -7616,7 +7694,7 @@ async function handleToolCall(params) {
7616
7694
  const { project_id, ...options } = args;
7617
7695
  const pipelines = await listPipelines(project_id, options);
7618
7696
  return {
7619
- content: [{ type: "text", text: JSON.stringify(pipelines, null, 2) }],
7697
+ content: [{ type: "text", text: JSON.stringify(pipelines) }],
7620
7698
  };
7621
7699
  }
7622
7700
  case "get_pipeline": {
@@ -7626,7 +7704,7 @@ async function handleToolCall(params) {
7626
7704
  content: [
7627
7705
  {
7628
7706
  type: "text",
7629
- text: JSON.stringify(pipeline, null, 2),
7707
+ text: JSON.stringify(pipeline),
7630
7708
  },
7631
7709
  ],
7632
7710
  };
@@ -7636,14 +7714,14 @@ async function handleToolCall(params) {
7636
7714
  const { project_id, ...options } = args;
7637
7715
  const deployments = await listDeployments(project_id, options);
7638
7716
  return {
7639
- content: [{ type: "text", text: JSON.stringify(deployments, null, 2) }],
7717
+ content: [{ type: "text", text: JSON.stringify(deployments) }],
7640
7718
  };
7641
7719
  }
7642
7720
  case "get_deployment": {
7643
7721
  const { project_id, deployment_id } = GetDeploymentSchema.parse(params.arguments);
7644
7722
  const deployment = await getDeployment(project_id, deployment_id);
7645
7723
  return {
7646
- content: [{ type: "text", text: JSON.stringify(deployment, null, 2) }],
7724
+ content: [{ type: "text", text: JSON.stringify(deployment) }],
7647
7725
  };
7648
7726
  }
7649
7727
  case "list_environments": {
@@ -7651,14 +7729,14 @@ async function handleToolCall(params) {
7651
7729
  const { project_id, ...options } = args;
7652
7730
  const environments = await listEnvironments(project_id, options);
7653
7731
  return {
7654
- content: [{ type: "text", text: JSON.stringify(environments, null, 2) }],
7732
+ content: [{ type: "text", text: JSON.stringify(environments) }],
7655
7733
  };
7656
7734
  }
7657
7735
  case "get_environment": {
7658
7736
  const { project_id, environment_id } = GetEnvironmentSchema.parse(params.arguments);
7659
7737
  const environment = await getEnvironment(project_id, environment_id);
7660
7738
  return {
7661
- content: [{ type: "text", text: JSON.stringify(environment, null, 2) }],
7739
+ content: [{ type: "text", text: JSON.stringify(environment) }],
7662
7740
  };
7663
7741
  }
7664
7742
  case "list_pipeline_jobs": {
@@ -7668,7 +7746,7 @@ async function handleToolCall(params) {
7668
7746
  content: [
7669
7747
  {
7670
7748
  type: "text",
7671
- text: JSON.stringify(jobs, null, 2),
7749
+ text: JSON.stringify(jobs),
7672
7750
  },
7673
7751
  ],
7674
7752
  };
@@ -7680,7 +7758,7 @@ async function handleToolCall(params) {
7680
7758
  content: [
7681
7759
  {
7682
7760
  type: "text",
7683
- text: JSON.stringify(triggerJobs, null, 2),
7761
+ text: JSON.stringify(triggerJobs),
7684
7762
  },
7685
7763
  ],
7686
7764
  };
@@ -7692,7 +7770,7 @@ async function handleToolCall(params) {
7692
7770
  content: [
7693
7771
  {
7694
7772
  type: "text",
7695
- text: JSON.stringify(jobDetails, null, 2),
7773
+ text: JSON.stringify(jobDetails),
7696
7774
  },
7697
7775
  ],
7698
7776
  };
@@ -7714,7 +7792,7 @@ async function handleToolCall(params) {
7714
7792
  const { project_id, ...options } = args;
7715
7793
  const result = await validateCiLint(project_id, options);
7716
7794
  return {
7717
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
7795
+ content: [{ type: "text", text: JSON.stringify(result) }],
7718
7796
  };
7719
7797
  }
7720
7798
  case "validate_project_ci_lint": {
@@ -7722,8 +7800,130 @@ async function handleToolCall(params) {
7722
7800
  const { project_id, ...options } = args;
7723
7801
  const result = await validateProjectCiLint(project_id, options);
7724
7802
  return {
7725
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
7726
- };
7803
+ content: [{ type: "text", text: JSON.stringify(result) }],
7804
+ };
7805
+ }
7806
+ case "list_ci_catalog_resources": {
7807
+ const args = ListCiCatalogResourcesSchema.parse(params.arguments);
7808
+ const result = await executeGitLabGraphQL(`query ListCiCatalogResources(
7809
+ $search: String,
7810
+ $first: Int,
7811
+ $after: String,
7812
+ $groupIds: [GroupID!],
7813
+ $scope: CiCatalogResourceScope,
7814
+ $sort: CiCatalogResourceSort,
7815
+ $topics: [String!],
7816
+ $verificationLevel: CiCatalogResourceVerificationLevel
7817
+ ) {
7818
+ ciCatalogResources(
7819
+ search: $search,
7820
+ first: $first,
7821
+ after: $after,
7822
+ groupIds: $groupIds,
7823
+ scope: $scope,
7824
+ sort: $sort,
7825
+ topics: $topics,
7826
+ verificationLevel: $verificationLevel
7827
+ ) {
7828
+ nodes {
7829
+ id
7830
+ name
7831
+ description
7832
+ fullPath
7833
+ icon
7834
+ starCount
7835
+ topics
7836
+ verificationLevel
7837
+ visibilityLevel
7838
+ webPath
7839
+ latestReleasedAt
7840
+ last30DayUsageCount
7841
+ }
7842
+ pageInfo { hasNextPage endCursor }
7843
+ }
7844
+ }`, {
7845
+ search: args.search,
7846
+ first: args.first ?? 20,
7847
+ after: args.after,
7848
+ groupIds: args.group_ids,
7849
+ scope: args.scope,
7850
+ sort: args.sort,
7851
+ topics: args.topics,
7852
+ verificationLevel: args.verification_level,
7853
+ });
7854
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
7855
+ }
7856
+ case "get_ci_catalog_resource": {
7857
+ const args = GetCiCatalogResourceSchema.parse(params.arguments);
7858
+ const result = await executeGitLabGraphQL(`query GetCiCatalogResource(
7859
+ $id: CiCatalogResourceID,
7860
+ $fullPath: ID,
7861
+ $versionLimit: Int!,
7862
+ $componentLimit: Int!,
7863
+ $includeReadme: Boolean!
7864
+ ) {
7865
+ ciCatalogResource(id: $id, fullPath: $fullPath) {
7866
+ id
7867
+ name
7868
+ description
7869
+ fullPath
7870
+ icon
7871
+ starCount
7872
+ topics
7873
+ verificationLevel
7874
+ visibilityLevel
7875
+ webPath
7876
+ latestReleasedAt
7877
+ last30DayUsageCount
7878
+ versions(first: $versionLimit) {
7879
+ nodes {
7880
+ id
7881
+ name
7882
+ path
7883
+ createdAt
7884
+ releasedAt
7885
+ readme @include(if: $includeReadme)
7886
+ semver { major minor patch }
7887
+ components(first: $componentLimit) {
7888
+ nodes {
7889
+ id
7890
+ name
7891
+ description
7892
+ includePath
7893
+ last30DayUsageCount
7894
+ inputs {
7895
+ name
7896
+ description
7897
+ type
7898
+ required
7899
+ default
7900
+ options
7901
+ regex
7902
+ }
7903
+ }
7904
+ pageInfo { hasNextPage endCursor }
7905
+ }
7906
+ }
7907
+ pageInfo { hasNextPage endCursor }
7908
+ }
7909
+ }
7910
+ }`, {
7911
+ id: args.id,
7912
+ fullPath: args.full_path,
7913
+ versionLimit: args.version_limit ?? 5,
7914
+ componentLimit: args.component_limit ?? 20,
7915
+ includeReadme: args.include_readme ?? false,
7916
+ });
7917
+ if (args.component_name) {
7918
+ const resource = result?.data?.ciCatalogResource;
7919
+ for (const version of resource?.versions?.nodes ?? []) {
7920
+ const components = version?.components?.nodes;
7921
+ if (Array.isArray(components)) {
7922
+ version.components.nodes = components.filter(component => component?.name === args.component_name);
7923
+ }
7924
+ }
7925
+ }
7926
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
7727
7927
  }
7728
7928
  case "create_pipeline": {
7729
7929
  const { project_id, ref, variables, inputs } = CreatePipelineSchema.parse(params.arguments);
@@ -7804,7 +8004,7 @@ async function handleToolCall(params) {
7804
8004
  content: [
7805
8005
  {
7806
8006
  type: "text",
7807
- text: JSON.stringify(artifacts, null, 2),
8007
+ text: JSON.stringify(artifacts),
7808
8008
  },
7809
8009
  ],
7810
8010
  };
@@ -7817,7 +8017,7 @@ async function handleToolCall(params) {
7817
8017
  }
7818
8018
  const downloadUrl = buildDownloadUrl("job-artifacts", { project_id, job_id });
7819
8019
  return {
7820
- content: [{ type: "text", text: JSON.stringify({ download_url: downloadUrl, filename: `artifacts_job_${job_id}.zip` }, null, 2) }],
8020
+ content: [{ type: "text", text: JSON.stringify({ download_url: downloadUrl, filename: `artifacts_job_${job_id}.zip` }) }],
7821
8021
  };
7822
8022
  }
7823
8023
  const filePath = await downloadJobArtifacts(project_id, job_id, local_path);
@@ -7825,7 +8025,7 @@ async function handleToolCall(params) {
7825
8025
  content: [
7826
8026
  {
7827
8027
  type: "text",
7828
- text: JSON.stringify({ success: true, file_path: filePath }, null, 2),
8028
+ text: JSON.stringify({ success: true, file_path: filePath }),
7829
8029
  },
7830
8030
  ],
7831
8031
  };
@@ -7847,7 +8047,7 @@ async function handleToolCall(params) {
7847
8047
  const cleanedOptions = cleanMutuallyExclusiveIdUsernameOptions(options, LIST_MERGE_REQUESTS_ID_USERNAME_PAIRS);
7848
8048
  const mergeRequests = await listMergeRequests(project_id, cleanedOptions);
7849
8049
  return {
7850
- content: [{ type: "text", text: JSON.stringify(mergeRequests, null, 2) }],
8050
+ content: [{ type: "text", text: JSON.stringify(mergeRequests) }],
7851
8051
  };
7852
8052
  }
7853
8053
  case "list_milestones": {
@@ -7857,7 +8057,7 @@ async function handleToolCall(params) {
7857
8057
  content: [
7858
8058
  {
7859
8059
  type: "text",
7860
- text: JSON.stringify(milestones, null, 2),
8060
+ text: JSON.stringify(milestones),
7861
8061
  },
7862
8062
  ],
7863
8063
  };
@@ -7869,7 +8069,7 @@ async function handleToolCall(params) {
7869
8069
  content: [
7870
8070
  {
7871
8071
  type: "text",
7872
- text: JSON.stringify(milestone, null, 2),
8072
+ text: JSON.stringify(milestone),
7873
8073
  },
7874
8074
  ],
7875
8075
  };
@@ -7881,7 +8081,7 @@ async function handleToolCall(params) {
7881
8081
  content: [
7882
8082
  {
7883
8083
  type: "text",
7884
- text: JSON.stringify(milestone, null, 2),
8084
+ text: JSON.stringify(milestone),
7885
8085
  },
7886
8086
  ],
7887
8087
  };
@@ -7893,7 +8093,7 @@ async function handleToolCall(params) {
7893
8093
  content: [
7894
8094
  {
7895
8095
  type: "text",
7896
- text: JSON.stringify(milestone, null, 2),
8096
+ text: JSON.stringify(milestone),
7897
8097
  },
7898
8098
  ],
7899
8099
  };
@@ -7920,7 +8120,7 @@ async function handleToolCall(params) {
7920
8120
  content: [
7921
8121
  {
7922
8122
  type: "text",
7923
- text: JSON.stringify(issues, null, 2),
8123
+ text: JSON.stringify(issues),
7924
8124
  },
7925
8125
  ],
7926
8126
  };
@@ -7932,7 +8132,7 @@ async function handleToolCall(params) {
7932
8132
  content: [
7933
8133
  {
7934
8134
  type: "text",
7935
- text: JSON.stringify(mergeRequests, null, 2),
8135
+ text: JSON.stringify(mergeRequests),
7936
8136
  },
7937
8137
  ],
7938
8138
  };
@@ -7944,7 +8144,7 @@ async function handleToolCall(params) {
7944
8144
  content: [
7945
8145
  {
7946
8146
  type: "text",
7947
- text: JSON.stringify(milestone, null, 2),
8147
+ text: JSON.stringify(milestone),
7948
8148
  },
7949
8149
  ],
7950
8150
  };
@@ -7956,7 +8156,7 @@ async function handleToolCall(params) {
7956
8156
  content: [
7957
8157
  {
7958
8158
  type: "text",
7959
- text: JSON.stringify(events, null, 2),
8159
+ text: JSON.stringify(events),
7960
8160
  },
7961
8161
  ],
7962
8162
  };
@@ -7965,21 +8165,21 @@ async function handleToolCall(params) {
7965
8165
  const args = ListCommitsSchema.parse(params.arguments);
7966
8166
  const commits = await listCommits(args.project_id, args);
7967
8167
  return {
7968
- content: [{ type: "text", text: JSON.stringify(commits, null, 2) }],
8168
+ content: [{ type: "text", text: JSON.stringify(commits) }],
7969
8169
  };
7970
8170
  }
7971
8171
  case "get_commit": {
7972
8172
  const args = GetCommitSchema.parse(params.arguments);
7973
8173
  const commit = await getCommit(args.project_id, args.sha, args.stats);
7974
8174
  return {
7975
- content: [{ type: "text", text: JSON.stringify(commit, null, 2) }],
8175
+ content: [{ type: "text", text: JSON.stringify(commit) }],
7976
8176
  };
7977
8177
  }
7978
8178
  case "get_commit_diff": {
7979
8179
  const args = GetCommitDiffSchema.parse(params.arguments);
7980
8180
  const diff = await getCommitDiff(args.project_id, args.sha, args.full_diff);
7981
8181
  return {
7982
- content: [{ type: "text", text: JSON.stringify(diff, null, 2) }],
8182
+ content: [{ type: "text", text: JSON.stringify(diff) }],
7983
8183
  };
7984
8184
  }
7985
8185
  case "get_file_blame": {
@@ -7987,7 +8187,7 @@ async function handleToolCall(params) {
7987
8187
  const { project_id, ...options } = args;
7988
8188
  const blame = await getFileBlame(project_id, options);
7989
8189
  return {
7990
- content: [{ type: "text", text: JSON.stringify(blame, null, 2) }],
8190
+ content: [{ type: "text", text: JSON.stringify(blame) }],
7991
8191
  };
7992
8192
  }
7993
8193
  case "list_commit_statuses": {
@@ -7995,7 +8195,7 @@ async function handleToolCall(params) {
7995
8195
  const { project_id, sha, ...options } = args;
7996
8196
  const statuses = await listCommitStatuses(project_id, sha, options);
7997
8197
  return {
7998
- content: [{ type: "text", text: JSON.stringify(statuses, null, 2) }],
8198
+ content: [{ type: "text", text: JSON.stringify(statuses) }],
7999
8199
  };
8000
8200
  }
8001
8201
  case "create_commit_status": {
@@ -8003,14 +8203,14 @@ async function handleToolCall(params) {
8003
8203
  const { project_id, sha, ...options } = args;
8004
8204
  const status = await createCommitStatus(project_id, sha, options);
8005
8205
  return {
8006
- content: [{ type: "text", text: JSON.stringify(status, null, 2) }],
8206
+ content: [{ type: "text", text: JSON.stringify(status) }],
8007
8207
  };
8008
8208
  }
8009
8209
  case "list_group_iterations": {
8010
8210
  const args = ListGroupIterationsSchema.parse(params.arguments);
8011
8211
  const iterations = await listGroupIterations(args.group_id, args);
8012
8212
  return {
8013
- content: [{ type: "text", text: JSON.stringify(iterations, null, 2) }],
8213
+ content: [{ type: "text", text: JSON.stringify(iterations) }],
8014
8214
  };
8015
8215
  }
8016
8216
  // --- CI/CD Variables ---
@@ -8018,24 +8218,24 @@ async function handleToolCall(params) {
8018
8218
  const args = ListProjectVariablesSchema.parse(params.arguments);
8019
8219
  const { project_id, ...options } = args;
8020
8220
  const variables = await listProjectVariables(project_id, options);
8021
- return { content: [{ type: "text", text: JSON.stringify(variables, null, 2) }] };
8221
+ return { content: [{ type: "text", text: JSON.stringify(variables) }] };
8022
8222
  }
8023
8223
  case "get_project_variable": {
8024
8224
  const args = GetProjectVariableSchema.parse(params.arguments);
8025
8225
  const variable = await getProjectVariable(args.project_id, args.key, args.filter);
8026
- return { content: [{ type: "text", text: JSON.stringify(variable, null, 2) }] };
8226
+ return { content: [{ type: "text", text: JSON.stringify(variable) }] };
8027
8227
  }
8028
8228
  case "create_project_variable": {
8029
8229
  const args = CreateProjectVariableSchema.parse(params.arguments);
8030
8230
  const { project_id, ...options } = args;
8031
8231
  const variable = await createProjectVariable(project_id, options);
8032
- return { content: [{ type: "text", text: JSON.stringify(variable, null, 2) }] };
8232
+ return { content: [{ type: "text", text: JSON.stringify(variable) }] };
8033
8233
  }
8034
8234
  case "update_project_variable": {
8035
8235
  const args = UpdateProjectVariableSchema.parse(params.arguments);
8036
8236
  const { project_id, key, ...options } = args;
8037
8237
  const variable = await updateProjectVariable(project_id, key, options);
8038
- return { content: [{ type: "text", text: JSON.stringify(variable, null, 2) }] };
8238
+ return { content: [{ type: "text", text: JSON.stringify(variable) }] };
8039
8239
  }
8040
8240
  case "delete_project_variable": {
8041
8241
  const args = DeleteProjectVariableSchema.parse(params.arguments);
@@ -8054,27 +8254,27 @@ async function handleToolCall(params) {
8054
8254
  const args = ListGroupVariablesSchema.parse(params.arguments);
8055
8255
  const { group_id, ...options } = args;
8056
8256
  const variables = await listGroupVariables(group_id, options);
8057
- return { content: [{ type: "text", text: JSON.stringify(variables, null, 2) }] };
8257
+ return { content: [{ type: "text", text: JSON.stringify(variables) }] };
8058
8258
  }
8059
8259
  case "get_group_variable": {
8060
8260
  rejectIfProjectScopedDeployment("get_group_variable");
8061
8261
  const args = GetGroupVariableSchema.parse(params.arguments);
8062
8262
  const variable = await getGroupVariable(args.group_id, args.key, args.filter);
8063
- return { content: [{ type: "text", text: JSON.stringify(variable, null, 2) }] };
8263
+ return { content: [{ type: "text", text: JSON.stringify(variable) }] };
8064
8264
  }
8065
8265
  case "create_group_variable": {
8066
8266
  rejectIfProjectScopedDeployment("create_group_variable");
8067
8267
  const args = CreateGroupVariableSchema.parse(params.arguments);
8068
8268
  const { group_id, ...options } = args;
8069
8269
  const variable = await createGroupVariable(group_id, options);
8070
- return { content: [{ type: "text", text: JSON.stringify(variable, null, 2) }] };
8270
+ return { content: [{ type: "text", text: JSON.stringify(variable) }] };
8071
8271
  }
8072
8272
  case "update_group_variable": {
8073
8273
  rejectIfProjectScopedDeployment("update_group_variable");
8074
8274
  const args = UpdateGroupVariableSchema.parse(params.arguments);
8075
8275
  const { group_id, key, ...options } = args;
8076
8276
  const variable = await updateGroupVariable(group_id, key, options);
8077
- return { content: [{ type: "text", text: JSON.stringify(variable, null, 2) }] };
8277
+ return { content: [{ type: "text", text: JSON.stringify(variable) }] };
8078
8278
  }
8079
8279
  case "delete_group_variable": {
8080
8280
  rejectIfProjectScopedDeployment("delete_group_variable");
@@ -8094,7 +8294,7 @@ async function handleToolCall(params) {
8094
8294
  const args = GetDependencyProxySettingsSchema.parse(params.arguments);
8095
8295
  const settings = await getDependencyProxySettings(args.group_id);
8096
8296
  return {
8097
- content: [{ type: "text", text: JSON.stringify(settings, null, 2) }],
8297
+ content: [{ type: "text", text: JSON.stringify(settings) }],
8098
8298
  };
8099
8299
  }
8100
8300
  case "update_dependency_proxy_settings": {
@@ -8103,7 +8303,7 @@ async function handleToolCall(params) {
8103
8303
  const { group_id, ...options } = args;
8104
8304
  const settings = await updateDependencyProxySettings(group_id, options);
8105
8305
  return {
8106
- content: [{ type: "text", text: JSON.stringify(settings, null, 2) }],
8306
+ content: [{ type: "text", text: JSON.stringify(settings) }],
8107
8307
  };
8108
8308
  }
8109
8309
  case "list_dependency_proxy_blobs": {
@@ -8112,7 +8312,7 @@ async function handleToolCall(params) {
8112
8312
  const { group_id, ...options } = args;
8113
8313
  const result = await listDependencyProxyBlobs(group_id, options);
8114
8314
  return {
8115
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
8315
+ content: [{ type: "text", text: JSON.stringify(result) }],
8116
8316
  };
8117
8317
  }
8118
8318
  case "purge_dependency_proxy_cache": {
@@ -8133,13 +8333,13 @@ async function handleToolCall(params) {
8133
8333
  const args = MarkdownUploadRemoteSchema.parse(params.arguments);
8134
8334
  const upload = await markdownUpload(args.project_id, undefined, args.content, args.filename);
8135
8335
  return {
8136
- content: [{ type: "text", text: JSON.stringify(upload, null, 2) }],
8336
+ content: [{ type: "text", text: JSON.stringify(upload) }],
8137
8337
  };
8138
8338
  }
8139
8339
  const args = MarkdownUploadSchema.parse(params.arguments);
8140
8340
  const upload = await markdownUpload(args.project_id, args.file_path);
8141
8341
  return {
8142
- content: [{ type: "text", text: JSON.stringify(upload, null, 2) }],
8342
+ content: [{ type: "text", text: JSON.stringify(upload) }],
8143
8343
  };
8144
8344
  }
8145
8345
  case "download_attachment": {
@@ -8151,10 +8351,12 @@ async function handleToolCall(params) {
8151
8351
  const mimeType = getImageMimeType(args.filename);
8152
8352
  if (IS_REMOTE && !mimeType) {
8153
8353
  const downloadUrl = buildDownloadUrl("attachment", {
8154
- project_id: args.project_id, secret: args.secret, filename: args.filename,
8354
+ project_id: args.project_id,
8355
+ secret: args.secret,
8356
+ filename: args.filename,
8155
8357
  });
8156
8358
  return {
8157
- content: [{ type: "text", text: JSON.stringify({ download_url: downloadUrl, filename: args.filename }, null, 2) }],
8359
+ content: [{ type: "text", text: JSON.stringify({ download_url: downloadUrl, filename: args.filename }) }],
8158
8360
  };
8159
8361
  }
8160
8362
  const result = await downloadAttachment(args.project_id, args.secret, args.filename, args.local_path);
@@ -8166,7 +8368,7 @@ async function handleToolCall(params) {
8166
8368
  { type: "image", data: base64, mimeType: result.mimeType },
8167
8369
  {
8168
8370
  type: "text",
8169
- text: JSON.stringify({ filename: result.filename, mimeType: result.mimeType }, null, 2),
8371
+ text: JSON.stringify({ filename: result.filename, mimeType: result.mimeType }),
8170
8372
  },
8171
8373
  ],
8172
8374
  };
@@ -8175,7 +8377,7 @@ async function handleToolCall(params) {
8175
8377
  content: [
8176
8378
  {
8177
8379
  type: "text",
8178
- text: JSON.stringify({ success: true, file_path: result.savedPath }, null, 2),
8380
+ text: JSON.stringify({ success: true, file_path: result.savedPath }),
8179
8381
  },
8180
8382
  ],
8181
8383
  };
@@ -8184,7 +8386,7 @@ async function handleToolCall(params) {
8184
8386
  const args = ListEventsSchema.parse(params.arguments);
8185
8387
  const events = await listEvents(args);
8186
8388
  return {
8187
- content: [{ type: "text", text: JSON.stringify(events, null, 2) }],
8389
+ content: [{ type: "text", text: JSON.stringify(events) }],
8188
8390
  };
8189
8391
  }
8190
8392
  case "get_project_events": {
@@ -8192,7 +8394,7 @@ async function handleToolCall(params) {
8192
8394
  const { project_id, ...options } = args;
8193
8395
  const events = await getProjectEvents(project_id, options);
8194
8396
  return {
8195
- content: [{ type: "text", text: JSON.stringify(events, null, 2) }],
8397
+ content: [{ type: "text", text: JSON.stringify(events) }],
8196
8398
  };
8197
8399
  }
8198
8400
  case "list_releases": {
@@ -8200,14 +8402,14 @@ async function handleToolCall(params) {
8200
8402
  const { project_id, ...options } = args;
8201
8403
  const releases = await listReleases(project_id, options);
8202
8404
  return {
8203
- content: [{ type: "text", text: JSON.stringify(releases, null, 2) }],
8405
+ content: [{ type: "text", text: JSON.stringify(releases) }],
8204
8406
  };
8205
8407
  }
8206
8408
  case "get_release": {
8207
8409
  const args = GetReleaseSchema.parse(params.arguments);
8208
8410
  const release = await getRelease(args.project_id, args.tag_name, args.include_html_description);
8209
8411
  return {
8210
- content: [{ type: "text", text: JSON.stringify(release, null, 2) }],
8412
+ content: [{ type: "text", text: JSON.stringify(release) }],
8211
8413
  };
8212
8414
  }
8213
8415
  case "create_release": {
@@ -8215,7 +8417,7 @@ async function handleToolCall(params) {
8215
8417
  const { project_id, ...options } = args;
8216
8418
  const release = await createRelease(project_id, options);
8217
8419
  return {
8218
- content: [{ type: "text", text: JSON.stringify(release, null, 2) }],
8420
+ content: [{ type: "text", text: JSON.stringify(release) }],
8219
8421
  };
8220
8422
  }
8221
8423
  case "update_release": {
@@ -8223,7 +8425,7 @@ async function handleToolCall(params) {
8223
8425
  const { project_id, tag_name, ...options } = args;
8224
8426
  const release = await updateRelease(project_id, tag_name, options);
8225
8427
  return {
8226
- content: [{ type: "text", text: JSON.stringify(release, null, 2) }],
8428
+ content: [{ type: "text", text: JSON.stringify(release) }],
8227
8429
  };
8228
8430
  }
8229
8431
  case "delete_release": {
@@ -8254,10 +8456,12 @@ async function handleToolCall(params) {
8254
8456
  const args = DownloadReleaseAssetSchema.parse(params.arguments);
8255
8457
  if (IS_REMOTE) {
8256
8458
  const downloadUrl = buildDownloadUrl("release-asset", {
8257
- project_id: args.project_id, tag_name: args.tag_name, direct_asset_path: args.direct_asset_path,
8459
+ project_id: args.project_id,
8460
+ tag_name: args.tag_name,
8461
+ direct_asset_path: args.direct_asset_path,
8258
8462
  });
8259
8463
  return {
8260
- content: [{ type: "text", text: JSON.stringify({ download_url: downloadUrl, filename: args.direct_asset_path.split("/").pop() || args.direct_asset_path }, null, 2) }],
8464
+ content: [{ type: "text", text: JSON.stringify({ download_url: downloadUrl, filename: args.direct_asset_path.split("/").pop() || args.direct_asset_path }) }],
8261
8465
  };
8262
8466
  }
8263
8467
  const assetContent = await downloadReleaseAsset(args.project_id, args.tag_name, args.direct_asset_path);
@@ -8270,14 +8474,14 @@ async function handleToolCall(params) {
8270
8474
  const { project_id, ...options } = args;
8271
8475
  const tags = await listTags(project_id, options);
8272
8476
  return {
8273
- content: [{ type: "text", text: JSON.stringify(tags, null, 2) }],
8477
+ content: [{ type: "text", text: JSON.stringify(tags) }],
8274
8478
  };
8275
8479
  }
8276
8480
  case "get_tag": {
8277
8481
  const args = GetTagSchema.parse(params.arguments);
8278
8482
  const tag = await getTag(args.project_id, args.tag_name);
8279
8483
  return {
8280
- content: [{ type: "text", text: JSON.stringify(tag, null, 2) }],
8484
+ content: [{ type: "text", text: JSON.stringify(tag) }],
8281
8485
  };
8282
8486
  }
8283
8487
  case "create_tag": {
@@ -8285,7 +8489,7 @@ async function handleToolCall(params) {
8285
8489
  const { project_id, ...options } = args;
8286
8490
  const tag = await createTag(project_id, options);
8287
8491
  return {
8288
- content: [{ type: "text", text: JSON.stringify(tag, null, 2) }],
8492
+ content: [{ type: "text", text: JSON.stringify(tag) }],
8289
8493
  };
8290
8494
  }
8291
8495
  case "delete_tag": {
@@ -8304,30 +8508,28 @@ async function handleToolCall(params) {
8304
8508
  const args = GetTagSignatureSchema.parse(params.arguments);
8305
8509
  const signature = await getTagSignature(args.project_id, args.tag_name);
8306
8510
  return {
8307
- content: [{ type: "text", text: JSON.stringify(signature, null, 2) }],
8511
+ content: [{ type: "text", text: JSON.stringify(signature) }],
8308
8512
  };
8309
8513
  }
8310
8514
  case "list_webhooks": {
8311
8515
  const args = ListWebhooksSchema.parse(params.arguments);
8312
8516
  const webhooks = await listWebhooks(args);
8313
8517
  return {
8314
- content: [{ type: "text", text: JSON.stringify(webhooks, null, 2) }],
8518
+ content: [{ type: "text", text: JSON.stringify(webhooks) }],
8315
8519
  };
8316
8520
  }
8317
8521
  case "list_webhook_events": {
8318
8522
  const args = ListWebhookEventsSchema.parse(params.arguments);
8319
8523
  const events = await listWebhookEvents(args);
8320
8524
  return {
8321
- content: [{ type: "text", text: JSON.stringify(events, null, 2) }],
8525
+ content: [{ type: "text", text: JSON.stringify(events) }],
8322
8526
  };
8323
8527
  }
8324
8528
  case "get_webhook_event": {
8325
8529
  const args = GetWebhookEventSchema.parse(params.arguments);
8326
8530
  const event = await getWebhookEvent(args);
8327
8531
  if (!event) {
8328
- const searchScope = args.page
8329
- ? `on page ${args.page}`
8330
- : "in the 500 most recent events";
8532
+ const searchScope = args.page ? `on page ${args.page}` : "in the 500 most recent events";
8331
8533
  return {
8332
8534
  content: [
8333
8535
  {
@@ -8338,7 +8540,7 @@ async function handleToolCall(params) {
8338
8540
  };
8339
8541
  }
8340
8542
  return {
8341
- content: [{ type: "text", text: JSON.stringify(event, null, 2) }],
8543
+ content: [{ type: "text", text: JSON.stringify(event) }],
8342
8544
  };
8343
8545
  }
8344
8546
  case "health_check": {
@@ -8346,13 +8548,24 @@ async function handleToolCall(params) {
8346
8548
  const url = new URL(`${getEffectiveApiUrl()}/user`);
8347
8549
  const response = await fetch(url.toString(), getFetchConfig());
8348
8550
  let authenticated = response.ok;
8349
- if (!authenticated && (response.status === 401 || response.status === 403) && (GITLAB_JOB_TOKEN || usesJobTokenHeader())) {
8551
+ if (!authenticated &&
8552
+ (response.status === 401 || response.status === 403) &&
8553
+ (GITLAB_JOB_TOKEN || usesJobTokenHeader())) {
8350
8554
  const jobUrl = new URL(`${getEffectiveApiUrl()}/job`);
8351
8555
  const jobResponse = await fetch(jobUrl.toString(), getFetchConfig());
8352
8556
  authenticated = jobResponse.ok;
8353
8557
  }
8354
8558
  return {
8355
- content: [{ type: "text", text: JSON.stringify({ status: authenticated ? "ok" : "error", authenticated, gitlab_url: getEffectiveApiUrl() }) }],
8559
+ content: [
8560
+ {
8561
+ type: "text",
8562
+ text: JSON.stringify({
8563
+ status: authenticated ? "ok" : "error",
8564
+ authenticated,
8565
+ gitlab_url: getEffectiveApiUrl(),
8566
+ }),
8567
+ },
8568
+ ],
8356
8569
  };
8357
8570
  }
8358
8571
  case "get_branch": {
@@ -8366,7 +8579,7 @@ async function handleToolCall(params) {
8366
8579
  const data = await response.json();
8367
8580
  const branch = GitLabBranchSchema.parse(data);
8368
8581
  return {
8369
- content: [{ type: "text", text: JSON.stringify(branch, null, 2) }],
8582
+ content: [{ type: "text", text: JSON.stringify(branch) }],
8370
8583
  };
8371
8584
  }
8372
8585
  case "list_branches": {
@@ -8389,7 +8602,7 @@ async function handleToolCall(params) {
8389
8602
  const data = await response.json();
8390
8603
  const branches = z.array(GitLabBranchSchema).parse(data);
8391
8604
  return {
8392
- content: [{ type: "text", text: JSON.stringify(branches, null, 2) }],
8605
+ content: [{ type: "text", text: JSON.stringify(branches) }],
8393
8606
  };
8394
8607
  }
8395
8608
  case "delete_branch": {
@@ -8402,7 +8615,7 @@ async function handleToolCall(params) {
8402
8615
  });
8403
8616
  await handleGitLabError(response);
8404
8617
  return {
8405
- content: [{ type: "text", text: JSON.stringify({ status: "deleted", branch: args.branch_name }, null, 2) }],
8618
+ content: [{ type: "text", text: JSON.stringify({ status: "deleted", branch: args.branch_name }) }],
8406
8619
  };
8407
8620
  }
8408
8621
  case "list_protected_branches": {
@@ -8422,7 +8635,7 @@ async function handleToolCall(params) {
8422
8635
  await handleGitLabError(response);
8423
8636
  const data = z.array(GitLabProtectedBranchSchema).parse(await response.json());
8424
8637
  return {
8425
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
8638
+ content: [{ type: "text", text: JSON.stringify(data) }],
8426
8639
  };
8427
8640
  }
8428
8641
  case "get_protected_branch": {
@@ -8436,7 +8649,7 @@ async function handleToolCall(params) {
8436
8649
  await handleGitLabError(response);
8437
8650
  const data = GitLabProtectedBranchSchema.parse(await response.json());
8438
8651
  return {
8439
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
8652
+ content: [{ type: "text", text: JSON.stringify(data) }],
8440
8653
  };
8441
8654
  }
8442
8655
  case "protect_branch": {
@@ -8463,7 +8676,7 @@ async function handleToolCall(params) {
8463
8676
  await handleGitLabError(response);
8464
8677
  const data = GitLabProtectedBranchSchema.parse(await response.json());
8465
8678
  return {
8466
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
8679
+ content: [{ type: "text", text: JSON.stringify(data) }],
8467
8680
  };
8468
8681
  }
8469
8682
  case "unprotect_branch": {
@@ -8477,7 +8690,7 @@ async function handleToolCall(params) {
8477
8690
  });
8478
8691
  await handleGitLabError(response);
8479
8692
  return {
8480
- content: [{ type: "text", text: JSON.stringify({ status: "unprotected", branch: args.branch_name }, null, 2) }],
8693
+ content: [{ type: "text", text: JSON.stringify({ status: "unprotected", branch: args.branch_name }) }],
8481
8694
  };
8482
8695
  }
8483
8696
  case "update_default_branch": {
@@ -8493,7 +8706,7 @@ async function handleToolCall(params) {
8493
8706
  await handleGitLabError(response);
8494
8707
  const data = await response.json();
8495
8708
  return {
8496
- content: [{ type: "text", text: JSON.stringify({ status: "updated", default_branch: args.default_branch, project: data }, null, 2) }],
8709
+ content: [{ type: "text", text: JSON.stringify({ status: "updated", default_branch: args.default_branch, project: data }) }],
8497
8710
  };
8498
8711
  }
8499
8712
  default:
@@ -8672,7 +8885,9 @@ function registerDownloadProxy(app, maxRequestsPerMinute = Number.parseInt(proce
8672
8885
  case "release-asset": {
8673
8886
  const { project_id, tag_name, direct_asset_path } = req.query;
8674
8887
  if (!project_id || !tag_name || !direct_asset_path) {
8675
- res.status(400).json({ error: "project_id, tag_name, and direct_asset_path are required" });
8888
+ res
8889
+ .status(400)
8890
+ .json({ error: "project_id, tag_name, and direct_asset_path are required" });
8676
8891
  return;
8677
8892
  }
8678
8893
  const effectiveProjectId = getEffectiveProjectId(decodeURIComponent(project_id));
@@ -9079,7 +9294,7 @@ async function startStreamableHTTPServer() {
9079
9294
  token: effective.token,
9080
9295
  lastUsed: Date.now(),
9081
9296
  apiUrl: effective.apiUrl,
9082
- publicBaseUrl: getForwardedPublicBaseUrl(req),
9297
+ publicBaseUrl: getForwardedPublicBaseUrl(req, MCP_TRUST_PROXY),
9083
9298
  };
9084
9299
  // Step 4: create a fresh transport per request.
9085
9300
  const transport = isInit
@@ -9273,16 +9488,14 @@ async function startStreamableHTTPServer() {
9273
9488
  // Streamable HTTP endpoint - handles both session creation and message handling
9274
9489
  app.post("/mcp", mcpBearerAuth, async (req, res) => {
9275
9490
  const sessionId = readMcpSessionIdHeader(req);
9276
- const publicBaseUrl = getForwardedPublicBaseUrl(req);
9491
+ const publicBaseUrl = getForwardedPublicBaseUrl(req, MCP_TRUST_PROXY);
9277
9492
  // Track request
9278
9493
  metrics.requestsProcessed++;
9279
9494
  // Stateless-mode branch: bypass authBySession / streamableTransports
9280
9495
  // entirely and derive the session auth from either the current request
9281
9496
  // headers (init) or a sealed Mcp-Session-Id (subsequent requests).
9282
9497
  // Rate limiting is disabled here because there is no shared counter.
9283
- if (OAUTH_STATELESS_MODE &&
9284
- STATELESS_MATERIAL &&
9285
- (REMOTE_AUTHORIZATION || GITLAB_MCP_OAUTH)) {
9498
+ if (OAUTH_STATELESS_MODE && STATELESS_MATERIAL && (REMOTE_AUTHORIZATION || GITLAB_MCP_OAUTH)) {
9286
9499
  await handleStatelessMcpRequest(req, res, STATELESS_MATERIAL, OAUTH_STATELESS_SESSION_TTL_SECONDS);
9287
9500
  return;
9288
9501
  }
@@ -9307,9 +9520,11 @@ async function startStreamableHTTPServer() {
9307
9520
  // Handle remote authorization: extract and store auth headers per session
9308
9521
  if (REMOTE_AUTHORIZATION) {
9309
9522
  const authData = parseAuthHeaders(req);
9523
+ const allowUnauthenticatedDiscovery = GITLAB_ALLOW_UNAUTHENTICATED_TOOL_DISCOVERY &&
9524
+ isUnauthenticatedDiscoveryRequestBody(req.body);
9310
9525
  if (sessionId && !authBySession[sessionId]) {
9311
- // New session: require auth headers
9312
- if (!authData) {
9526
+ // New session: require auth headers unless public discovery was explicitly enabled.
9527
+ if (!authData && !allowUnauthenticatedDiscovery) {
9313
9528
  metrics.authFailures++;
9314
9529
  res.status(401).json({
9315
9530
  error: "Missing Private-Token, JOB-TOKEN, or Authorization header",
@@ -9317,10 +9532,16 @@ async function startStreamableHTTPServer() {
9317
9532
  });
9318
9533
  return;
9319
9534
  }
9320
- // Store auth for this session
9321
- authBySession[sessionId] = withPublicBaseUrl(authData, publicBaseUrl);
9322
- logger.info(`Session ${sessionId}: stored ${authData.header} header`);
9323
- setAuthTimeout(sessionId);
9535
+ // Store auth only when provided. Public discovery intentionally leaves the session unauthenticated.
9536
+ if (authData) {
9537
+ authBySession[sessionId] = withPublicBaseUrl(authData, publicBaseUrl);
9538
+ logger.info(`Session ${sessionId}: stored ${authData.header} header`);
9539
+ setAuthTimeout(sessionId);
9540
+ }
9541
+ else if (allowUnauthenticatedDiscovery) {
9542
+ // Schedule cleanup for unauthenticated discovery sessions to prevent slot exhaustion
9543
+ setAuthTimeout(sessionId);
9544
+ }
9324
9545
  }
9325
9546
  else if (sessionId && authData) {
9326
9547
  // Existing session: allow auth rotation/update
@@ -9493,25 +9714,112 @@ async function startStreamableHTTPServer() {
9493
9714
  message: "GET /mcp is not supported when STREAMABLE_HTTP is enabled. Use POST to communicate with the MCP server.",
9494
9715
  });
9495
9716
  });
9717
+ const getMetricsSnapshot = () => ({
9718
+ ...metrics,
9719
+ activeSessions: Object.keys(streamableTransports).length,
9720
+ authenticatedSessions: Object.keys(authBySession).length,
9721
+ gitlabClientPool: clientPool.getStats(),
9722
+ uptime: process.uptime(),
9723
+ memoryUsage: process.memoryUsage(),
9724
+ config: {
9725
+ maxSessions: MAX_SESSIONS,
9726
+ maxRequestsPerMinute: MAX_REQUESTS_PER_MINUTE,
9727
+ sessionTimeoutSeconds: SESSION_TIMEOUT_SECONDS,
9728
+ remoteAuthEnabled: REMOTE_AUTHORIZATION,
9729
+ mcpOAuthEnabled: GITLAB_MCP_OAUTH,
9730
+ statelessModeEnabled: OAUTH_STATELESS_MODE && STATELESS_MATERIAL !== null,
9731
+ statelessRotationKey: OAUTH_STATELESS_MODE && STATELESS_MATERIAL?.previous != null,
9732
+ },
9733
+ });
9734
+ const escapePrometheusLabel = (value) => String(value).replace(/\\/g, "\\\\").replace(/\n/g, "\\n").replace(/"/g, '\\"');
9735
+ const formatPrometheusMetrics = () => {
9736
+ const snapshot = getMetricsSnapshot();
9737
+ const configLabels = Object.entries({
9738
+ max_sessions: snapshot.config.maxSessions,
9739
+ max_requests_per_minute: snapshot.config.maxRequestsPerMinute,
9740
+ session_timeout_seconds: snapshot.config.sessionTimeoutSeconds,
9741
+ remote_auth_enabled: snapshot.config.remoteAuthEnabled,
9742
+ mcp_oauth_enabled: snapshot.config.mcpOAuthEnabled,
9743
+ stateless_mode_enabled: snapshot.config.statelessModeEnabled,
9744
+ stateless_rotation_key: snapshot.config.statelessRotationKey,
9745
+ })
9746
+ .map(([key, value]) => `${key}="${escapePrometheusLabel(value)}"`)
9747
+ .join(",");
9748
+ return [
9749
+ "# HELP gitlab_mcp_requests_processed_total Total MCP requests processed",
9750
+ "# TYPE gitlab_mcp_requests_processed_total counter",
9751
+ `gitlab_mcp_requests_processed_total ${snapshot.requestsProcessed}`,
9752
+ "",
9753
+ "# HELP gitlab_mcp_requests_rejected_total Requests rejected, by reason",
9754
+ "# TYPE gitlab_mcp_requests_rejected_total counter",
9755
+ `gitlab_mcp_requests_rejected_total{reason="rate_limit"} ${snapshot.rejectedByRateLimit}`,
9756
+ `gitlab_mcp_requests_rejected_total{reason="capacity"} ${snapshot.rejectedByCapacity}`,
9757
+ "",
9758
+ "# HELP gitlab_mcp_auth_failures_total Authentication failures",
9759
+ "# TYPE gitlab_mcp_auth_failures_total counter",
9760
+ `gitlab_mcp_auth_failures_total ${snapshot.authFailures}`,
9761
+ "",
9762
+ "# HELP gitlab_mcp_sessions_total Total sessions created",
9763
+ "# TYPE gitlab_mcp_sessions_total counter",
9764
+ `gitlab_mcp_sessions_total ${snapshot.totalSessions}`,
9765
+ "",
9766
+ "# HELP gitlab_mcp_sessions_expired_total Sessions expired due to inactivity",
9767
+ "# TYPE gitlab_mcp_sessions_expired_total counter",
9768
+ `gitlab_mcp_sessions_expired_total ${snapshot.expiredSessions}`,
9769
+ "",
9770
+ "# HELP gitlab_mcp_active_sessions Currently active sessions",
9771
+ "# TYPE gitlab_mcp_active_sessions gauge",
9772
+ `gitlab_mcp_active_sessions ${snapshot.activeSessions}`,
9773
+ "",
9774
+ "# HELP gitlab_mcp_authenticated_sessions Currently authenticated sessions",
9775
+ "# TYPE gitlab_mcp_authenticated_sessions gauge",
9776
+ `gitlab_mcp_authenticated_sessions ${snapshot.authenticatedSessions}`,
9777
+ "",
9778
+ "# HELP gitlab_mcp_client_pool_size Current GitLab client pool size",
9779
+ "# TYPE gitlab_mcp_client_pool_size gauge",
9780
+ `gitlab_mcp_client_pool_size ${snapshot.gitlabClientPool.size}`,
9781
+ "",
9782
+ "# HELP gitlab_mcp_client_pool_max_size Maximum GitLab client pool size",
9783
+ "# TYPE gitlab_mcp_client_pool_max_size gauge",
9784
+ `gitlab_mcp_client_pool_max_size ${snapshot.gitlabClientPool.maxSize}`,
9785
+ "",
9786
+ "# HELP gitlab_mcp_uptime_seconds Process uptime in seconds",
9787
+ "# TYPE gitlab_mcp_uptime_seconds gauge",
9788
+ `gitlab_mcp_uptime_seconds ${snapshot.uptime}`,
9789
+ "",
9790
+ "# HELP gitlab_mcp_memory_usage_bytes Node.js memory usage by type",
9791
+ "# TYPE gitlab_mcp_memory_usage_bytes gauge",
9792
+ ...Object.entries(snapshot.memoryUsage).map(([key, value]) => `gitlab_mcp_memory_usage_bytes{type="${escapePrometheusLabel(key)}"} ${value}`),
9793
+ "",
9794
+ "# HELP gitlab_mcp_stateless_requests_total Stateless MCP requests processed",
9795
+ "# TYPE gitlab_mcp_stateless_requests_total counter",
9796
+ `gitlab_mcp_stateless_requests_total ${snapshot.statelessRequests}`,
9797
+ "",
9798
+ "# HELP gitlab_mcp_stateless_auth_total Stateless auth successes, by source",
9799
+ "# TYPE gitlab_mcp_stateless_auth_total counter",
9800
+ `gitlab_mcp_stateless_auth_total{source="header"} ${snapshot.statelessAuthFromHeader}`,
9801
+ `gitlab_mcp_stateless_auth_total{source="sealed_session_id"} ${snapshot.statelessAuthFromSealedSid}`,
9802
+ "",
9803
+ "# HELP gitlab_mcp_stateless_auth_failures_total Stateless auth failures",
9804
+ "# TYPE gitlab_mcp_stateless_auth_failures_total counter",
9805
+ `gitlab_mcp_stateless_auth_failures_total ${snapshot.statelessAuthFailures}`,
9806
+ "",
9807
+ "# HELP gitlab_mcp_stateless_session_id_rotations_total Stateless session id rotations",
9808
+ "# TYPE gitlab_mcp_stateless_session_id_rotations_total counter",
9809
+ `gitlab_mcp_stateless_session_id_rotations_total ${snapshot.statelessSidRotated}`,
9810
+ "",
9811
+ "# HELP gitlab_mcp_config_info Static configuration (value is always 1)",
9812
+ "# TYPE gitlab_mcp_config_info gauge",
9813
+ `gitlab_mcp_config_info{${configLabels}} 1`,
9814
+ "",
9815
+ ].join("\n");
9816
+ };
9496
9817
  // Metrics endpoint
9497
9818
  app.get("/metrics", (_req, res) => {
9498
- res.json({
9499
- ...metrics,
9500
- activeSessions: Object.keys(streamableTransports).length,
9501
- authenticatedSessions: Object.keys(authBySession).length,
9502
- gitlabClientPool: clientPool.getStats(),
9503
- uptime: process.uptime(),
9504
- memoryUsage: process.memoryUsage(),
9505
- config: {
9506
- maxSessions: MAX_SESSIONS,
9507
- maxRequestsPerMinute: MAX_REQUESTS_PER_MINUTE,
9508
- sessionTimeoutSeconds: SESSION_TIMEOUT_SECONDS,
9509
- remoteAuthEnabled: REMOTE_AUTHORIZATION,
9510
- mcpOAuthEnabled: GITLAB_MCP_OAUTH,
9511
- statelessModeEnabled: OAUTH_STATELESS_MODE && STATELESS_MATERIAL !== null,
9512
- statelessRotationKey: OAUTH_STATELESS_MODE && STATELESS_MATERIAL?.previous != null,
9513
- },
9514
- });
9819
+ res.type("text/plain; version=0.0.4").send(formatPrometheusMetrics());
9820
+ });
9821
+ app.get("/metrics.json", (_req, res) => {
9822
+ res.json(getMetricsSnapshot());
9515
9823
  });
9516
9824
  // Health check endpoint
9517
9825
  app.get("/health", (_req, res) => {