@zereight/mcp-gitlab 2.1.24 → 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
  }
@@ -5737,8 +5741,19 @@ async function getCurrentUser() {
5737
5741
  async function myIssues(options = {}) {
5738
5742
  // Get current user to find their username
5739
5743
  const currentUser = await getCurrentUser();
5740
- // Use getEffectiveProjectId to handle project ID resolution
5741
- const effectiveProjectId = getEffectiveProjectId(options.project_id || "");
5744
+ let effectiveProjectId;
5745
+ try {
5746
+ effectiveProjectId = getEffectiveProjectId(options.project_id || "");
5747
+ }
5748
+ catch (err) {
5749
+ if (err instanceof Error &&
5750
+ err.message.includes("No project ID provided and GITLAB_PROJECT_ID is not set")) {
5751
+ effectiveProjectId = "";
5752
+ }
5753
+ else {
5754
+ throw err;
5755
+ }
5756
+ }
5742
5757
  // Use listIssues with assignee_username filter
5743
5758
  let listIssuesOptions = {
5744
5759
  state: options.state || "opened", // Default to "opened" if not specified
@@ -5946,7 +5961,11 @@ async function updateGroupVariable(groupId, key, options) {
5946
5961
  if (filter?.environment_scope) {
5947
5962
  url.searchParams.append("filter[environment_scope]", filter.environment_scope);
5948
5963
  }
5949
- 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
+ });
5950
5969
  await handleGitLabError(response);
5951
5970
  const data = await response.json();
5952
5971
  return GitLabCiVariableSchema.parse(data);
@@ -5994,7 +6013,9 @@ async function getDependencyProxySettings(groupPath) {
5994
6013
  });
5995
6014
  }
5996
6015
  async function updateDependencyProxySettings(groupPath, options) {
5997
- if (options.enabled === undefined && options.identity === undefined && options.secret === undefined) {
6016
+ if (options.enabled === undefined &&
6017
+ options.identity === undefined &&
6018
+ options.secret === undefined) {
5998
6019
  throw new Error("At least one of enabled, identity, or secret must be provided");
5999
6020
  }
6000
6021
  const fullPath = await resolveGroupFullPath(groupPath);
@@ -6028,7 +6049,11 @@ async function listDependencyProxyBlobs(groupPath, options = {}) {
6028
6049
  if (!conn)
6029
6050
  throw new Error(`Group not found or dependency proxy not enabled: ${fullPath}`);
6030
6051
  return {
6031
- 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
+ })),
6032
6057
  pageInfo: conn.pageInfo,
6033
6058
  };
6034
6059
  }
@@ -6088,7 +6113,7 @@ async function markdownUpload(projectId, filePath, content, filename) {
6088
6113
  const response = await fetch(url.toString(), {
6089
6114
  ...defaultFetchConfig,
6090
6115
  method: "POST",
6091
- body: form
6116
+ body: form,
6092
6117
  });
6093
6118
  if (!response.ok) {
6094
6119
  await handleGitLabError(response);
@@ -6409,6 +6434,34 @@ async function getTagSignature(projectId, tagName) {
6409
6434
  const data = await response.json();
6410
6435
  return GitLabTagSignatureSchema.parse(data);
6411
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
+ }
6412
6465
  // Request handlers are now registered inside createServer() factory function
6413
6466
  // to ensure each transport connection gets its own Server instance (GHSA-345p-7cg4-v4c7).
6414
6467
  async function handleToolCall(params) {
@@ -6472,7 +6525,7 @@ async function handleToolCall(params) {
6472
6525
  }
6473
6526
  const json = await response.json();
6474
6527
  return {
6475
- content: [{ type: "text", text: JSON.stringify(json, null, 2) }],
6528
+ content: [{ type: "text", text: JSON.stringify(json) }],
6476
6529
  };
6477
6530
  }
6478
6531
  catch (err) {
@@ -6481,7 +6534,7 @@ async function handleToolCall(params) {
6481
6534
  content: [
6482
6535
  {
6483
6536
  type: "text",
6484
- text: JSON.stringify({ error: `GraphQL request failed: ${message}` }, null, 2),
6537
+ text: JSON.stringify({ error: `GraphQL request failed: ${message}` }),
6485
6538
  },
6486
6539
  ],
6487
6540
  };
@@ -6496,7 +6549,7 @@ async function handleToolCall(params) {
6496
6549
  try {
6497
6550
  const forkedProject = await forkProject(forkArgs.project_id, forkArgs.namespace);
6498
6551
  return {
6499
- content: [{ type: "text", text: JSON.stringify(forkedProject, null, 2) }],
6552
+ content: [{ type: "text", text: JSON.stringify(forkedProject) }],
6500
6553
  };
6501
6554
  }
6502
6555
  catch (forkError) {
@@ -6509,7 +6562,7 @@ async function handleToolCall(params) {
6509
6562
  content: [
6510
6563
  {
6511
6564
  type: "text",
6512
- text: JSON.stringify({ error: forkErrorMessage }, null, 2),
6565
+ text: JSON.stringify({ error: forkErrorMessage }),
6513
6566
  },
6514
6567
  ],
6515
6568
  };
@@ -6526,7 +6579,7 @@ async function handleToolCall(params) {
6526
6579
  ref,
6527
6580
  });
6528
6581
  return {
6529
- content: [{ type: "text", text: JSON.stringify(branch, null, 2) }],
6582
+ content: [{ type: "text", text: JSON.stringify(branch) }],
6530
6583
  };
6531
6584
  }
6532
6585
  case "get_branch_diffs": {
@@ -6534,14 +6587,14 @@ async function handleToolCall(params) {
6534
6587
  const diffResp = await getBranchDiffs(args.project_id, args.from, args.to, args.straight);
6535
6588
  diffResp.diffs = filterDiffsByPatterns(diffResp.diffs, args.excluded_file_patterns);
6536
6589
  return {
6537
- content: [{ type: "text", text: JSON.stringify(diffResp, null, 2) }],
6590
+ content: [{ type: "text", text: JSON.stringify(diffResp) }],
6538
6591
  };
6539
6592
  }
6540
6593
  case "search_repositories": {
6541
6594
  const args = SearchRepositoriesSchema.parse(params.arguments);
6542
6595
  const results = await searchProjects(args.search, args.page, args.per_page);
6543
6596
  return {
6544
- content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
6597
+ content: [{ type: "text", text: JSON.stringify(results) }],
6545
6598
  };
6546
6599
  }
6547
6600
  case "search_code": {
@@ -6555,7 +6608,7 @@ async function handleToolCall(params) {
6555
6608
  per_page: args.per_page,
6556
6609
  });
6557
6610
  return {
6558
- content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
6611
+ content: [{ type: "text", text: JSON.stringify(results) }],
6559
6612
  };
6560
6613
  }
6561
6614
  case "search_project_code": {
@@ -6571,7 +6624,7 @@ async function handleToolCall(params) {
6571
6624
  per_page: args.per_page,
6572
6625
  });
6573
6626
  return {
6574
- content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
6627
+ content: [{ type: "text", text: JSON.stringify(results) }],
6575
6628
  };
6576
6629
  }
6577
6630
  case "search_group_code": {
@@ -6586,7 +6639,7 @@ async function handleToolCall(params) {
6586
6639
  per_page: args.per_page,
6587
6640
  });
6588
6641
  return {
6589
- content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
6642
+ content: [{ type: "text", text: JSON.stringify(results) }],
6590
6643
  };
6591
6644
  }
6592
6645
  case "create_repository": {
@@ -6594,7 +6647,7 @@ async function handleToolCall(params) {
6594
6647
  const args = CreateRepositorySchema.parse(params.arguments);
6595
6648
  const repository = await createRepository(args);
6596
6649
  return {
6597
- content: [{ type: "text", text: JSON.stringify(repository, null, 2) }],
6650
+ content: [{ type: "text", text: JSON.stringify(repository) }],
6598
6651
  };
6599
6652
  }
6600
6653
  case "create_group": {
@@ -6621,28 +6674,28 @@ async function handleToolCall(params) {
6621
6674
  const data = await response.json();
6622
6675
  const group = GitLabGroupSchema.parse(data);
6623
6676
  return {
6624
- content: [{ type: "text", text: JSON.stringify(group, null, 2) }],
6677
+ content: [{ type: "text", text: JSON.stringify(group) }],
6625
6678
  };
6626
6679
  }
6627
6680
  case "get_file_contents": {
6628
6681
  const args = GetFileContentsSchema.parse(params.arguments);
6629
6682
  const contents = await getFileContents(args.project_id, args.file_path, args.ref);
6630
6683
  return {
6631
- content: [{ type: "text", text: JSON.stringify(contents, null, 2) }],
6684
+ content: [{ type: "text", text: JSON.stringify(contents) }],
6632
6685
  };
6633
6686
  }
6634
6687
  case "create_or_update_file": {
6635
6688
  const args = CreateOrUpdateFileSchema.parse(params.arguments);
6636
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);
6637
6690
  return {
6638
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
6691
+ content: [{ type: "text", text: JSON.stringify(result) }],
6639
6692
  };
6640
6693
  }
6641
6694
  case "push_files": {
6642
6695
  const args = PushFilesSchema.parse(params.arguments);
6643
6696
  const result = await createCommit(args.project_id, args.commit_message, args.branch, args.files.map(f => ({ path: f.file_path, content: f.content })));
6644
6697
  return {
6645
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
6698
+ content: [{ type: "text", text: JSON.stringify(result) }],
6646
6699
  };
6647
6700
  }
6648
6701
  case "create_issue": {
@@ -6650,7 +6703,7 @@ async function handleToolCall(params) {
6650
6703
  const { project_id, ...options } = args;
6651
6704
  const issue = await createIssue(project_id, options);
6652
6705
  return {
6653
- content: [{ type: "text", text: JSON.stringify(issue, null, 2) }],
6706
+ content: [{ type: "text", text: JSON.stringify(issue) }],
6654
6707
  };
6655
6708
  }
6656
6709
  case "create_merge_request": {
@@ -6658,7 +6711,7 @@ async function handleToolCall(params) {
6658
6711
  const { project_id, ...options } = args;
6659
6712
  const mergeRequest = await createMergeRequest(project_id, options);
6660
6713
  return {
6661
- content: [{ type: "text", text: JSON.stringify(mergeRequest, null, 2) }],
6714
+ content: [{ type: "text", text: JSON.stringify(mergeRequest) }],
6662
6715
  };
6663
6716
  }
6664
6717
  case "delete_merge_request_discussion_note": {
@@ -6675,21 +6728,21 @@ async function handleToolCall(params) {
6675
6728
  args.resolved // Now one of body or resolved must be provided, not both
6676
6729
  );
6677
6730
  return {
6678
- content: [{ type: "text", text: JSON.stringify(note, null, 2) }],
6731
+ content: [{ type: "text", text: JSON.stringify(note) }],
6679
6732
  };
6680
6733
  }
6681
6734
  case "create_merge_request_discussion_note": {
6682
6735
  const args = CreateMergeRequestDiscussionNoteSchema.parse(params.arguments);
6683
6736
  const note = await createMergeRequestDiscussionNote(args.project_id, args.merge_request_iid, args.discussion_id, args.body, args.created_at);
6684
6737
  return {
6685
- content: [{ type: "text", text: JSON.stringify(note, null, 2) }],
6738
+ content: [{ type: "text", text: JSON.stringify(note) }],
6686
6739
  };
6687
6740
  }
6688
6741
  case "create_merge_request_note": {
6689
6742
  const args = CreateMergeRequestNoteSchema.parse(params.arguments);
6690
6743
  const note = await createMergeRequestNote(args.project_id, args.merge_request_iid, args.body);
6691
6744
  return {
6692
- content: [{ type: "text", text: JSON.stringify(note, null, 2) }],
6745
+ content: [{ type: "text", text: JSON.stringify(note) }],
6693
6746
  };
6694
6747
  }
6695
6748
  case "delete_merge_request_note": {
@@ -6703,121 +6756,141 @@ async function handleToolCall(params) {
6703
6756
  const args = GetMergeRequestNoteSchema.parse(params.arguments);
6704
6757
  const note = await getMergeRequestNote(args.project_id, args.merge_request_iid, args.note_id);
6705
6758
  return {
6706
- content: [{ type: "text", text: JSON.stringify(note, null, 2) }],
6759
+ content: [{ type: "text", text: JSON.stringify(note) }],
6707
6760
  };
6708
6761
  }
6709
6762
  case "get_merge_request_notes": {
6710
6763
  const args = GetMergeRequestNotesSchema.parse(params.arguments);
6711
6764
  const notes = await getMergeRequestNotes(args.project_id, args.merge_request_iid, args.sort, args.order_by, args.per_page, args.page);
6712
6765
  return {
6713
- content: [{ type: "text", text: JSON.stringify(notes, null, 2) }],
6766
+ content: [{ type: "text", text: JSON.stringify(notes) }],
6714
6767
  };
6715
6768
  }
6716
6769
  case "update_merge_request_note": {
6717
6770
  const args = UpdateMergeRequestNoteSchema.parse(params.arguments);
6718
6771
  const note = await updateMergeRequestNote(args.project_id, args.merge_request_iid, args.note_id, args.body);
6719
6772
  return {
6720
- content: [{ type: "text", text: JSON.stringify(note, null, 2) }],
6773
+ content: [{ type: "text", text: JSON.stringify(note) }],
6721
6774
  };
6722
6775
  }
6723
6776
  case "list_merge_request_emoji_reactions": {
6724
6777
  const args = ListMergeRequestEmojiReactionsSchema.parse(params.arguments);
6725
6778
  const path = buildAwardEmojiPath("merge_requests", args.project_id, args.merge_request_iid);
6726
6779
  const result = await listRestAwardEmoji(path);
6727
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
6780
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
6728
6781
  }
6729
6782
  case "list_merge_request_note_emoji_reactions": {
6730
6783
  const args = ListMergeRequestNoteEmojiReactionsSchema.parse(params.arguments);
6731
6784
  const path = buildAwardEmojiPath("merge_requests", args.project_id, args.merge_request_iid, { noteId: args.note_id, discussionId: args.discussion_id });
6732
6785
  const result = await listRestAwardEmoji(path);
6733
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
6786
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
6734
6787
  }
6735
6788
  case "create_merge_request_emoji_reaction": {
6736
6789
  const args = CreateMergeRequestEmojiReactionSchema.parse(params.arguments);
6737
6790
  const path = buildAwardEmojiPath("merge_requests", args.project_id, args.merge_request_iid);
6738
6791
  const result = await createRestAwardEmoji(path, args.name);
6739
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
6792
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
6740
6793
  }
6741
6794
  case "delete_merge_request_emoji_reaction": {
6742
6795
  const args = DeleteMergeRequestEmojiReactionSchema.parse(params.arguments);
6743
6796
  const path = buildAwardEmojiPath("merge_requests", args.project_id, args.merge_request_iid, { awardId: args.award_id });
6744
6797
  await deleteRestAwardEmoji(path);
6745
- 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
+ };
6746
6801
  }
6747
6802
  case "create_merge_request_note_emoji_reaction": {
6748
6803
  const args = CreateMergeRequestNoteEmojiReactionSchema.parse(params.arguments);
6749
6804
  const path = buildAwardEmojiPath("merge_requests", args.project_id, args.merge_request_iid, { noteId: args.note_id, discussionId: args.discussion_id });
6750
6805
  const result = await createRestAwardEmoji(path, args.name);
6751
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
6806
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
6752
6807
  }
6753
6808
  case "delete_merge_request_note_emoji_reaction": {
6754
6809
  const args = DeleteMergeRequestNoteEmojiReactionSchema.parse(params.arguments);
6755
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 });
6756
6811
  await deleteRestAwardEmoji(path);
6757
- 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
+ };
6758
6817
  }
6759
6818
  case "update_issue_note": {
6760
6819
  const args = UpdateIssueNoteSchema.parse(params.arguments);
6761
6820
  const note = await updateIssueNote(args.project_id, args.issue_iid, args.discussion_id, args.note_id, args.body, args.resolved);
6762
6821
  return {
6763
- content: [{ type: "text", text: JSON.stringify(note, null, 2) }],
6822
+ content: [{ type: "text", text: JSON.stringify(note) }],
6764
6823
  };
6765
6824
  }
6766
6825
  case "create_issue_note": {
6767
6826
  const args = CreateIssueNoteSchema.parse(params.arguments);
6768
6827
  const note = await createIssueNote(args.project_id, args.issue_iid, args.discussion_id, args.body, args.created_at);
6769
6828
  return {
6770
- content: [{ type: "text", text: JSON.stringify(note, null, 2) }],
6829
+ content: [{ type: "text", text: JSON.stringify(note) }],
6771
6830
  };
6772
6831
  }
6773
6832
  case "list_issue_emoji_reactions": {
6774
6833
  const args = ListIssueEmojiReactionsSchema.parse(params.arguments);
6775
6834
  const path = buildAwardEmojiPath("issues", args.project_id, args.issue_iid);
6776
6835
  const result = await listRestAwardEmoji(path);
6777
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
6836
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
6778
6837
  }
6779
6838
  case "list_issue_note_emoji_reactions": {
6780
6839
  const args = ListIssueNoteEmojiReactionsSchema.parse(params.arguments);
6781
- 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
+ });
6782
6844
  const result = await listRestAwardEmoji(path);
6783
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
6845
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
6784
6846
  }
6785
6847
  case "create_issue_emoji_reaction": {
6786
6848
  const args = CreateIssueEmojiReactionSchema.parse(params.arguments);
6787
6849
  const path = buildAwardEmojiPath("issues", args.project_id, args.issue_iid);
6788
6850
  const result = await createRestAwardEmoji(path, args.name);
6789
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
6851
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
6790
6852
  }
6791
6853
  case "delete_issue_emoji_reaction": {
6792
6854
  const args = DeleteIssueEmojiReactionSchema.parse(params.arguments);
6793
- 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
+ });
6794
6858
  await deleteRestAwardEmoji(path);
6795
6859
  return { content: [{ type: "text", text: "Issue emoji reaction deleted successfully" }] };
6796
6860
  }
6797
6861
  case "create_issue_note_emoji_reaction": {
6798
6862
  const args = CreateIssueNoteEmojiReactionSchema.parse(params.arguments);
6799
- 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
+ });
6800
6867
  const result = await createRestAwardEmoji(path, args.name);
6801
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
6868
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
6802
6869
  }
6803
6870
  case "delete_issue_note_emoji_reaction": {
6804
6871
  const args = DeleteIssueNoteEmojiReactionSchema.parse(params.arguments);
6805
- 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
+ });
6806
6877
  await deleteRestAwardEmoji(path);
6807
- 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
+ };
6808
6881
  }
6809
6882
  case "list_todos": {
6810
6883
  const args = ListTodosSchema.parse(params.arguments);
6811
6884
  const todos = await listTodos(args);
6812
6885
  return {
6813
- content: [{ type: "text", text: JSON.stringify(todos, null, 2) }],
6886
+ content: [{ type: "text", text: JSON.stringify(todos) }],
6814
6887
  };
6815
6888
  }
6816
6889
  case "mark_todo_done": {
6817
6890
  const args = MarkTodoDoneSchema.parse(params.arguments);
6818
6891
  const todo = await markTodoDone(args.id);
6819
6892
  return {
6820
- content: [{ type: "text", text: JSON.stringify(todo, null, 2) }],
6893
+ content: [{ type: "text", text: JSON.stringify(todo) }],
6821
6894
  };
6822
6895
  }
6823
6896
  case "mark_all_todos_done": {
@@ -6851,7 +6924,7 @@ async function handleToolCall(params) {
6851
6924
  content: [
6852
6925
  {
6853
6926
  type: "text",
6854
- text: JSON.stringify(mergeRequestWithDeploymentSummary, null, 2),
6927
+ text: JSON.stringify(mergeRequestWithDeploymentSummary),
6855
6928
  },
6856
6929
  ],
6857
6930
  };
@@ -6861,14 +6934,14 @@ async function handleToolCall(params) {
6861
6934
  const diffs = await getMergeRequestDiffs(args.project_id, args.merge_request_iid, args.source_branch, args.view);
6862
6935
  const filteredDiffs = filterDiffsByPatterns(diffs, args.excluded_file_patterns);
6863
6936
  return {
6864
- content: [{ type: "text", text: JSON.stringify(filteredDiffs, null, 2) }],
6937
+ content: [{ type: "text", text: JSON.stringify(filteredDiffs) }],
6865
6938
  };
6866
6939
  }
6867
6940
  case "list_merge_request_changed_files": {
6868
6941
  const args = ListMergeRequestChangedFilesSchema.parse(params.arguments);
6869
6942
  const files = await listMergeRequestChangedFiles(args.project_id, args.merge_request_iid, args.source_branch, args.excluded_file_patterns);
6870
6943
  return {
6871
- content: [{ type: "text", text: JSON.stringify(files, null, 2) }],
6944
+ content: [{ type: "text", text: JSON.stringify(files) }],
6872
6945
  };
6873
6946
  }
6874
6947
  case "list_merge_request_pipelines": {
@@ -6876,35 +6949,35 @@ async function handleToolCall(params) {
6876
6949
  const { project_id, merge_request_iid, ...options } = args;
6877
6950
  const pipelines = await listMergeRequestPipelines(project_id, merge_request_iid, options);
6878
6951
  return {
6879
- content: [{ type: "text", text: JSON.stringify(pipelines, null, 2) }],
6952
+ content: [{ type: "text", text: JSON.stringify(pipelines) }],
6880
6953
  };
6881
6954
  }
6882
6955
  case "list_merge_request_diffs": {
6883
6956
  const args = ListMergeRequestDiffsSchema.parse(params.arguments);
6884
6957
  const changes = await listMergeRequestDiffs(args.project_id, args.merge_request_iid, args.source_branch, args.page, args.per_page, args.unidiff);
6885
6958
  return {
6886
- content: [{ type: "text", text: JSON.stringify(changes, null, 2) }],
6959
+ content: [{ type: "text", text: JSON.stringify(changes) }],
6887
6960
  };
6888
6961
  }
6889
6962
  case "get_merge_request_file_diff": {
6890
6963
  const args = GetMergeRequestFileDiffSchema.parse(params.arguments);
6891
6964
  const fileDiff = await getMergeRequestFileDiff(args.project_id, args.file_paths, args.merge_request_iid, args.source_branch, args.unidiff);
6892
6965
  return {
6893
- content: [{ type: "text", text: JSON.stringify(fileDiff, null, 2) }],
6966
+ content: [{ type: "text", text: JSON.stringify(fileDiff) }],
6894
6967
  };
6895
6968
  }
6896
6969
  case "list_merge_request_versions": {
6897
6970
  const args = ListMergeRequestVersionsSchema.parse(params.arguments);
6898
6971
  const versions = await listMergeRequestVersions(args.project_id, args.merge_request_iid);
6899
6972
  return {
6900
- content: [{ type: "text", text: JSON.stringify(versions, null, 2) }],
6973
+ content: [{ type: "text", text: JSON.stringify(versions) }],
6901
6974
  };
6902
6975
  }
6903
6976
  case "get_merge_request_version": {
6904
6977
  const args = GetMergeRequestVersionSchema.parse(params.arguments);
6905
6978
  const version = await getMergeRequestVersion(args.project_id, args.merge_request_iid, args.version_id, args.unidiff);
6906
6979
  return {
6907
- content: [{ type: "text", text: JSON.stringify(version, null, 2) }],
6980
+ content: [{ type: "text", text: JSON.stringify(version) }],
6908
6981
  };
6909
6982
  }
6910
6983
  case "update_merge_request": {
@@ -6912,7 +6985,7 @@ async function handleToolCall(params) {
6912
6985
  const { project_id, merge_request_iid, source_branch, ...options } = args;
6913
6986
  const mergeRequest = await updateMergeRequest(project_id, options, merge_request_iid, source_branch);
6914
6987
  return {
6915
- content: [{ type: "text", text: JSON.stringify(mergeRequest, null, 2) }],
6988
+ content: [{ type: "text", text: JSON.stringify(mergeRequest) }],
6916
6989
  };
6917
6990
  }
6918
6991
  case "merge_merge_request": {
@@ -6920,35 +6993,35 @@ async function handleToolCall(params) {
6920
6993
  const { project_id, merge_request_iid, ...options } = args;
6921
6994
  const mergeRequest = await mergeMergeRequest(project_id, options, merge_request_iid);
6922
6995
  return {
6923
- content: [{ type: "text", text: JSON.stringify(mergeRequest, null, 2) }],
6996
+ content: [{ type: "text", text: JSON.stringify(mergeRequest) }],
6924
6997
  };
6925
6998
  }
6926
6999
  case "approve_merge_request": {
6927
7000
  const args = ApproveMergeRequestSchema.parse(params.arguments);
6928
7001
  const approvalState = await approveMergeRequest(args.project_id, args.merge_request_iid, args.sha, args.approval_password);
6929
7002
  return {
6930
- content: [{ type: "text", text: JSON.stringify(approvalState, null, 2) }],
7003
+ content: [{ type: "text", text: JSON.stringify(approvalState) }],
6931
7004
  };
6932
7005
  }
6933
7006
  case "unapprove_merge_request": {
6934
7007
  const args = UnapproveMergeRequestSchema.parse(params.arguments);
6935
7008
  const approvalState = await unapproveMergeRequest(args.project_id, args.merge_request_iid);
6936
7009
  return {
6937
- content: [{ type: "text", text: JSON.stringify(approvalState, null, 2) }],
7010
+ content: [{ type: "text", text: JSON.stringify(approvalState) }],
6938
7011
  };
6939
7012
  }
6940
7013
  case "get_merge_request_approval_state": {
6941
7014
  const args = GetMergeRequestApprovalStateSchema.parse(params.arguments);
6942
7015
  const approvalState = await getMergeRequestApprovalState(args.project_id, args.merge_request_iid);
6943
7016
  return {
6944
- content: [{ type: "text", text: JSON.stringify(approvalState, null, 2) }],
7017
+ content: [{ type: "text", text: JSON.stringify(approvalState) }],
6945
7018
  };
6946
7019
  }
6947
7020
  case "get_merge_request_conflicts": {
6948
7021
  const args = GetMergeRequestConflictsSchema.parse(params.arguments);
6949
7022
  const conflicts = await getMergeRequestConflicts(args.project_id, args.merge_request_iid);
6950
7023
  return {
6951
- content: [{ type: "text", text: JSON.stringify(conflicts, null, 2) }],
7024
+ content: [{ type: "text", text: JSON.stringify(conflicts) }],
6952
7025
  };
6953
7026
  }
6954
7027
  case "mr_discussions": {
@@ -6956,7 +7029,7 @@ async function handleToolCall(params) {
6956
7029
  const { project_id, merge_request_iid, ...options } = args;
6957
7030
  const discussions = await listMergeRequestDiscussions(project_id, merge_request_iid, options);
6958
7031
  return {
6959
- content: [{ type: "text", text: JSON.stringify(discussions, null, 2) }],
7032
+ content: [{ type: "text", text: JSON.stringify(discussions) }],
6960
7033
  };
6961
7034
  }
6962
7035
  case "list_namespaces": {
@@ -6981,7 +7054,7 @@ async function handleToolCall(params) {
6981
7054
  const data = await response.json();
6982
7055
  const namespaces = z.array(GitLabNamespaceSchema).parse(data);
6983
7056
  return {
6984
- content: [{ type: "text", text: JSON.stringify(namespaces, null, 2) }],
7057
+ content: [{ type: "text", text: JSON.stringify(namespaces) }],
6985
7058
  };
6986
7059
  }
6987
7060
  case "get_namespace": {
@@ -6994,12 +7067,14 @@ async function handleToolCall(params) {
6994
7067
  const data = await response.json();
6995
7068
  const namespace = GitLabNamespaceSchema.parse(data);
6996
7069
  return {
6997
- content: [{ type: "text", text: JSON.stringify(namespace, null, 2) }],
7070
+ content: [{ type: "text", text: JSON.stringify(namespace) }],
6998
7071
  };
6999
7072
  }
7000
7073
  case "verify_namespace": {
7001
7074
  const args = VerifyNamespaceSchema.parse(params.arguments);
7002
7075
  const url = new URL(`${GITLAB_API_URL}/namespaces/${encodeURIComponent(args.path)}/exists`);
7076
+ if (args.parent_id !== undefined)
7077
+ url.searchParams.set("parent_id", String(args.parent_id));
7003
7078
  const response = await fetch(url.toString(), {
7004
7079
  ...getFetchConfig(),
7005
7080
  });
@@ -7007,7 +7082,7 @@ async function handleToolCall(params) {
7007
7082
  const data = await response.json();
7008
7083
  const namespaceExists = GitLabNamespaceExistsResponseSchema.parse(data);
7009
7084
  return {
7010
- content: [{ type: "text", text: JSON.stringify(namespaceExists, null, 2) }],
7085
+ content: [{ type: "text", text: JSON.stringify(namespaceExists) }],
7011
7086
  };
7012
7087
  }
7013
7088
  case "get_project": {
@@ -7028,14 +7103,29 @@ async function handleToolCall(params) {
7028
7103
  const data = await response.json();
7029
7104
  // Return raw data without parsing through our schema to avoid type mismatches in tests
7030
7105
  return {
7031
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
7106
+ content: [{ type: "text", text: JSON.stringify(data) }],
7032
7107
  };
7033
7108
  }
7034
7109
  case "list_projects": {
7035
7110
  const args = ListProjectsSchema.parse(params.arguments);
7036
7111
  const projects = await listProjects(args);
7037
7112
  return {
7038
- 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) }],
7039
7129
  };
7040
7130
  }
7041
7131
  case "list_project_members": {
@@ -7043,14 +7133,14 @@ async function handleToolCall(params) {
7043
7133
  const { project_id, ...options } = args;
7044
7134
  const members = await listProjectMembers(project_id, options);
7045
7135
  return {
7046
- content: [{ type: "text", text: JSON.stringify(members, null, 2) }],
7136
+ content: [{ type: "text", text: JSON.stringify(members) }],
7047
7137
  };
7048
7138
  }
7049
7139
  case "get_users": {
7050
7140
  const args = GetUsersSchema.parse(params.arguments);
7051
7141
  const usersMap = await getUsers(args.usernames);
7052
7142
  return {
7053
- content: [{ type: "text", text: JSON.stringify(usersMap, null, 2) }],
7143
+ content: [{ type: "text", text: JSON.stringify(usersMap) }],
7054
7144
  };
7055
7145
  }
7056
7146
  case "get_user": {
@@ -7063,7 +7153,7 @@ async function handleToolCall(params) {
7063
7153
  const data = await response.json();
7064
7154
  const user = GitLabUserFullSchema.parse(data);
7065
7155
  return {
7066
- content: [{ type: "text", text: JSON.stringify(user, null, 2) }],
7156
+ content: [{ type: "text", text: JSON.stringify(user) }],
7067
7157
  };
7068
7158
  }
7069
7159
  case "whoami": {
@@ -7076,7 +7166,7 @@ async function handleToolCall(params) {
7076
7166
  const data = await response.json();
7077
7167
  const user = GitLabCurrentUserSchema.parse(data);
7078
7168
  return {
7079
- content: [{ type: "text", text: JSON.stringify(user, null, 2) }],
7169
+ content: [{ type: "text", text: JSON.stringify(user) }],
7080
7170
  };
7081
7171
  }
7082
7172
  case "create_note": {
@@ -7084,7 +7174,7 @@ async function handleToolCall(params) {
7084
7174
  const { project_id, noteable_type, noteable_iid, body } = args;
7085
7175
  const note = await createNote(project_id, noteable_type, noteable_iid, body);
7086
7176
  return {
7087
- content: [{ type: "text", text: JSON.stringify(note, null, 2) }],
7177
+ content: [{ type: "text", text: JSON.stringify(note) }],
7088
7178
  };
7089
7179
  }
7090
7180
  case "get_draft_note": {
@@ -7092,7 +7182,7 @@ async function handleToolCall(params) {
7092
7182
  const { project_id, merge_request_iid, draft_note_id } = args;
7093
7183
  const draftNote = await getDraftNote(project_id, merge_request_iid, draft_note_id);
7094
7184
  return {
7095
- content: [{ type: "text", text: JSON.stringify(draftNote, null, 2) }],
7185
+ content: [{ type: "text", text: JSON.stringify(draftNote) }],
7096
7186
  };
7097
7187
  }
7098
7188
  case "list_draft_notes": {
@@ -7100,15 +7190,15 @@ async function handleToolCall(params) {
7100
7190
  const { project_id, merge_request_iid } = args;
7101
7191
  const draftNotes = await listDraftNotes(project_id, merge_request_iid);
7102
7192
  return {
7103
- content: [{ type: "text", text: JSON.stringify(draftNotes, null, 2) }],
7193
+ content: [{ type: "text", text: JSON.stringify(draftNotes) }],
7104
7194
  };
7105
7195
  }
7106
7196
  case "create_draft_note": {
7107
7197
  const args = CreateDraftNoteSchema.parse(params.arguments);
7108
- 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;
7109
7199
  const draftNote = await createDraftNote(project_id, merge_request_iid, body, in_reply_to_discussion_id, position, resolve_discussion);
7110
7200
  return {
7111
- content: [{ type: "text", text: JSON.stringify(draftNote, null, 2) }],
7201
+ content: [{ type: "text", text: JSON.stringify(draftNote) }],
7112
7202
  };
7113
7203
  }
7114
7204
  case "update_draft_note": {
@@ -7116,7 +7206,7 @@ async function handleToolCall(params) {
7116
7206
  const { project_id, merge_request_iid, draft_note_id, body, position, resolve_discussion } = args;
7117
7207
  const draftNote = await updateDraftNote(project_id, merge_request_iid, draft_note_id, body, position, resolve_discussion);
7118
7208
  return {
7119
- content: [{ type: "text", text: JSON.stringify(draftNote, null, 2) }],
7209
+ content: [{ type: "text", text: JSON.stringify(draftNote) }],
7120
7210
  };
7121
7211
  }
7122
7212
  case "delete_draft_note": {
@@ -7132,7 +7222,7 @@ async function handleToolCall(params) {
7132
7222
  const { project_id, merge_request_iid, draft_note_id } = args;
7133
7223
  const publishedNote = await publishDraftNote(project_id, merge_request_iid, draft_note_id);
7134
7224
  return {
7135
- content: [{ type: "text", text: JSON.stringify(publishedNote, null, 2) }],
7225
+ content: [{ type: "text", text: JSON.stringify(publishedNote) }],
7136
7226
  };
7137
7227
  }
7138
7228
  case "bulk_publish_draft_notes": {
@@ -7140,7 +7230,7 @@ async function handleToolCall(params) {
7140
7230
  const { project_id, merge_request_iid } = args;
7141
7231
  const publishedNotes = await bulkPublishDraftNotes(project_id, merge_request_iid);
7142
7232
  return {
7143
- content: [{ type: "text", text: JSON.stringify(publishedNotes, null, 2) }],
7233
+ content: [{ type: "text", text: JSON.stringify(publishedNotes) }],
7144
7234
  };
7145
7235
  }
7146
7236
  case "create_merge_request_thread": {
@@ -7148,7 +7238,7 @@ async function handleToolCall(params) {
7148
7238
  const { project_id, merge_request_iid, body, position, created_at } = args;
7149
7239
  const thread = await createMergeRequestThread(project_id, merge_request_iid, body, position, created_at);
7150
7240
  return {
7151
- content: [{ type: "text", text: JSON.stringify(thread, null, 2) }],
7241
+ content: [{ type: "text", text: JSON.stringify(thread) }],
7152
7242
  };
7153
7243
  }
7154
7244
  case "resolve_merge_request_thread": {
@@ -7165,21 +7255,21 @@ async function handleToolCall(params) {
7165
7255
  const cleanedOptions = cleanMutuallyExclusiveIdUsernameOptions(options);
7166
7256
  const issues = await listIssues(project_id, cleanedOptions);
7167
7257
  return {
7168
- content: [{ type: "text", text: JSON.stringify(issues, null, 2) }],
7258
+ content: [{ type: "text", text: JSON.stringify(issues) }],
7169
7259
  };
7170
7260
  }
7171
7261
  case "my_issues": {
7172
7262
  const args = MyIssuesSchema.parse(params.arguments);
7173
7263
  const issues = await myIssues(args);
7174
7264
  return {
7175
- content: [{ type: "text", text: JSON.stringify(issues, null, 2) }],
7265
+ content: [{ type: "text", text: JSON.stringify(issues) }],
7176
7266
  };
7177
7267
  }
7178
7268
  case "get_issue": {
7179
7269
  const args = GetIssueSchema.parse(params.arguments);
7180
7270
  const issue = await getIssue(args.project_id, args.issue_iid);
7181
7271
  return {
7182
- content: [{ type: "text", text: JSON.stringify(issue, null, 2) }],
7272
+ content: [{ type: "text", text: JSON.stringify(issue) }],
7183
7273
  };
7184
7274
  }
7185
7275
  case "update_issue": {
@@ -7187,7 +7277,7 @@ async function handleToolCall(params) {
7187
7277
  const { project_id, issue_iid, ...options } = args;
7188
7278
  const issue = await updateIssue(project_id, issue_iid, options);
7189
7279
  return {
7190
- content: [{ type: "text", text: JSON.stringify(issue, null, 2) }],
7280
+ content: [{ type: "text", text: JSON.stringify(issue) }],
7191
7281
  };
7192
7282
  }
7193
7283
  case "update_issue_description_patch": {
@@ -7281,7 +7371,7 @@ async function handleToolCall(params) {
7281
7371
  const args = ListIssueLinksSchema.parse(params.arguments);
7282
7372
  const links = await listIssueLinks(args.project_id, args.issue_iid);
7283
7373
  return {
7284
- content: [{ type: "text", text: JSON.stringify(links, null, 2) }],
7374
+ content: [{ type: "text", text: JSON.stringify(links) }],
7285
7375
  };
7286
7376
  }
7287
7377
  case "list_issue_discussions": {
@@ -7289,21 +7379,21 @@ async function handleToolCall(params) {
7289
7379
  const { project_id, issue_iid, ...options } = args;
7290
7380
  const discussions = await listIssueDiscussions(project_id, issue_iid, options);
7291
7381
  return {
7292
- content: [{ type: "text", text: JSON.stringify(discussions, null, 2) }],
7382
+ content: [{ type: "text", text: JSON.stringify(discussions) }],
7293
7383
  };
7294
7384
  }
7295
7385
  case "get_issue_link": {
7296
7386
  const args = GetIssueLinkSchema.parse(params.arguments);
7297
7387
  const link = await getIssueLink(args.project_id, args.issue_iid, args.issue_link_id);
7298
7388
  return {
7299
- content: [{ type: "text", text: JSON.stringify(link, null, 2) }],
7389
+ content: [{ type: "text", text: JSON.stringify(link) }],
7300
7390
  };
7301
7391
  }
7302
7392
  case "create_issue_link": {
7303
7393
  const args = CreateIssueLinkSchema.parse(params.arguments);
7304
7394
  const link = await createIssueLink(args.project_id, args.issue_iid, args.target_project_id, args.target_issue_iid, args.link_type);
7305
7395
  return {
7306
- content: [{ type: "text", text: JSON.stringify(link, null, 2) }],
7396
+ content: [{ type: "text", text: JSON.stringify(link) }],
7307
7397
  };
7308
7398
  }
7309
7399
  case "delete_issue_link": {
@@ -7325,7 +7415,7 @@ async function handleToolCall(params) {
7325
7415
  const args = GetWorkItemSchema.parse(params.arguments);
7326
7416
  const result = await getWorkItem(args.project_id, args.iid);
7327
7417
  return {
7328
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
7418
+ content: [{ type: "text", text: JSON.stringify(result) }],
7329
7419
  };
7330
7420
  }
7331
7421
  case "list_work_items": {
@@ -7333,7 +7423,7 @@ async function handleToolCall(params) {
7333
7423
  const { project_id, ...options } = args;
7334
7424
  const result = await listWorkItems(project_id, options);
7335
7425
  return {
7336
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
7426
+ content: [{ type: "text", text: JSON.stringify(result) }],
7337
7427
  };
7338
7428
  }
7339
7429
  case "create_work_item": {
@@ -7341,7 +7431,7 @@ async function handleToolCall(params) {
7341
7431
  const { project_id, ...options } = args;
7342
7432
  const result = await createWorkItem(project_id, options);
7343
7433
  return {
7344
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
7434
+ content: [{ type: "text", text: JSON.stringify(result) }],
7345
7435
  };
7346
7436
  }
7347
7437
  case "update_work_item": {
@@ -7349,117 +7439,117 @@ async function handleToolCall(params) {
7349
7439
  const { project_id, iid, ...options } = args;
7350
7440
  const result = await updateWorkItem(project_id, iid, options);
7351
7441
  return {
7352
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
7442
+ content: [{ type: "text", text: JSON.stringify(result) }],
7353
7443
  };
7354
7444
  }
7355
7445
  case "convert_work_item_type": {
7356
7446
  const args = ConvertWorkItemTypeSchema.parse(params.arguments);
7357
7447
  const result = await convertIssueType(args.project_id, args.iid, args.new_type);
7358
7448
  return {
7359
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
7449
+ content: [{ type: "text", text: JSON.stringify(result) }],
7360
7450
  };
7361
7451
  }
7362
7452
  case "list_work_item_statuses": {
7363
7453
  const args = ListWorkItemStatusesSchema.parse(params.arguments);
7364
7454
  const result = await listIssueStatuses(args.project_id, args.work_item_type);
7365
7455
  return {
7366
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
7456
+ content: [{ type: "text", text: JSON.stringify(result) }],
7367
7457
  };
7368
7458
  }
7369
7459
  case "list_custom_field_definitions": {
7370
7460
  const args = ListCustomFieldDefinitionsSchema.parse(params.arguments);
7371
7461
  const result = await listCustomFieldDefinitions(args.project_id, args.work_item_type);
7372
7462
  return {
7373
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
7463
+ content: [{ type: "text", text: JSON.stringify(result) }],
7374
7464
  };
7375
7465
  }
7376
7466
  case "move_work_item": {
7377
7467
  const args = MoveWorkItemSchema.parse(params.arguments);
7378
7468
  const result = await moveWorkItem(args.project_id, args.iid, args.target_project_id);
7379
7469
  return {
7380
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
7470
+ content: [{ type: "text", text: JSON.stringify(result) }],
7381
7471
  };
7382
7472
  }
7383
7473
  case "list_work_item_notes": {
7384
7474
  const args = ListWorkItemNotesSchema.parse(params.arguments);
7385
7475
  const result = await listWorkItemNotes(args.project_id, args.iid, args);
7386
7476
  return {
7387
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
7477
+ content: [{ type: "text", text: JSON.stringify(result) }],
7388
7478
  };
7389
7479
  }
7390
7480
  case "create_work_item_note": {
7391
7481
  const args = CreateWorkItemNoteSchema.parse(params.arguments);
7392
7482
  const result = await createWorkItemNote(args.project_id, args.iid, args.body, args);
7393
7483
  return {
7394
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
7484
+ content: [{ type: "text", text: JSON.stringify(result) }],
7395
7485
  };
7396
7486
  }
7397
7487
  case "list_work_item_emoji_reactions": {
7398
7488
  const args = ListWorkItemEmojiReactionsSchema.parse(params.arguments);
7399
7489
  const { workItemGID } = await resolveWorkItemGID(args.project_id, args.iid);
7400
7490
  const result = await listGraphQLAwardEmoji(workItemGID);
7401
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
7491
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
7402
7492
  }
7403
7493
  case "list_work_item_note_emoji_reactions": {
7404
7494
  const args = ListWorkItemNoteEmojiReactionsSchema.parse(params.arguments);
7405
7495
  const result = await listGraphQLAwardEmoji(args.note_id);
7406
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
7496
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
7407
7497
  }
7408
7498
  case "create_work_item_emoji_reaction": {
7409
7499
  const args = CreateWorkItemEmojiReactionSchema.parse(params.arguments);
7410
7500
  const { workItemGID } = await resolveWorkItemGID(args.project_id, args.iid);
7411
7501
  const result = await addGraphQLAwardEmoji(workItemGID, args.name);
7412
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
7502
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
7413
7503
  }
7414
7504
  case "delete_work_item_emoji_reaction": {
7415
7505
  const args = DeleteWorkItemEmojiReactionSchema.parse(params.arguments);
7416
7506
  const { workItemGID } = await resolveWorkItemGID(args.project_id, args.iid);
7417
7507
  const result = await removeGraphQLAwardEmoji(workItemGID, args.name);
7418
- 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" }) }] };
7419
7509
  }
7420
7510
  case "create_work_item_note_emoji_reaction": {
7421
7511
  const args = CreateWorkItemNoteEmojiReactionSchema.parse(params.arguments);
7422
7512
  const result = await addGraphQLAwardEmoji(args.note_id, args.name);
7423
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
7513
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
7424
7514
  }
7425
7515
  case "delete_work_item_note_emoji_reaction": {
7426
7516
  const args = DeleteWorkItemNoteEmojiReactionSchema.parse(params.arguments);
7427
7517
  const result = await removeGraphQLAwardEmoji(args.note_id, args.name);
7428
- 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" }) }] };
7429
7519
  }
7430
7520
  case "get_timeline_events": {
7431
7521
  const args = GetTimelineEventsSchema.parse(params.arguments);
7432
7522
  const result = await getTimelineEvents(args.project_id, args.incident_iid);
7433
7523
  return {
7434
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
7524
+ content: [{ type: "text", text: JSON.stringify(result) }],
7435
7525
  };
7436
7526
  }
7437
7527
  case "create_timeline_event": {
7438
7528
  const args = CreateTimelineEventSchema.parse(params.arguments);
7439
7529
  const result = await createTimelineEvent(args.project_id, args.incident_iid, args.note, args.occurred_at, args.tag_names);
7440
7530
  return {
7441
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
7531
+ content: [{ type: "text", text: JSON.stringify(result) }],
7442
7532
  };
7443
7533
  }
7444
7534
  case "list_labels": {
7445
7535
  const args = ListLabelsSchema.parse(params.arguments);
7446
7536
  const labels = await listLabels(args.project_id, args);
7447
7537
  return {
7448
- content: [{ type: "text", text: JSON.stringify(labels, null, 2) }],
7538
+ content: [{ type: "text", text: JSON.stringify(labels) }],
7449
7539
  };
7450
7540
  }
7451
7541
  case "get_label": {
7452
7542
  const args = GetLabelSchema.parse(params.arguments);
7453
7543
  const label = await getLabel(args.project_id, args.label_id, args.include_ancestor_groups);
7454
7544
  return {
7455
- content: [{ type: "text", text: JSON.stringify(label, null, 2) }],
7545
+ content: [{ type: "text", text: JSON.stringify(label) }],
7456
7546
  };
7457
7547
  }
7458
7548
  case "create_label": {
7459
7549
  const args = CreateLabelSchema.parse(params.arguments);
7460
7550
  const label = await createLabel(args.project_id, args);
7461
7551
  return {
7462
- content: [{ type: "text", text: JSON.stringify(label, null, 2) }],
7552
+ content: [{ type: "text", text: JSON.stringify(label) }],
7463
7553
  };
7464
7554
  }
7465
7555
  case "update_label": {
@@ -7467,7 +7557,7 @@ async function handleToolCall(params) {
7467
7557
  const { project_id, label_id, ...options } = args;
7468
7558
  const label = await updateLabel(project_id, label_id, options);
7469
7559
  return {
7470
- content: [{ type: "text", text: JSON.stringify(label, null, 2) }],
7560
+ content: [{ type: "text", text: JSON.stringify(label) }],
7471
7561
  };
7472
7562
  }
7473
7563
  case "delete_label": {
@@ -7486,7 +7576,7 @@ async function handleToolCall(params) {
7486
7576
  const args = ListGroupProjectsSchema.parse(params.arguments);
7487
7577
  const projects = await listGroupProjects(args);
7488
7578
  return {
7489
- content: [{ type: "text", text: JSON.stringify(projects, null, 2) }],
7579
+ content: [{ type: "text", text: JSON.stringify(projects) }],
7490
7580
  };
7491
7581
  }
7492
7582
  case "list_wiki_pages": {
@@ -7497,28 +7587,28 @@ async function handleToolCall(params) {
7497
7587
  with_content,
7498
7588
  });
7499
7589
  return {
7500
- content: [{ type: "text", text: JSON.stringify(wikiPages, null, 2) }],
7590
+ content: [{ type: "text", text: JSON.stringify(wikiPages) }],
7501
7591
  };
7502
7592
  }
7503
7593
  case "get_wiki_page": {
7504
7594
  const { project_id, slug } = GetWikiPageSchema.parse(params.arguments);
7505
7595
  const wikiPage = await getWikiPage(project_id, slug);
7506
7596
  return {
7507
- content: [{ type: "text", text: JSON.stringify(wikiPage, null, 2) }],
7597
+ content: [{ type: "text", text: JSON.stringify(wikiPage) }],
7508
7598
  };
7509
7599
  }
7510
7600
  case "create_wiki_page": {
7511
7601
  const { project_id, title, content, format } = CreateWikiPageSchema.parse(params.arguments);
7512
7602
  const wikiPage = await createWikiPage(project_id, title, content, format);
7513
7603
  return {
7514
- content: [{ type: "text", text: JSON.stringify(wikiPage, null, 2) }],
7604
+ content: [{ type: "text", text: JSON.stringify(wikiPage) }],
7515
7605
  };
7516
7606
  }
7517
7607
  case "update_wiki_page": {
7518
7608
  const { project_id, slug, title, content, format } = UpdateWikiPageSchema.parse(params.arguments);
7519
7609
  const wikiPage = await updateWikiPage(project_id, slug, title, content, format);
7520
7610
  return {
7521
- content: [{ type: "text", text: JSON.stringify(wikiPage, null, 2) }],
7611
+ content: [{ type: "text", text: JSON.stringify(wikiPage) }],
7522
7612
  };
7523
7613
  }
7524
7614
  case "delete_wiki_page": {
@@ -7544,28 +7634,28 @@ async function handleToolCall(params) {
7544
7634
  with_content,
7545
7635
  });
7546
7636
  return {
7547
- content: [{ type: "text", text: JSON.stringify(wikiPages, null, 2) }],
7637
+ content: [{ type: "text", text: JSON.stringify(wikiPages) }],
7548
7638
  };
7549
7639
  }
7550
7640
  case "get_group_wiki_page": {
7551
7641
  const { group_id, slug } = GetGroupWikiPageSchema.parse(params.arguments);
7552
7642
  const wikiPage = await getGroupWikiPage(group_id, slug);
7553
7643
  return {
7554
- content: [{ type: "text", text: JSON.stringify(wikiPage, null, 2) }],
7644
+ content: [{ type: "text", text: JSON.stringify(wikiPage) }],
7555
7645
  };
7556
7646
  }
7557
7647
  case "create_group_wiki_page": {
7558
7648
  const { group_id, title, content, format } = CreateGroupWikiPageSchema.parse(params.arguments);
7559
7649
  const wikiPage = await createGroupWikiPage(group_id, title, content, format);
7560
7650
  return {
7561
- content: [{ type: "text", text: JSON.stringify(wikiPage, null, 2) }],
7651
+ content: [{ type: "text", text: JSON.stringify(wikiPage) }],
7562
7652
  };
7563
7653
  }
7564
7654
  case "update_group_wiki_page": {
7565
7655
  const { group_id, slug, title, content, format } = UpdateGroupWikiPageSchema.parse(params.arguments);
7566
7656
  const wikiPage = await updateGroupWikiPage(group_id, slug, title, content, format);
7567
7657
  return {
7568
- content: [{ type: "text", text: JSON.stringify(wikiPage, null, 2) }],
7658
+ content: [{ type: "text", text: JSON.stringify(wikiPage) }],
7569
7659
  };
7570
7660
  }
7571
7661
  case "delete_group_wiki_page": {
@@ -7596,7 +7686,7 @@ async function handleToolCall(params) {
7596
7686
  }
7597
7687
  : items;
7598
7688
  return {
7599
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
7689
+ content: [{ type: "text", text: JSON.stringify(result) }],
7600
7690
  };
7601
7691
  }
7602
7692
  case "list_pipelines": {
@@ -7604,7 +7694,7 @@ async function handleToolCall(params) {
7604
7694
  const { project_id, ...options } = args;
7605
7695
  const pipelines = await listPipelines(project_id, options);
7606
7696
  return {
7607
- content: [{ type: "text", text: JSON.stringify(pipelines, null, 2) }],
7697
+ content: [{ type: "text", text: JSON.stringify(pipelines) }],
7608
7698
  };
7609
7699
  }
7610
7700
  case "get_pipeline": {
@@ -7614,7 +7704,7 @@ async function handleToolCall(params) {
7614
7704
  content: [
7615
7705
  {
7616
7706
  type: "text",
7617
- text: JSON.stringify(pipeline, null, 2),
7707
+ text: JSON.stringify(pipeline),
7618
7708
  },
7619
7709
  ],
7620
7710
  };
@@ -7624,14 +7714,14 @@ async function handleToolCall(params) {
7624
7714
  const { project_id, ...options } = args;
7625
7715
  const deployments = await listDeployments(project_id, options);
7626
7716
  return {
7627
- content: [{ type: "text", text: JSON.stringify(deployments, null, 2) }],
7717
+ content: [{ type: "text", text: JSON.stringify(deployments) }],
7628
7718
  };
7629
7719
  }
7630
7720
  case "get_deployment": {
7631
7721
  const { project_id, deployment_id } = GetDeploymentSchema.parse(params.arguments);
7632
7722
  const deployment = await getDeployment(project_id, deployment_id);
7633
7723
  return {
7634
- content: [{ type: "text", text: JSON.stringify(deployment, null, 2) }],
7724
+ content: [{ type: "text", text: JSON.stringify(deployment) }],
7635
7725
  };
7636
7726
  }
7637
7727
  case "list_environments": {
@@ -7639,14 +7729,14 @@ async function handleToolCall(params) {
7639
7729
  const { project_id, ...options } = args;
7640
7730
  const environments = await listEnvironments(project_id, options);
7641
7731
  return {
7642
- content: [{ type: "text", text: JSON.stringify(environments, null, 2) }],
7732
+ content: [{ type: "text", text: JSON.stringify(environments) }],
7643
7733
  };
7644
7734
  }
7645
7735
  case "get_environment": {
7646
7736
  const { project_id, environment_id } = GetEnvironmentSchema.parse(params.arguments);
7647
7737
  const environment = await getEnvironment(project_id, environment_id);
7648
7738
  return {
7649
- content: [{ type: "text", text: JSON.stringify(environment, null, 2) }],
7739
+ content: [{ type: "text", text: JSON.stringify(environment) }],
7650
7740
  };
7651
7741
  }
7652
7742
  case "list_pipeline_jobs": {
@@ -7656,7 +7746,7 @@ async function handleToolCall(params) {
7656
7746
  content: [
7657
7747
  {
7658
7748
  type: "text",
7659
- text: JSON.stringify(jobs, null, 2),
7749
+ text: JSON.stringify(jobs),
7660
7750
  },
7661
7751
  ],
7662
7752
  };
@@ -7668,7 +7758,7 @@ async function handleToolCall(params) {
7668
7758
  content: [
7669
7759
  {
7670
7760
  type: "text",
7671
- text: JSON.stringify(triggerJobs, null, 2),
7761
+ text: JSON.stringify(triggerJobs),
7672
7762
  },
7673
7763
  ],
7674
7764
  };
@@ -7680,7 +7770,7 @@ async function handleToolCall(params) {
7680
7770
  content: [
7681
7771
  {
7682
7772
  type: "text",
7683
- text: JSON.stringify(jobDetails, null, 2),
7773
+ text: JSON.stringify(jobDetails),
7684
7774
  },
7685
7775
  ],
7686
7776
  };
@@ -7702,7 +7792,7 @@ async function handleToolCall(params) {
7702
7792
  const { project_id, ...options } = args;
7703
7793
  const result = await validateCiLint(project_id, options);
7704
7794
  return {
7705
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
7795
+ content: [{ type: "text", text: JSON.stringify(result) }],
7706
7796
  };
7707
7797
  }
7708
7798
  case "validate_project_ci_lint": {
@@ -7710,8 +7800,130 @@ async function handleToolCall(params) {
7710
7800
  const { project_id, ...options } = args;
7711
7801
  const result = await validateProjectCiLint(project_id, options);
7712
7802
  return {
7713
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
7714
- };
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) }] };
7715
7927
  }
7716
7928
  case "create_pipeline": {
7717
7929
  const { project_id, ref, variables, inputs } = CreatePipelineSchema.parse(params.arguments);
@@ -7792,7 +8004,7 @@ async function handleToolCall(params) {
7792
8004
  content: [
7793
8005
  {
7794
8006
  type: "text",
7795
- text: JSON.stringify(artifacts, null, 2),
8007
+ text: JSON.stringify(artifacts),
7796
8008
  },
7797
8009
  ],
7798
8010
  };
@@ -7805,7 +8017,7 @@ async function handleToolCall(params) {
7805
8017
  }
7806
8018
  const downloadUrl = buildDownloadUrl("job-artifacts", { project_id, job_id });
7807
8019
  return {
7808
- 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` }) }],
7809
8021
  };
7810
8022
  }
7811
8023
  const filePath = await downloadJobArtifacts(project_id, job_id, local_path);
@@ -7813,7 +8025,7 @@ async function handleToolCall(params) {
7813
8025
  content: [
7814
8026
  {
7815
8027
  type: "text",
7816
- text: JSON.stringify({ success: true, file_path: filePath }, null, 2),
8028
+ text: JSON.stringify({ success: true, file_path: filePath }),
7817
8029
  },
7818
8030
  ],
7819
8031
  };
@@ -7835,7 +8047,7 @@ async function handleToolCall(params) {
7835
8047
  const cleanedOptions = cleanMutuallyExclusiveIdUsernameOptions(options, LIST_MERGE_REQUESTS_ID_USERNAME_PAIRS);
7836
8048
  const mergeRequests = await listMergeRequests(project_id, cleanedOptions);
7837
8049
  return {
7838
- content: [{ type: "text", text: JSON.stringify(mergeRequests, null, 2) }],
8050
+ content: [{ type: "text", text: JSON.stringify(mergeRequests) }],
7839
8051
  };
7840
8052
  }
7841
8053
  case "list_milestones": {
@@ -7845,7 +8057,7 @@ async function handleToolCall(params) {
7845
8057
  content: [
7846
8058
  {
7847
8059
  type: "text",
7848
- text: JSON.stringify(milestones, null, 2),
8060
+ text: JSON.stringify(milestones),
7849
8061
  },
7850
8062
  ],
7851
8063
  };
@@ -7857,7 +8069,7 @@ async function handleToolCall(params) {
7857
8069
  content: [
7858
8070
  {
7859
8071
  type: "text",
7860
- text: JSON.stringify(milestone, null, 2),
8072
+ text: JSON.stringify(milestone),
7861
8073
  },
7862
8074
  ],
7863
8075
  };
@@ -7869,7 +8081,7 @@ async function handleToolCall(params) {
7869
8081
  content: [
7870
8082
  {
7871
8083
  type: "text",
7872
- text: JSON.stringify(milestone, null, 2),
8084
+ text: JSON.stringify(milestone),
7873
8085
  },
7874
8086
  ],
7875
8087
  };
@@ -7881,7 +8093,7 @@ async function handleToolCall(params) {
7881
8093
  content: [
7882
8094
  {
7883
8095
  type: "text",
7884
- text: JSON.stringify(milestone, null, 2),
8096
+ text: JSON.stringify(milestone),
7885
8097
  },
7886
8098
  ],
7887
8099
  };
@@ -7908,7 +8120,7 @@ async function handleToolCall(params) {
7908
8120
  content: [
7909
8121
  {
7910
8122
  type: "text",
7911
- text: JSON.stringify(issues, null, 2),
8123
+ text: JSON.stringify(issues),
7912
8124
  },
7913
8125
  ],
7914
8126
  };
@@ -7920,7 +8132,7 @@ async function handleToolCall(params) {
7920
8132
  content: [
7921
8133
  {
7922
8134
  type: "text",
7923
- text: JSON.stringify(mergeRequests, null, 2),
8135
+ text: JSON.stringify(mergeRequests),
7924
8136
  },
7925
8137
  ],
7926
8138
  };
@@ -7932,7 +8144,7 @@ async function handleToolCall(params) {
7932
8144
  content: [
7933
8145
  {
7934
8146
  type: "text",
7935
- text: JSON.stringify(milestone, null, 2),
8147
+ text: JSON.stringify(milestone),
7936
8148
  },
7937
8149
  ],
7938
8150
  };
@@ -7944,7 +8156,7 @@ async function handleToolCall(params) {
7944
8156
  content: [
7945
8157
  {
7946
8158
  type: "text",
7947
- text: JSON.stringify(events, null, 2),
8159
+ text: JSON.stringify(events),
7948
8160
  },
7949
8161
  ],
7950
8162
  };
@@ -7953,21 +8165,21 @@ async function handleToolCall(params) {
7953
8165
  const args = ListCommitsSchema.parse(params.arguments);
7954
8166
  const commits = await listCommits(args.project_id, args);
7955
8167
  return {
7956
- content: [{ type: "text", text: JSON.stringify(commits, null, 2) }],
8168
+ content: [{ type: "text", text: JSON.stringify(commits) }],
7957
8169
  };
7958
8170
  }
7959
8171
  case "get_commit": {
7960
8172
  const args = GetCommitSchema.parse(params.arguments);
7961
8173
  const commit = await getCommit(args.project_id, args.sha, args.stats);
7962
8174
  return {
7963
- content: [{ type: "text", text: JSON.stringify(commit, null, 2) }],
8175
+ content: [{ type: "text", text: JSON.stringify(commit) }],
7964
8176
  };
7965
8177
  }
7966
8178
  case "get_commit_diff": {
7967
8179
  const args = GetCommitDiffSchema.parse(params.arguments);
7968
8180
  const diff = await getCommitDiff(args.project_id, args.sha, args.full_diff);
7969
8181
  return {
7970
- content: [{ type: "text", text: JSON.stringify(diff, null, 2) }],
8182
+ content: [{ type: "text", text: JSON.stringify(diff) }],
7971
8183
  };
7972
8184
  }
7973
8185
  case "get_file_blame": {
@@ -7975,7 +8187,7 @@ async function handleToolCall(params) {
7975
8187
  const { project_id, ...options } = args;
7976
8188
  const blame = await getFileBlame(project_id, options);
7977
8189
  return {
7978
- content: [{ type: "text", text: JSON.stringify(blame, null, 2) }],
8190
+ content: [{ type: "text", text: JSON.stringify(blame) }],
7979
8191
  };
7980
8192
  }
7981
8193
  case "list_commit_statuses": {
@@ -7983,7 +8195,7 @@ async function handleToolCall(params) {
7983
8195
  const { project_id, sha, ...options } = args;
7984
8196
  const statuses = await listCommitStatuses(project_id, sha, options);
7985
8197
  return {
7986
- content: [{ type: "text", text: JSON.stringify(statuses, null, 2) }],
8198
+ content: [{ type: "text", text: JSON.stringify(statuses) }],
7987
8199
  };
7988
8200
  }
7989
8201
  case "create_commit_status": {
@@ -7991,14 +8203,14 @@ async function handleToolCall(params) {
7991
8203
  const { project_id, sha, ...options } = args;
7992
8204
  const status = await createCommitStatus(project_id, sha, options);
7993
8205
  return {
7994
- content: [{ type: "text", text: JSON.stringify(status, null, 2) }],
8206
+ content: [{ type: "text", text: JSON.stringify(status) }],
7995
8207
  };
7996
8208
  }
7997
8209
  case "list_group_iterations": {
7998
8210
  const args = ListGroupIterationsSchema.parse(params.arguments);
7999
8211
  const iterations = await listGroupIterations(args.group_id, args);
8000
8212
  return {
8001
- content: [{ type: "text", text: JSON.stringify(iterations, null, 2) }],
8213
+ content: [{ type: "text", text: JSON.stringify(iterations) }],
8002
8214
  };
8003
8215
  }
8004
8216
  // --- CI/CD Variables ---
@@ -8006,24 +8218,24 @@ async function handleToolCall(params) {
8006
8218
  const args = ListProjectVariablesSchema.parse(params.arguments);
8007
8219
  const { project_id, ...options } = args;
8008
8220
  const variables = await listProjectVariables(project_id, options);
8009
- return { content: [{ type: "text", text: JSON.stringify(variables, null, 2) }] };
8221
+ return { content: [{ type: "text", text: JSON.stringify(variables) }] };
8010
8222
  }
8011
8223
  case "get_project_variable": {
8012
8224
  const args = GetProjectVariableSchema.parse(params.arguments);
8013
8225
  const variable = await getProjectVariable(args.project_id, args.key, args.filter);
8014
- return { content: [{ type: "text", text: JSON.stringify(variable, null, 2) }] };
8226
+ return { content: [{ type: "text", text: JSON.stringify(variable) }] };
8015
8227
  }
8016
8228
  case "create_project_variable": {
8017
8229
  const args = CreateProjectVariableSchema.parse(params.arguments);
8018
8230
  const { project_id, ...options } = args;
8019
8231
  const variable = await createProjectVariable(project_id, options);
8020
- return { content: [{ type: "text", text: JSON.stringify(variable, null, 2) }] };
8232
+ return { content: [{ type: "text", text: JSON.stringify(variable) }] };
8021
8233
  }
8022
8234
  case "update_project_variable": {
8023
8235
  const args = UpdateProjectVariableSchema.parse(params.arguments);
8024
8236
  const { project_id, key, ...options } = args;
8025
8237
  const variable = await updateProjectVariable(project_id, key, options);
8026
- return { content: [{ type: "text", text: JSON.stringify(variable, null, 2) }] };
8238
+ return { content: [{ type: "text", text: JSON.stringify(variable) }] };
8027
8239
  }
8028
8240
  case "delete_project_variable": {
8029
8241
  const args = DeleteProjectVariableSchema.parse(params.arguments);
@@ -8042,27 +8254,27 @@ async function handleToolCall(params) {
8042
8254
  const args = ListGroupVariablesSchema.parse(params.arguments);
8043
8255
  const { group_id, ...options } = args;
8044
8256
  const variables = await listGroupVariables(group_id, options);
8045
- return { content: [{ type: "text", text: JSON.stringify(variables, null, 2) }] };
8257
+ return { content: [{ type: "text", text: JSON.stringify(variables) }] };
8046
8258
  }
8047
8259
  case "get_group_variable": {
8048
8260
  rejectIfProjectScopedDeployment("get_group_variable");
8049
8261
  const args = GetGroupVariableSchema.parse(params.arguments);
8050
8262
  const variable = await getGroupVariable(args.group_id, args.key, args.filter);
8051
- return { content: [{ type: "text", text: JSON.stringify(variable, null, 2) }] };
8263
+ return { content: [{ type: "text", text: JSON.stringify(variable) }] };
8052
8264
  }
8053
8265
  case "create_group_variable": {
8054
8266
  rejectIfProjectScopedDeployment("create_group_variable");
8055
8267
  const args = CreateGroupVariableSchema.parse(params.arguments);
8056
8268
  const { group_id, ...options } = args;
8057
8269
  const variable = await createGroupVariable(group_id, options);
8058
- return { content: [{ type: "text", text: JSON.stringify(variable, null, 2) }] };
8270
+ return { content: [{ type: "text", text: JSON.stringify(variable) }] };
8059
8271
  }
8060
8272
  case "update_group_variable": {
8061
8273
  rejectIfProjectScopedDeployment("update_group_variable");
8062
8274
  const args = UpdateGroupVariableSchema.parse(params.arguments);
8063
8275
  const { group_id, key, ...options } = args;
8064
8276
  const variable = await updateGroupVariable(group_id, key, options);
8065
- return { content: [{ type: "text", text: JSON.stringify(variable, null, 2) }] };
8277
+ return { content: [{ type: "text", text: JSON.stringify(variable) }] };
8066
8278
  }
8067
8279
  case "delete_group_variable": {
8068
8280
  rejectIfProjectScopedDeployment("delete_group_variable");
@@ -8082,7 +8294,7 @@ async function handleToolCall(params) {
8082
8294
  const args = GetDependencyProxySettingsSchema.parse(params.arguments);
8083
8295
  const settings = await getDependencyProxySettings(args.group_id);
8084
8296
  return {
8085
- content: [{ type: "text", text: JSON.stringify(settings, null, 2) }],
8297
+ content: [{ type: "text", text: JSON.stringify(settings) }],
8086
8298
  };
8087
8299
  }
8088
8300
  case "update_dependency_proxy_settings": {
@@ -8091,7 +8303,7 @@ async function handleToolCall(params) {
8091
8303
  const { group_id, ...options } = args;
8092
8304
  const settings = await updateDependencyProxySettings(group_id, options);
8093
8305
  return {
8094
- content: [{ type: "text", text: JSON.stringify(settings, null, 2) }],
8306
+ content: [{ type: "text", text: JSON.stringify(settings) }],
8095
8307
  };
8096
8308
  }
8097
8309
  case "list_dependency_proxy_blobs": {
@@ -8100,7 +8312,7 @@ async function handleToolCall(params) {
8100
8312
  const { group_id, ...options } = args;
8101
8313
  const result = await listDependencyProxyBlobs(group_id, options);
8102
8314
  return {
8103
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
8315
+ content: [{ type: "text", text: JSON.stringify(result) }],
8104
8316
  };
8105
8317
  }
8106
8318
  case "purge_dependency_proxy_cache": {
@@ -8121,13 +8333,13 @@ async function handleToolCall(params) {
8121
8333
  const args = MarkdownUploadRemoteSchema.parse(params.arguments);
8122
8334
  const upload = await markdownUpload(args.project_id, undefined, args.content, args.filename);
8123
8335
  return {
8124
- content: [{ type: "text", text: JSON.stringify(upload, null, 2) }],
8336
+ content: [{ type: "text", text: JSON.stringify(upload) }],
8125
8337
  };
8126
8338
  }
8127
8339
  const args = MarkdownUploadSchema.parse(params.arguments);
8128
8340
  const upload = await markdownUpload(args.project_id, args.file_path);
8129
8341
  return {
8130
- content: [{ type: "text", text: JSON.stringify(upload, null, 2) }],
8342
+ content: [{ type: "text", text: JSON.stringify(upload) }],
8131
8343
  };
8132
8344
  }
8133
8345
  case "download_attachment": {
@@ -8139,10 +8351,12 @@ async function handleToolCall(params) {
8139
8351
  const mimeType = getImageMimeType(args.filename);
8140
8352
  if (IS_REMOTE && !mimeType) {
8141
8353
  const downloadUrl = buildDownloadUrl("attachment", {
8142
- 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,
8143
8357
  });
8144
8358
  return {
8145
- 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 }) }],
8146
8360
  };
8147
8361
  }
8148
8362
  const result = await downloadAttachment(args.project_id, args.secret, args.filename, args.local_path);
@@ -8154,7 +8368,7 @@ async function handleToolCall(params) {
8154
8368
  { type: "image", data: base64, mimeType: result.mimeType },
8155
8369
  {
8156
8370
  type: "text",
8157
- text: JSON.stringify({ filename: result.filename, mimeType: result.mimeType }, null, 2),
8371
+ text: JSON.stringify({ filename: result.filename, mimeType: result.mimeType }),
8158
8372
  },
8159
8373
  ],
8160
8374
  };
@@ -8163,7 +8377,7 @@ async function handleToolCall(params) {
8163
8377
  content: [
8164
8378
  {
8165
8379
  type: "text",
8166
- text: JSON.stringify({ success: true, file_path: result.savedPath }, null, 2),
8380
+ text: JSON.stringify({ success: true, file_path: result.savedPath }),
8167
8381
  },
8168
8382
  ],
8169
8383
  };
@@ -8172,7 +8386,7 @@ async function handleToolCall(params) {
8172
8386
  const args = ListEventsSchema.parse(params.arguments);
8173
8387
  const events = await listEvents(args);
8174
8388
  return {
8175
- content: [{ type: "text", text: JSON.stringify(events, null, 2) }],
8389
+ content: [{ type: "text", text: JSON.stringify(events) }],
8176
8390
  };
8177
8391
  }
8178
8392
  case "get_project_events": {
@@ -8180,7 +8394,7 @@ async function handleToolCall(params) {
8180
8394
  const { project_id, ...options } = args;
8181
8395
  const events = await getProjectEvents(project_id, options);
8182
8396
  return {
8183
- content: [{ type: "text", text: JSON.stringify(events, null, 2) }],
8397
+ content: [{ type: "text", text: JSON.stringify(events) }],
8184
8398
  };
8185
8399
  }
8186
8400
  case "list_releases": {
@@ -8188,14 +8402,14 @@ async function handleToolCall(params) {
8188
8402
  const { project_id, ...options } = args;
8189
8403
  const releases = await listReleases(project_id, options);
8190
8404
  return {
8191
- content: [{ type: "text", text: JSON.stringify(releases, null, 2) }],
8405
+ content: [{ type: "text", text: JSON.stringify(releases) }],
8192
8406
  };
8193
8407
  }
8194
8408
  case "get_release": {
8195
8409
  const args = GetReleaseSchema.parse(params.arguments);
8196
8410
  const release = await getRelease(args.project_id, args.tag_name, args.include_html_description);
8197
8411
  return {
8198
- content: [{ type: "text", text: JSON.stringify(release, null, 2) }],
8412
+ content: [{ type: "text", text: JSON.stringify(release) }],
8199
8413
  };
8200
8414
  }
8201
8415
  case "create_release": {
@@ -8203,7 +8417,7 @@ async function handleToolCall(params) {
8203
8417
  const { project_id, ...options } = args;
8204
8418
  const release = await createRelease(project_id, options);
8205
8419
  return {
8206
- content: [{ type: "text", text: JSON.stringify(release, null, 2) }],
8420
+ content: [{ type: "text", text: JSON.stringify(release) }],
8207
8421
  };
8208
8422
  }
8209
8423
  case "update_release": {
@@ -8211,7 +8425,7 @@ async function handleToolCall(params) {
8211
8425
  const { project_id, tag_name, ...options } = args;
8212
8426
  const release = await updateRelease(project_id, tag_name, options);
8213
8427
  return {
8214
- content: [{ type: "text", text: JSON.stringify(release, null, 2) }],
8428
+ content: [{ type: "text", text: JSON.stringify(release) }],
8215
8429
  };
8216
8430
  }
8217
8431
  case "delete_release": {
@@ -8242,10 +8456,12 @@ async function handleToolCall(params) {
8242
8456
  const args = DownloadReleaseAssetSchema.parse(params.arguments);
8243
8457
  if (IS_REMOTE) {
8244
8458
  const downloadUrl = buildDownloadUrl("release-asset", {
8245
- 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,
8246
8462
  });
8247
8463
  return {
8248
- 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 }) }],
8249
8465
  };
8250
8466
  }
8251
8467
  const assetContent = await downloadReleaseAsset(args.project_id, args.tag_name, args.direct_asset_path);
@@ -8258,14 +8474,14 @@ async function handleToolCall(params) {
8258
8474
  const { project_id, ...options } = args;
8259
8475
  const tags = await listTags(project_id, options);
8260
8476
  return {
8261
- content: [{ type: "text", text: JSON.stringify(tags, null, 2) }],
8477
+ content: [{ type: "text", text: JSON.stringify(tags) }],
8262
8478
  };
8263
8479
  }
8264
8480
  case "get_tag": {
8265
8481
  const args = GetTagSchema.parse(params.arguments);
8266
8482
  const tag = await getTag(args.project_id, args.tag_name);
8267
8483
  return {
8268
- content: [{ type: "text", text: JSON.stringify(tag, null, 2) }],
8484
+ content: [{ type: "text", text: JSON.stringify(tag) }],
8269
8485
  };
8270
8486
  }
8271
8487
  case "create_tag": {
@@ -8273,7 +8489,7 @@ async function handleToolCall(params) {
8273
8489
  const { project_id, ...options } = args;
8274
8490
  const tag = await createTag(project_id, options);
8275
8491
  return {
8276
- content: [{ type: "text", text: JSON.stringify(tag, null, 2) }],
8492
+ content: [{ type: "text", text: JSON.stringify(tag) }],
8277
8493
  };
8278
8494
  }
8279
8495
  case "delete_tag": {
@@ -8292,30 +8508,28 @@ async function handleToolCall(params) {
8292
8508
  const args = GetTagSignatureSchema.parse(params.arguments);
8293
8509
  const signature = await getTagSignature(args.project_id, args.tag_name);
8294
8510
  return {
8295
- content: [{ type: "text", text: JSON.stringify(signature, null, 2) }],
8511
+ content: [{ type: "text", text: JSON.stringify(signature) }],
8296
8512
  };
8297
8513
  }
8298
8514
  case "list_webhooks": {
8299
8515
  const args = ListWebhooksSchema.parse(params.arguments);
8300
8516
  const webhooks = await listWebhooks(args);
8301
8517
  return {
8302
- content: [{ type: "text", text: JSON.stringify(webhooks, null, 2) }],
8518
+ content: [{ type: "text", text: JSON.stringify(webhooks) }],
8303
8519
  };
8304
8520
  }
8305
8521
  case "list_webhook_events": {
8306
8522
  const args = ListWebhookEventsSchema.parse(params.arguments);
8307
8523
  const events = await listWebhookEvents(args);
8308
8524
  return {
8309
- content: [{ type: "text", text: JSON.stringify(events, null, 2) }],
8525
+ content: [{ type: "text", text: JSON.stringify(events) }],
8310
8526
  };
8311
8527
  }
8312
8528
  case "get_webhook_event": {
8313
8529
  const args = GetWebhookEventSchema.parse(params.arguments);
8314
8530
  const event = await getWebhookEvent(args);
8315
8531
  if (!event) {
8316
- const searchScope = args.page
8317
- ? `on page ${args.page}`
8318
- : "in the 500 most recent events";
8532
+ const searchScope = args.page ? `on page ${args.page}` : "in the 500 most recent events";
8319
8533
  return {
8320
8534
  content: [
8321
8535
  {
@@ -8326,7 +8540,7 @@ async function handleToolCall(params) {
8326
8540
  };
8327
8541
  }
8328
8542
  return {
8329
- content: [{ type: "text", text: JSON.stringify(event, null, 2) }],
8543
+ content: [{ type: "text", text: JSON.stringify(event) }],
8330
8544
  };
8331
8545
  }
8332
8546
  case "health_check": {
@@ -8334,13 +8548,24 @@ async function handleToolCall(params) {
8334
8548
  const url = new URL(`${getEffectiveApiUrl()}/user`);
8335
8549
  const response = await fetch(url.toString(), getFetchConfig());
8336
8550
  let authenticated = response.ok;
8337
- 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())) {
8338
8554
  const jobUrl = new URL(`${getEffectiveApiUrl()}/job`);
8339
8555
  const jobResponse = await fetch(jobUrl.toString(), getFetchConfig());
8340
8556
  authenticated = jobResponse.ok;
8341
8557
  }
8342
8558
  return {
8343
- 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
+ ],
8344
8569
  };
8345
8570
  }
8346
8571
  case "get_branch": {
@@ -8354,7 +8579,7 @@ async function handleToolCall(params) {
8354
8579
  const data = await response.json();
8355
8580
  const branch = GitLabBranchSchema.parse(data);
8356
8581
  return {
8357
- content: [{ type: "text", text: JSON.stringify(branch, null, 2) }],
8582
+ content: [{ type: "text", text: JSON.stringify(branch) }],
8358
8583
  };
8359
8584
  }
8360
8585
  case "list_branches": {
@@ -8377,7 +8602,7 @@ async function handleToolCall(params) {
8377
8602
  const data = await response.json();
8378
8603
  const branches = z.array(GitLabBranchSchema).parse(data);
8379
8604
  return {
8380
- content: [{ type: "text", text: JSON.stringify(branches, null, 2) }],
8605
+ content: [{ type: "text", text: JSON.stringify(branches) }],
8381
8606
  };
8382
8607
  }
8383
8608
  case "delete_branch": {
@@ -8390,7 +8615,7 @@ async function handleToolCall(params) {
8390
8615
  });
8391
8616
  await handleGitLabError(response);
8392
8617
  return {
8393
- 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 }) }],
8394
8619
  };
8395
8620
  }
8396
8621
  case "list_protected_branches": {
@@ -8410,7 +8635,7 @@ async function handleToolCall(params) {
8410
8635
  await handleGitLabError(response);
8411
8636
  const data = z.array(GitLabProtectedBranchSchema).parse(await response.json());
8412
8637
  return {
8413
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
8638
+ content: [{ type: "text", text: JSON.stringify(data) }],
8414
8639
  };
8415
8640
  }
8416
8641
  case "get_protected_branch": {
@@ -8424,7 +8649,7 @@ async function handleToolCall(params) {
8424
8649
  await handleGitLabError(response);
8425
8650
  const data = GitLabProtectedBranchSchema.parse(await response.json());
8426
8651
  return {
8427
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
8652
+ content: [{ type: "text", text: JSON.stringify(data) }],
8428
8653
  };
8429
8654
  }
8430
8655
  case "protect_branch": {
@@ -8451,7 +8676,7 @@ async function handleToolCall(params) {
8451
8676
  await handleGitLabError(response);
8452
8677
  const data = GitLabProtectedBranchSchema.parse(await response.json());
8453
8678
  return {
8454
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
8679
+ content: [{ type: "text", text: JSON.stringify(data) }],
8455
8680
  };
8456
8681
  }
8457
8682
  case "unprotect_branch": {
@@ -8465,7 +8690,7 @@ async function handleToolCall(params) {
8465
8690
  });
8466
8691
  await handleGitLabError(response);
8467
8692
  return {
8468
- 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 }) }],
8469
8694
  };
8470
8695
  }
8471
8696
  case "update_default_branch": {
@@ -8481,7 +8706,7 @@ async function handleToolCall(params) {
8481
8706
  await handleGitLabError(response);
8482
8707
  const data = await response.json();
8483
8708
  return {
8484
- 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 }) }],
8485
8710
  };
8486
8711
  }
8487
8712
  default:
@@ -8660,7 +8885,9 @@ function registerDownloadProxy(app, maxRequestsPerMinute = Number.parseInt(proce
8660
8885
  case "release-asset": {
8661
8886
  const { project_id, tag_name, direct_asset_path } = req.query;
8662
8887
  if (!project_id || !tag_name || !direct_asset_path) {
8663
- 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" });
8664
8891
  return;
8665
8892
  }
8666
8893
  const effectiveProjectId = getEffectiveProjectId(decodeURIComponent(project_id));
@@ -9067,7 +9294,7 @@ async function startStreamableHTTPServer() {
9067
9294
  token: effective.token,
9068
9295
  lastUsed: Date.now(),
9069
9296
  apiUrl: effective.apiUrl,
9070
- publicBaseUrl: getForwardedPublicBaseUrl(req),
9297
+ publicBaseUrl: getForwardedPublicBaseUrl(req, MCP_TRUST_PROXY),
9071
9298
  };
9072
9299
  // Step 4: create a fresh transport per request.
9073
9300
  const transport = isInit
@@ -9261,16 +9488,14 @@ async function startStreamableHTTPServer() {
9261
9488
  // Streamable HTTP endpoint - handles both session creation and message handling
9262
9489
  app.post("/mcp", mcpBearerAuth, async (req, res) => {
9263
9490
  const sessionId = readMcpSessionIdHeader(req);
9264
- const publicBaseUrl = getForwardedPublicBaseUrl(req);
9491
+ const publicBaseUrl = getForwardedPublicBaseUrl(req, MCP_TRUST_PROXY);
9265
9492
  // Track request
9266
9493
  metrics.requestsProcessed++;
9267
9494
  // Stateless-mode branch: bypass authBySession / streamableTransports
9268
9495
  // entirely and derive the session auth from either the current request
9269
9496
  // headers (init) or a sealed Mcp-Session-Id (subsequent requests).
9270
9497
  // Rate limiting is disabled here because there is no shared counter.
9271
- if (OAUTH_STATELESS_MODE &&
9272
- STATELESS_MATERIAL &&
9273
- (REMOTE_AUTHORIZATION || GITLAB_MCP_OAUTH)) {
9498
+ if (OAUTH_STATELESS_MODE && STATELESS_MATERIAL && (REMOTE_AUTHORIZATION || GITLAB_MCP_OAUTH)) {
9274
9499
  await handleStatelessMcpRequest(req, res, STATELESS_MATERIAL, OAUTH_STATELESS_SESSION_TTL_SECONDS);
9275
9500
  return;
9276
9501
  }
@@ -9295,9 +9520,11 @@ async function startStreamableHTTPServer() {
9295
9520
  // Handle remote authorization: extract and store auth headers per session
9296
9521
  if (REMOTE_AUTHORIZATION) {
9297
9522
  const authData = parseAuthHeaders(req);
9523
+ const allowUnauthenticatedDiscovery = GITLAB_ALLOW_UNAUTHENTICATED_TOOL_DISCOVERY &&
9524
+ isUnauthenticatedDiscoveryRequestBody(req.body);
9298
9525
  if (sessionId && !authBySession[sessionId]) {
9299
- // New session: require auth headers
9300
- if (!authData) {
9526
+ // New session: require auth headers unless public discovery was explicitly enabled.
9527
+ if (!authData && !allowUnauthenticatedDiscovery) {
9301
9528
  metrics.authFailures++;
9302
9529
  res.status(401).json({
9303
9530
  error: "Missing Private-Token, JOB-TOKEN, or Authorization header",
@@ -9305,10 +9532,16 @@ async function startStreamableHTTPServer() {
9305
9532
  });
9306
9533
  return;
9307
9534
  }
9308
- // Store auth for this session
9309
- authBySession[sessionId] = withPublicBaseUrl(authData, publicBaseUrl);
9310
- logger.info(`Session ${sessionId}: stored ${authData.header} header`);
9311
- 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
+ }
9312
9545
  }
9313
9546
  else if (sessionId && authData) {
9314
9547
  // Existing session: allow auth rotation/update
@@ -9481,25 +9714,112 @@ async function startStreamableHTTPServer() {
9481
9714
  message: "GET /mcp is not supported when STREAMABLE_HTTP is enabled. Use POST to communicate with the MCP server.",
9482
9715
  });
9483
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
+ };
9484
9817
  // Metrics endpoint
9485
9818
  app.get("/metrics", (_req, res) => {
9486
- res.json({
9487
- ...metrics,
9488
- activeSessions: Object.keys(streamableTransports).length,
9489
- authenticatedSessions: Object.keys(authBySession).length,
9490
- gitlabClientPool: clientPool.getStats(),
9491
- uptime: process.uptime(),
9492
- memoryUsage: process.memoryUsage(),
9493
- config: {
9494
- maxSessions: MAX_SESSIONS,
9495
- maxRequestsPerMinute: MAX_REQUESTS_PER_MINUTE,
9496
- sessionTimeoutSeconds: SESSION_TIMEOUT_SECONDS,
9497
- remoteAuthEnabled: REMOTE_AUTHORIZATION,
9498
- mcpOAuthEnabled: GITLAB_MCP_OAUTH,
9499
- statelessModeEnabled: OAUTH_STATELESS_MODE && STATELESS_MATERIAL !== null,
9500
- statelessRotationKey: OAUTH_STATELESS_MODE && STATELESS_MATERIAL?.previous != null,
9501
- },
9502
- });
9819
+ res.type("text/plain; version=0.0.4").send(formatPrometheusMetrics());
9820
+ });
9821
+ app.get("/metrics.json", (_req, res) => {
9822
+ res.json(getMetricsSnapshot());
9503
9823
  });
9504
9824
  // Health check endpoint
9505
9825
  app.get("/health", (_req, res) => {