@zereight/mcp-gitlab 2.1.25 → 2.1.27

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
@@ -507,6 +467,14 @@ function createServer() {
507
467
  /**
508
468
  * Validate configuration at startup
509
469
  */
470
+ function isLoopbackBindHost(host) {
471
+ const normalized = host.trim().toLowerCase().replace(/^\[|\]$/g, "");
472
+ const isIpv4Loopback = /^127(?:\.\d{1,3}){3}$/.test(normalized);
473
+ return (normalized === "localhost" ||
474
+ isIpv4Loopback ||
475
+ normalized === "::1" ||
476
+ normalized === "0:0:0:0:0:0:0:1");
477
+ }
510
478
  function validateConfiguration() {
511
479
  const errors = [];
512
480
  // Validate SESSION_TIMEOUT_SECONDS
@@ -557,6 +525,12 @@ function validateConfiguration() {
557
525
  }
558
526
  }
559
527
  }
528
+ const allowedHosts = getConfig("allowed-hosts", "GITLAB_ALLOWED_HOSTS")?.split(",") || [];
529
+ for (const host of allowedHosts) {
530
+ if (host.trim() && !toAllowedGitLabApiUrl(host)) {
531
+ errors.push(`GITLAB_ALLOWED_HOSTS contains an invalid host or URL: ${host.trim()}`);
532
+ }
533
+ }
560
534
  // Validate auth configuration
561
535
  const remoteAuth = getConfig("remote-auth", "REMOTE_AUTHORIZATION") === "true";
562
536
  const useOAuth = getConfig("use-oauth", "GITLAB_USE_OAUTH") === "true";
@@ -566,12 +540,19 @@ function validateConfiguration() {
566
540
  const mcpOAuth = getConfig("mcp-oauth", "GITLAB_MCP_OAUTH") === "true";
567
541
  const mcpServerUrl = getConfig("mcp-server-url", "MCP_SERVER_URL");
568
542
  const streamableHttp = getConfig("streamable-http", "STREAMABLE_HTTP") === "true";
543
+ const sse = getConfig("sse", "SSE") === "true";
544
+ const bindHost = getConfig("host", "HOST") || "127.0.0.1";
545
+ const sseAuthToken = getConfig("sse-auth-token", "SSE_AUTH_TOKEN");
546
+ const allowUnauthenticatedRemoteSse = getConfig("sse-dangerously-allow-unauthenticated-remote", "SSE_DANGEROUSLY_ALLOW_UNAUTHENTICATED_REMOTE") === "true";
569
547
  if (!remoteAuth && !useOAuth && !hasToken && !hasJobToken && !hasCookie && !mcpOAuth) {
570
548
  errors.push("Either --token, --job-token, --cookie-path, --use-oauth=true, --remote-auth=true, or --mcp-oauth=true must be set (or use environment variables)");
571
549
  }
572
550
  if (streamableHttp && (hasToken || hasJobToken) && !remoteAuth && !mcpOAuth) {
573
551
  errors.push("STREAMABLE_HTTP=true/--streamable-http with GITLAB_PERSONAL_ACCESS_TOKEN/--token or GITLAB_JOB_TOKEN/--job-token requires REMOTE_AUTHORIZATION=true/--remote-auth=true or GITLAB_MCP_OAUTH=true/--mcp-oauth=true");
574
552
  }
553
+ if (sse && !isLoopbackBindHost(bindHost) && !sseAuthToken && !allowUnauthenticatedRemoteSse) {
554
+ errors.push("SSE=true on a non-loopback HOST requires SSE_AUTH_TOKEN (or explicitly set SSE_DANGEROUSLY_ALLOW_UNAUTHENTICATED_REMOTE=true)");
555
+ }
575
556
  if (mcpOAuth) {
576
557
  if (!mcpServerUrl) {
577
558
  errors.push("MCP_SERVER_URL is required when GITLAB_MCP_OAUTH=true (e.g. https://mcp.example.com)");
@@ -636,13 +617,24 @@ function redactSessionIdForLog(sid) {
636
617
  function isInitializationRequestBody(body) {
637
618
  if (!body)
638
619
  return false;
639
- const isInitObj = (m) => typeof m === "object" &&
640
- m !== null &&
641
- m.method === "initialize";
620
+ const isInitObj = (m) => typeof m === "object" && m !== null && m.method === "initialize";
642
621
  if (Array.isArray(body))
643
622
  return body.some(isInitObj);
644
623
  return isInitObj(body);
645
624
  }
625
+ function isUnauthenticatedDiscoveryRequestBody(body) {
626
+ if (!body)
627
+ return false;
628
+ const isDiscoveryMethod = (m) => {
629
+ if (typeof m !== "object" || m === null)
630
+ return false;
631
+ const method = m.method;
632
+ return (method === "initialize" || method === "notifications/initialized" || method === "tools/list");
633
+ };
634
+ if (Array.isArray(body))
635
+ return body.every(isDiscoveryMethod);
636
+ return isDiscoveryMethod(body);
637
+ }
646
638
  /**
647
639
  * Normalize an `Mcp-Session-Id` header value.
648
640
  *
@@ -699,9 +691,7 @@ catch (err) {
699
691
  * a 401 from the OAuth bearer middleware.
700
692
  */
701
693
  export function hasStatelessSessionId(req) {
702
- return Boolean(OAUTH_STATELESS_MODE &&
703
- STATELESS_MATERIAL &&
704
- readMcpSessionIdHeader(req));
694
+ return Boolean(OAUTH_STATELESS_MODE && STATELESS_MATERIAL && readMcpSessionIdHeader(req));
705
695
  }
706
696
  /**
707
697
  * Ensure the OAuth token is valid before making an API call.
@@ -845,7 +835,9 @@ function defaultAuthRetryConfig() {
845
835
  return {
846
836
  isOAuthEnabled: () => USE_OAUTH && oauthClient != null,
847
837
  refreshToken: (force) => oauthClient.getAccessToken(force),
848
- onTokenRefreshed: (token) => { OAUTH_ACCESS_TOKEN = token; },
838
+ onTokenRefreshed: (token) => {
839
+ OAUTH_ACCESS_TOKEN = token;
840
+ },
849
841
  buildAuthHeaders,
850
842
  logger,
851
843
  };
@@ -1015,11 +1007,57 @@ if (GITLAB_TOOLSETS_RAW && (USE_PIPELINE || USE_MILESTONE || USE_GITLAB_WIKI)) {
1015
1007
  "Legacy flags add tools additively on top of the toolset selection and may produce unexpected results.");
1016
1008
  }
1017
1009
  const MERGE_REQUEST_DEPLOYMENT_SUMMARY_LIMIT = 10;
1010
+ function toAllowedGitLabApiUrl(value) {
1011
+ const trimmed = value.trim();
1012
+ if (!trimmed)
1013
+ return null;
1014
+ try {
1015
+ const url = new URL(trimmed.includes("://") ? trimmed : `https://${trimmed}`);
1016
+ if (url.protocol !== "http:" && url.protocol !== "https:")
1017
+ return null;
1018
+ return { host: url.host, apiUrl: normalizeGitLabApiUrl(url.toString()) };
1019
+ }
1020
+ catch {
1021
+ return null;
1022
+ }
1023
+ }
1024
+ function parseAllowedGitLabApiUrls(value) {
1025
+ return value
1026
+ .split(",")
1027
+ .map(toAllowedGitLabApiUrl)
1028
+ .filter((entry) => Boolean(entry));
1029
+ }
1030
+ function encodeGitLabPathSegment(value) {
1031
+ return encodeURIComponent(decodeURIComponent(value));
1032
+ }
1033
+ function encodeGitLabPath(value) {
1034
+ return value.split("/").map(encodeGitLabPathSegment).join("/");
1035
+ }
1036
+ function resolveTrustedGitLabApiUrl(value) {
1037
+ const parsed = new URL(normalizeGitLabApiUrl(value));
1038
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
1039
+ throw new Error("GitLab API URL must use HTTP or HTTPS");
1040
+ }
1041
+ const allowedApiUrl = GITLAB_ALLOWED_API_URLS_BY_HOST.get(parsed.host);
1042
+ if (!allowedApiUrl) {
1043
+ throw new Error(`GitLab API URL host is not allowed: ${parsed.host}`);
1044
+ }
1045
+ return allowedApiUrl;
1046
+ }
1018
1047
  // Use the normalizeGitLabApiUrl function to handle various URL formats
1019
1048
  const GITLAB_API_URLS = (getConfig("api-url", "GITLAB_API_URL") || "https://gitlab.com")
1020
1049
  .split(",")
1021
1050
  .map(normalizeGitLabApiUrl);
1022
1051
  const GITLAB_API_URL = GITLAB_API_URLS[0];
1052
+ const GITLAB_ALLOWED_API_URLS_BY_HOST = new Map();
1053
+ for (const { host, apiUrl } of [
1054
+ ...GITLAB_API_URLS.map(toAllowedGitLabApiUrl).filter((entry) => Boolean(entry)),
1055
+ ...parseAllowedGitLabApiUrls(getConfig("allowed-hosts", "GITLAB_ALLOWED_HOSTS") || ""),
1056
+ ]) {
1057
+ if (!GITLAB_ALLOWED_API_URLS_BY_HOST.has(host)) {
1058
+ GITLAB_ALLOWED_API_URLS_BY_HOST.set(host, apiUrl);
1059
+ }
1060
+ }
1023
1061
  const GITLAB_PROJECT_ID = process.env.GITLAB_PROJECT_ID;
1024
1062
  const GITLAB_ALLOWED_PROJECT_IDS = process.env.GITLAB_ALLOWED_PROJECT_IDS?.split(",")
1025
1063
  .map(id => id.trim())
@@ -1058,7 +1096,12 @@ if (GITLAB_MCP_OAUTH) {
1058
1096
  }
1059
1097
  logger.info("MCP OAuth enabled: GitLab OAuth proxy active (Private-Token/JOB-TOKEN headers bypass OAuth)");
1060
1098
  }
1061
- if (!REMOTE_AUTHORIZATION && !GITLAB_MCP_OAUTH && !USE_OAUTH && !GITLAB_PERSONAL_ACCESS_TOKEN && !GITLAB_JOB_TOKEN && !GITLAB_AUTH_COOKIE_PATH) {
1099
+ if (!REMOTE_AUTHORIZATION &&
1100
+ !GITLAB_MCP_OAUTH &&
1101
+ !USE_OAUTH &&
1102
+ !GITLAB_PERSONAL_ACCESS_TOKEN &&
1103
+ !GITLAB_JOB_TOKEN &&
1104
+ !GITLAB_AUTH_COOKIE_PATH) {
1062
1105
  // Standard mode: token must be in environment (unless using OAuth)
1063
1106
  logger.error("GITLAB_PERSONAL_ACCESS_TOKEN environment variable is not set");
1064
1107
  logger.info("Either set GITLAB_PERSONAL_ACCESS_TOKEN or enable OAuth with GITLAB_USE_OAUTH=true");
@@ -1483,18 +1526,28 @@ async function resolveNamesToIds(projectPath, labelNames, usernames) {
1483
1526
  if (!labelNames?.length && !usernames?.length) {
1484
1527
  return { labelIds: [], userIds: [] };
1485
1528
  }
1486
- const data = await executeGraphQL(`query($path: ID!, $usernames: [String!]!) {
1487
- project(fullPath: $path) { labels(includeAncestorGroups: true, first: 250) { nodes { id title } } }
1529
+ labelNames ??= [];
1530
+ usernames ??= [];
1531
+ const labelVars = Object.fromEntries(labelNames.map((name, i) => [`l${i}`, name]));
1532
+ // One alias per label — exact title match via the `title` argument, includes ancestor
1533
+ // group labels, single round trip with no pagination needed.
1534
+ const varDefs = labelNames.map((_, i) => `$l${i}: String!`).join(", ");
1535
+ const aliases = labelNames.map((_, i) => `l${i}: labels(title: $l${i}, includeAncestorGroups: true, first: 1) { nodes { id } }`).join(" ");
1536
+ const { project, users } = await executeGraphQL(`query($path: ID!, $usernames: [String!]!${varDefs ? `, ${varDefs}` : ""}) {
1537
+ project(fullPath: $path) { ${aliases || "__typename"} }
1488
1538
  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)
1539
+ }`, { path: projectPath, usernames, ...labelVars });
1540
+ if (!project) {
1541
+ throw new Error(`Project '${projectPath}' not found or inaccessible`);
1542
+ }
1543
+ const labelIds = labelNames.map((name, i) => {
1544
+ const nodes = project[`l${i}`]?.nodes;
1545
+ if (!nodes?.length)
1493
1546
  throw new Error(`Label '${name}' not found in project`);
1494
- return label.id;
1547
+ return nodes[0].id;
1495
1548
  });
1496
- const userIds = (usernames || []).map(name => {
1497
- const user = data.users.nodes.find(u => u.username === name);
1549
+ const userIds = usernames.map(name => {
1550
+ const user = users.nodes.find(u => u.username === name);
1498
1551
  if (!user)
1499
1552
  throw new Error(`User '${name}' not found`);
1500
1553
  return user.id;
@@ -1534,7 +1587,7 @@ async function resolveWorkItemTypeGID(projectPath, typeName) {
1534
1587
  }
1535
1588
  }
1536
1589
  }`, { path: projectPath });
1537
- const typeNode = data.namespace?.workItemTypes?.nodes?.find((n) => n.name === targetName);
1590
+ const typeNode = data.namespace?.workItemTypes?.nodes?.find(n => n.name === targetName);
1538
1591
  if (!typeNode) {
1539
1592
  throw new Error(`Work item type '${targetName}' not found in project ${projectPath}`);
1540
1593
  }
@@ -2140,7 +2193,11 @@ async function getWorkItem(projectId, iid) {
2140
2193
  if (wi.closedAt)
2141
2194
  result.closedAt = wi.closedAt;
2142
2195
  if (statusWidget?.status)
2143
- result.status = { name: statusWidget.status.name, id: statusWidget.status.id, category: statusWidget.status.category };
2196
+ result.status = {
2197
+ name: statusWidget.status.name,
2198
+ id: statusWidget.status.id,
2199
+ category: statusWidget.status.category,
2200
+ };
2144
2201
  const labels = (labelsWidget?.labels?.nodes || []).map((l) => l.title);
2145
2202
  if (labels.length > 0)
2146
2203
  result.labels = labels;
@@ -2178,10 +2235,23 @@ async function getWorkItem(projectId, iid) {
2178
2235
  if (colorWidget?.color)
2179
2236
  result.color = colorWidget.color;
2180
2237
  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 };
2238
+ result.parent = {
2239
+ iid: hierarchyWidget.parent.iid,
2240
+ title: hierarchyWidget.parent.title,
2241
+ type: hierarchyWidget.parent.workItemType?.name,
2242
+ project: hierarchyWidget.parent.namespace?.fullPath,
2243
+ webUrl: hierarchyWidget.parent.webUrl,
2244
+ };
2182
2245
  const children = hierarchyWidget?.children?.nodes || [];
2183
2246
  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 }));
2247
+ result.children = children.map((c) => ({
2248
+ iid: c.iid,
2249
+ title: c.title,
2250
+ state: c.state,
2251
+ type: c.workItemType?.name,
2252
+ project: c.namespace?.fullPath,
2253
+ webUrl: c.webUrl,
2254
+ }));
2185
2255
  if (linkedItemsWidget?.blocked)
2186
2256
  result.blocked = true;
2187
2257
  if (linkedItemsWidget?.blockedByCount > 0)
@@ -2253,7 +2323,7 @@ async function listWorkItems(projectId, options) {
2253
2323
  first: options.first || 20,
2254
2324
  };
2255
2325
  if (options.types && options.types.length > 0) {
2256
- variables.types = options.types.map((t) => typeMap[t] || t.replace(/ /g, "_").toUpperCase());
2326
+ variables.types = options.types.map(t => typeMap[t] || t.replace(/ /g, "_").toUpperCase());
2257
2327
  }
2258
2328
  if (options.state) {
2259
2329
  variables.state = options.state === "opened" ? "opened" : "closed";
@@ -2721,7 +2791,9 @@ async function updateWorkItem(projectId, iid, options) {
2721
2791
  linked_items_added: options.linked_items_to_add?.length || 0,
2722
2792
  linked_items_removed: options.linked_items_to_remove?.length || 0,
2723
2793
  ...(options.severity !== undefined && { severity: options.severity }),
2724
- ...(options.escalation_status !== undefined && { escalation_status: options.escalation_status }),
2794
+ ...(options.escalation_status !== undefined && {
2795
+ escalation_status: options.escalation_status,
2796
+ }),
2725
2797
  };
2726
2798
  }
2727
2799
  /**
@@ -3023,9 +3095,7 @@ async function updateIssueNote(projectId, issueIid, discussionId, noteId, body,
3023
3095
  async function createIssueNote(projectId, issueIid, discussionId, body, createdAt) {
3024
3096
  projectId = decodeURIComponent(projectId); // Decode project ID
3025
3097
  const basePath = `${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/issues/${issueIid}`;
3026
- const url = new URL(discussionId
3027
- ? `${basePath}/discussions/${discussionId}/notes`
3028
- : `${basePath}/notes`);
3098
+ const url = new URL(discussionId ? `${basePath}/discussions/${discussionId}/notes` : `${basePath}/notes`);
3029
3099
  const payload = { body };
3030
3100
  if (createdAt) {
3031
3101
  payload.created_at = createdAt;
@@ -3392,6 +3462,7 @@ async function createRepository(options) {
3392
3462
  method: "POST",
3393
3463
  body: JSON.stringify({
3394
3464
  name: options.name,
3465
+ ...(options.namespace_id !== undefined ? { namespace_id: options.namespace_id } : {}),
3395
3466
  description: options.description,
3396
3467
  visibility: options.visibility,
3397
3468
  initialize_with_readme: options.initialize_with_readme,
@@ -5718,7 +5789,7 @@ async function getCurrentUser() {
5718
5789
  if ((response.status === 401 || response.status === 403) && usesJobTokenHeader()) {
5719
5790
  const jobResponse = await fetch(`${getEffectiveApiUrl()}/job`, getFetchConfig());
5720
5791
  if (jobResponse.ok) {
5721
- const jobData = await jobResponse.json();
5792
+ const jobData = (await jobResponse.json());
5722
5793
  if (jobData.user) {
5723
5794
  return GitLabUserSchema.parse(jobData.user);
5724
5795
  }
@@ -5742,7 +5813,8 @@ async function myIssues(options = {}) {
5742
5813
  effectiveProjectId = getEffectiveProjectId(options.project_id || "");
5743
5814
  }
5744
5815
  catch (err) {
5745
- if (err instanceof Error && err.message.includes("No project ID provided and GITLAB_PROJECT_ID is not set")) {
5816
+ if (err instanceof Error &&
5817
+ err.message.includes("No project ID provided and GITLAB_PROJECT_ID is not set")) {
5746
5818
  effectiveProjectId = "";
5747
5819
  }
5748
5820
  else {
@@ -5956,7 +6028,11 @@ async function updateGroupVariable(groupId, key, options) {
5956
6028
  if (filter?.environment_scope) {
5957
6029
  url.searchParams.append("filter[environment_scope]", filter.environment_scope);
5958
6030
  }
5959
- const response = await fetch(url.toString(), { ...getFetchConfig(), method: "PUT", body: JSON.stringify(body) });
6031
+ const response = await fetch(url.toString(), {
6032
+ ...getFetchConfig(),
6033
+ method: "PUT",
6034
+ body: JSON.stringify(body),
6035
+ });
5960
6036
  await handleGitLabError(response);
5961
6037
  const data = await response.json();
5962
6038
  return GitLabCiVariableSchema.parse(data);
@@ -6004,7 +6080,9 @@ async function getDependencyProxySettings(groupPath) {
6004
6080
  });
6005
6081
  }
6006
6082
  async function updateDependencyProxySettings(groupPath, options) {
6007
- if (options.enabled === undefined && options.identity === undefined && options.secret === undefined) {
6083
+ if (options.enabled === undefined &&
6084
+ options.identity === undefined &&
6085
+ options.secret === undefined) {
6008
6086
  throw new Error("At least one of enabled, identity, or secret must be provided");
6009
6087
  }
6010
6088
  const fullPath = await resolveGroupFullPath(groupPath);
@@ -6038,7 +6116,11 @@ async function listDependencyProxyBlobs(groupPath, options = {}) {
6038
6116
  if (!conn)
6039
6117
  throw new Error(`Group not found or dependency proxy not enabled: ${fullPath}`);
6040
6118
  return {
6041
- blobs: conn.nodes.map(n => GitLabDependencyProxyBlobSchema.parse({ file_name: n.fileName, size: n.size, created_at: n.createdAt })),
6119
+ blobs: conn.nodes.map(n => GitLabDependencyProxyBlobSchema.parse({
6120
+ file_name: n.fileName,
6121
+ size: n.size,
6122
+ created_at: n.createdAt,
6123
+ })),
6042
6124
  pageInfo: conn.pageInfo,
6043
6125
  };
6044
6126
  }
@@ -6098,7 +6180,7 @@ async function markdownUpload(projectId, filePath, content, filename) {
6098
6180
  const response = await fetch(url.toString(), {
6099
6181
  ...defaultFetchConfig,
6100
6182
  method: "POST",
6101
- body: form
6183
+ body: form,
6102
6184
  });
6103
6185
  if (!response.ok) {
6104
6186
  await handleGitLabError(response);
@@ -6419,6 +6501,34 @@ async function getTagSignature(projectId, tagName) {
6419
6501
  const data = await response.json();
6420
6502
  return GitLabTagSignatureSchema.parse(data);
6421
6503
  }
6504
+ async function executeGitLabGraphQL(query, variables = {}) {
6505
+ const apiUrl = new URL(getEffectiveApiUrl());
6506
+ const restPath = apiUrl.pathname || "";
6507
+ const idx = restPath.lastIndexOf("/api/v4");
6508
+ const prefix = idx >= 0 ? restPath.slice(0, idx) : "";
6509
+ const graphqlUrl = process.env.GITLAB_GRAPHQL_URL || `${apiUrl.origin}${prefix}/api/graphql`;
6510
+ const controller = new AbortController();
6511
+ const timeout = setTimeout(() => controller.abort(), 45000);
6512
+ try {
6513
+ const response = await fetch(graphqlUrl, {
6514
+ ...getFetchConfig(),
6515
+ method: "POST",
6516
+ headers: {
6517
+ ...BASE_HEADERS,
6518
+ ...buildAuthHeaders(),
6519
+ },
6520
+ body: JSON.stringify({ query, variables }),
6521
+ signal: controller.signal,
6522
+ });
6523
+ if (!response.ok) {
6524
+ await handleGitLabError(response);
6525
+ }
6526
+ return await response.json();
6527
+ }
6528
+ finally {
6529
+ clearTimeout(timeout);
6530
+ }
6531
+ }
6422
6532
  // Request handlers are now registered inside createServer() factory function
6423
6533
  // to ensure each transport connection gets its own Server instance (GHSA-345p-7cg4-v4c7).
6424
6534
  async function handleToolCall(params) {
@@ -6482,7 +6592,7 @@ async function handleToolCall(params) {
6482
6592
  }
6483
6593
  const json = await response.json();
6484
6594
  return {
6485
- content: [{ type: "text", text: JSON.stringify(json, null, 2) }],
6595
+ content: [{ type: "text", text: JSON.stringify(json) }],
6486
6596
  };
6487
6597
  }
6488
6598
  catch (err) {
@@ -6491,7 +6601,7 @@ async function handleToolCall(params) {
6491
6601
  content: [
6492
6602
  {
6493
6603
  type: "text",
6494
- text: JSON.stringify({ error: `GraphQL request failed: ${message}` }, null, 2),
6604
+ text: JSON.stringify({ error: `GraphQL request failed: ${message}` }),
6495
6605
  },
6496
6606
  ],
6497
6607
  };
@@ -6506,7 +6616,7 @@ async function handleToolCall(params) {
6506
6616
  try {
6507
6617
  const forkedProject = await forkProject(forkArgs.project_id, forkArgs.namespace);
6508
6618
  return {
6509
- content: [{ type: "text", text: JSON.stringify(forkedProject, null, 2) }],
6619
+ content: [{ type: "text", text: JSON.stringify(forkedProject) }],
6510
6620
  };
6511
6621
  }
6512
6622
  catch (forkError) {
@@ -6519,7 +6629,7 @@ async function handleToolCall(params) {
6519
6629
  content: [
6520
6630
  {
6521
6631
  type: "text",
6522
- text: JSON.stringify({ error: forkErrorMessage }, null, 2),
6632
+ text: JSON.stringify({ error: forkErrorMessage }),
6523
6633
  },
6524
6634
  ],
6525
6635
  };
@@ -6536,7 +6646,7 @@ async function handleToolCall(params) {
6536
6646
  ref,
6537
6647
  });
6538
6648
  return {
6539
- content: [{ type: "text", text: JSON.stringify(branch, null, 2) }],
6649
+ content: [{ type: "text", text: JSON.stringify(branch) }],
6540
6650
  };
6541
6651
  }
6542
6652
  case "get_branch_diffs": {
@@ -6544,14 +6654,14 @@ async function handleToolCall(params) {
6544
6654
  const diffResp = await getBranchDiffs(args.project_id, args.from, args.to, args.straight);
6545
6655
  diffResp.diffs = filterDiffsByPatterns(diffResp.diffs, args.excluded_file_patterns);
6546
6656
  return {
6547
- content: [{ type: "text", text: JSON.stringify(diffResp, null, 2) }],
6657
+ content: [{ type: "text", text: JSON.stringify(diffResp) }],
6548
6658
  };
6549
6659
  }
6550
6660
  case "search_repositories": {
6551
6661
  const args = SearchRepositoriesSchema.parse(params.arguments);
6552
6662
  const results = await searchProjects(args.search, args.page, args.per_page);
6553
6663
  return {
6554
- content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
6664
+ content: [{ type: "text", text: JSON.stringify(results) }],
6555
6665
  };
6556
6666
  }
6557
6667
  case "search_code": {
@@ -6565,7 +6675,7 @@ async function handleToolCall(params) {
6565
6675
  per_page: args.per_page,
6566
6676
  });
6567
6677
  return {
6568
- content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
6678
+ content: [{ type: "text", text: JSON.stringify(results) }],
6569
6679
  };
6570
6680
  }
6571
6681
  case "search_project_code": {
@@ -6581,7 +6691,7 @@ async function handleToolCall(params) {
6581
6691
  per_page: args.per_page,
6582
6692
  });
6583
6693
  return {
6584
- content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
6694
+ content: [{ type: "text", text: JSON.stringify(results) }],
6585
6695
  };
6586
6696
  }
6587
6697
  case "search_group_code": {
@@ -6596,7 +6706,7 @@ async function handleToolCall(params) {
6596
6706
  per_page: args.per_page,
6597
6707
  });
6598
6708
  return {
6599
- content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
6709
+ content: [{ type: "text", text: JSON.stringify(results) }],
6600
6710
  };
6601
6711
  }
6602
6712
  case "create_repository": {
@@ -6604,7 +6714,7 @@ async function handleToolCall(params) {
6604
6714
  const args = CreateRepositorySchema.parse(params.arguments);
6605
6715
  const repository = await createRepository(args);
6606
6716
  return {
6607
- content: [{ type: "text", text: JSON.stringify(repository, null, 2) }],
6717
+ content: [{ type: "text", text: JSON.stringify(repository) }],
6608
6718
  };
6609
6719
  }
6610
6720
  case "create_group": {
@@ -6631,28 +6741,28 @@ async function handleToolCall(params) {
6631
6741
  const data = await response.json();
6632
6742
  const group = GitLabGroupSchema.parse(data);
6633
6743
  return {
6634
- content: [{ type: "text", text: JSON.stringify(group, null, 2) }],
6744
+ content: [{ type: "text", text: JSON.stringify(group) }],
6635
6745
  };
6636
6746
  }
6637
6747
  case "get_file_contents": {
6638
6748
  const args = GetFileContentsSchema.parse(params.arguments);
6639
6749
  const contents = await getFileContents(args.project_id, args.file_path, args.ref);
6640
6750
  return {
6641
- content: [{ type: "text", text: JSON.stringify(contents, null, 2) }],
6751
+ content: [{ type: "text", text: JSON.stringify(contents) }],
6642
6752
  };
6643
6753
  }
6644
6754
  case "create_or_update_file": {
6645
6755
  const args = CreateOrUpdateFileSchema.parse(params.arguments);
6646
6756
  const result = await createOrUpdateFile(args.project_id, args.file_path, args.content, args.commit_message, args.branch, args.previous_path, args.last_commit_id, args.commit_id);
6647
6757
  return {
6648
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
6758
+ content: [{ type: "text", text: JSON.stringify(result) }],
6649
6759
  };
6650
6760
  }
6651
6761
  case "push_files": {
6652
6762
  const args = PushFilesSchema.parse(params.arguments);
6653
6763
  const result = await createCommit(args.project_id, args.commit_message, args.branch, args.files.map(f => ({ path: f.file_path, content: f.content })));
6654
6764
  return {
6655
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
6765
+ content: [{ type: "text", text: JSON.stringify(result) }],
6656
6766
  };
6657
6767
  }
6658
6768
  case "create_issue": {
@@ -6660,7 +6770,7 @@ async function handleToolCall(params) {
6660
6770
  const { project_id, ...options } = args;
6661
6771
  const issue = await createIssue(project_id, options);
6662
6772
  return {
6663
- content: [{ type: "text", text: JSON.stringify(issue, null, 2) }],
6773
+ content: [{ type: "text", text: JSON.stringify(issue) }],
6664
6774
  };
6665
6775
  }
6666
6776
  case "create_merge_request": {
@@ -6668,7 +6778,7 @@ async function handleToolCall(params) {
6668
6778
  const { project_id, ...options } = args;
6669
6779
  const mergeRequest = await createMergeRequest(project_id, options);
6670
6780
  return {
6671
- content: [{ type: "text", text: JSON.stringify(mergeRequest, null, 2) }],
6781
+ content: [{ type: "text", text: JSON.stringify(mergeRequest) }],
6672
6782
  };
6673
6783
  }
6674
6784
  case "delete_merge_request_discussion_note": {
@@ -6685,21 +6795,21 @@ async function handleToolCall(params) {
6685
6795
  args.resolved // Now one of body or resolved must be provided, not both
6686
6796
  );
6687
6797
  return {
6688
- content: [{ type: "text", text: JSON.stringify(note, null, 2) }],
6798
+ content: [{ type: "text", text: JSON.stringify(note) }],
6689
6799
  };
6690
6800
  }
6691
6801
  case "create_merge_request_discussion_note": {
6692
6802
  const args = CreateMergeRequestDiscussionNoteSchema.parse(params.arguments);
6693
6803
  const note = await createMergeRequestDiscussionNote(args.project_id, args.merge_request_iid, args.discussion_id, args.body, args.created_at);
6694
6804
  return {
6695
- content: [{ type: "text", text: JSON.stringify(note, null, 2) }],
6805
+ content: [{ type: "text", text: JSON.stringify(note) }],
6696
6806
  };
6697
6807
  }
6698
6808
  case "create_merge_request_note": {
6699
6809
  const args = CreateMergeRequestNoteSchema.parse(params.arguments);
6700
6810
  const note = await createMergeRequestNote(args.project_id, args.merge_request_iid, args.body);
6701
6811
  return {
6702
- content: [{ type: "text", text: JSON.stringify(note, null, 2) }],
6812
+ content: [{ type: "text", text: JSON.stringify(note) }],
6703
6813
  };
6704
6814
  }
6705
6815
  case "delete_merge_request_note": {
@@ -6713,121 +6823,141 @@ async function handleToolCall(params) {
6713
6823
  const args = GetMergeRequestNoteSchema.parse(params.arguments);
6714
6824
  const note = await getMergeRequestNote(args.project_id, args.merge_request_iid, args.note_id);
6715
6825
  return {
6716
- content: [{ type: "text", text: JSON.stringify(note, null, 2) }],
6826
+ content: [{ type: "text", text: JSON.stringify(note) }],
6717
6827
  };
6718
6828
  }
6719
6829
  case "get_merge_request_notes": {
6720
6830
  const args = GetMergeRequestNotesSchema.parse(params.arguments);
6721
6831
  const notes = await getMergeRequestNotes(args.project_id, args.merge_request_iid, args.sort, args.order_by, args.per_page, args.page);
6722
6832
  return {
6723
- content: [{ type: "text", text: JSON.stringify(notes, null, 2) }],
6833
+ content: [{ type: "text", text: JSON.stringify(notes) }],
6724
6834
  };
6725
6835
  }
6726
6836
  case "update_merge_request_note": {
6727
6837
  const args = UpdateMergeRequestNoteSchema.parse(params.arguments);
6728
6838
  const note = await updateMergeRequestNote(args.project_id, args.merge_request_iid, args.note_id, args.body);
6729
6839
  return {
6730
- content: [{ type: "text", text: JSON.stringify(note, null, 2) }],
6840
+ content: [{ type: "text", text: JSON.stringify(note) }],
6731
6841
  };
6732
6842
  }
6733
6843
  case "list_merge_request_emoji_reactions": {
6734
6844
  const args = ListMergeRequestEmojiReactionsSchema.parse(params.arguments);
6735
6845
  const path = buildAwardEmojiPath("merge_requests", args.project_id, args.merge_request_iid);
6736
6846
  const result = await listRestAwardEmoji(path);
6737
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
6847
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
6738
6848
  }
6739
6849
  case "list_merge_request_note_emoji_reactions": {
6740
6850
  const args = ListMergeRequestNoteEmojiReactionsSchema.parse(params.arguments);
6741
6851
  const path = buildAwardEmojiPath("merge_requests", args.project_id, args.merge_request_iid, { noteId: args.note_id, discussionId: args.discussion_id });
6742
6852
  const result = await listRestAwardEmoji(path);
6743
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
6853
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
6744
6854
  }
6745
6855
  case "create_merge_request_emoji_reaction": {
6746
6856
  const args = CreateMergeRequestEmojiReactionSchema.parse(params.arguments);
6747
6857
  const path = buildAwardEmojiPath("merge_requests", args.project_id, args.merge_request_iid);
6748
6858
  const result = await createRestAwardEmoji(path, args.name);
6749
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
6859
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
6750
6860
  }
6751
6861
  case "delete_merge_request_emoji_reaction": {
6752
6862
  const args = DeleteMergeRequestEmojiReactionSchema.parse(params.arguments);
6753
6863
  const path = buildAwardEmojiPath("merge_requests", args.project_id, args.merge_request_iid, { awardId: args.award_id });
6754
6864
  await deleteRestAwardEmoji(path);
6755
- return { content: [{ type: "text", text: "Merge request emoji reaction deleted successfully" }] };
6865
+ return {
6866
+ content: [{ type: "text", text: "Merge request emoji reaction deleted successfully" }],
6867
+ };
6756
6868
  }
6757
6869
  case "create_merge_request_note_emoji_reaction": {
6758
6870
  const args = CreateMergeRequestNoteEmojiReactionSchema.parse(params.arguments);
6759
6871
  const path = buildAwardEmojiPath("merge_requests", args.project_id, args.merge_request_iid, { noteId: args.note_id, discussionId: args.discussion_id });
6760
6872
  const result = await createRestAwardEmoji(path, args.name);
6761
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
6873
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
6762
6874
  }
6763
6875
  case "delete_merge_request_note_emoji_reaction": {
6764
6876
  const args = DeleteMergeRequestNoteEmojiReactionSchema.parse(params.arguments);
6765
6877
  const path = buildAwardEmojiPath("merge_requests", args.project_id, args.merge_request_iid, { noteId: args.note_id, discussionId: args.discussion_id, awardId: args.award_id });
6766
6878
  await deleteRestAwardEmoji(path);
6767
- return { content: [{ type: "text", text: "Merge request note emoji reaction deleted successfully" }] };
6879
+ return {
6880
+ content: [
6881
+ { type: "text", text: "Merge request note emoji reaction deleted successfully" },
6882
+ ],
6883
+ };
6768
6884
  }
6769
6885
  case "update_issue_note": {
6770
6886
  const args = UpdateIssueNoteSchema.parse(params.arguments);
6771
6887
  const note = await updateIssueNote(args.project_id, args.issue_iid, args.discussion_id, args.note_id, args.body, args.resolved);
6772
6888
  return {
6773
- content: [{ type: "text", text: JSON.stringify(note, null, 2) }],
6889
+ content: [{ type: "text", text: JSON.stringify(note) }],
6774
6890
  };
6775
6891
  }
6776
6892
  case "create_issue_note": {
6777
6893
  const args = CreateIssueNoteSchema.parse(params.arguments);
6778
6894
  const note = await createIssueNote(args.project_id, args.issue_iid, args.discussion_id, args.body, args.created_at);
6779
6895
  return {
6780
- content: [{ type: "text", text: JSON.stringify(note, null, 2) }],
6896
+ content: [{ type: "text", text: JSON.stringify(note) }],
6781
6897
  };
6782
6898
  }
6783
6899
  case "list_issue_emoji_reactions": {
6784
6900
  const args = ListIssueEmojiReactionsSchema.parse(params.arguments);
6785
6901
  const path = buildAwardEmojiPath("issues", args.project_id, args.issue_iid);
6786
6902
  const result = await listRestAwardEmoji(path);
6787
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
6903
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
6788
6904
  }
6789
6905
  case "list_issue_note_emoji_reactions": {
6790
6906
  const args = ListIssueNoteEmojiReactionsSchema.parse(params.arguments);
6791
- const path = buildAwardEmojiPath("issues", args.project_id, args.issue_iid, { noteId: args.note_id, discussionId: args.discussion_id });
6907
+ const path = buildAwardEmojiPath("issues", args.project_id, args.issue_iid, {
6908
+ noteId: args.note_id,
6909
+ discussionId: args.discussion_id,
6910
+ });
6792
6911
  const result = await listRestAwardEmoji(path);
6793
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
6912
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
6794
6913
  }
6795
6914
  case "create_issue_emoji_reaction": {
6796
6915
  const args = CreateIssueEmojiReactionSchema.parse(params.arguments);
6797
6916
  const path = buildAwardEmojiPath("issues", args.project_id, args.issue_iid);
6798
6917
  const result = await createRestAwardEmoji(path, args.name);
6799
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
6918
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
6800
6919
  }
6801
6920
  case "delete_issue_emoji_reaction": {
6802
6921
  const args = DeleteIssueEmojiReactionSchema.parse(params.arguments);
6803
- const path = buildAwardEmojiPath("issues", args.project_id, args.issue_iid, { awardId: args.award_id });
6922
+ const path = buildAwardEmojiPath("issues", args.project_id, args.issue_iid, {
6923
+ awardId: args.award_id,
6924
+ });
6804
6925
  await deleteRestAwardEmoji(path);
6805
6926
  return { content: [{ type: "text", text: "Issue emoji reaction deleted successfully" }] };
6806
6927
  }
6807
6928
  case "create_issue_note_emoji_reaction": {
6808
6929
  const args = CreateIssueNoteEmojiReactionSchema.parse(params.arguments);
6809
- const path = buildAwardEmojiPath("issues", args.project_id, args.issue_iid, { noteId: args.note_id, discussionId: args.discussion_id });
6930
+ const path = buildAwardEmojiPath("issues", args.project_id, args.issue_iid, {
6931
+ noteId: args.note_id,
6932
+ discussionId: args.discussion_id,
6933
+ });
6810
6934
  const result = await createRestAwardEmoji(path, args.name);
6811
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
6935
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
6812
6936
  }
6813
6937
  case "delete_issue_note_emoji_reaction": {
6814
6938
  const args = DeleteIssueNoteEmojiReactionSchema.parse(params.arguments);
6815
- const path = buildAwardEmojiPath("issues", args.project_id, args.issue_iid, { noteId: args.note_id, discussionId: args.discussion_id, awardId: args.award_id });
6939
+ const path = buildAwardEmojiPath("issues", args.project_id, args.issue_iid, {
6940
+ noteId: args.note_id,
6941
+ discussionId: args.discussion_id,
6942
+ awardId: args.award_id,
6943
+ });
6816
6944
  await deleteRestAwardEmoji(path);
6817
- return { content: [{ type: "text", text: "Issue note emoji reaction deleted successfully" }] };
6945
+ return {
6946
+ content: [{ type: "text", text: "Issue note emoji reaction deleted successfully" }],
6947
+ };
6818
6948
  }
6819
6949
  case "list_todos": {
6820
6950
  const args = ListTodosSchema.parse(params.arguments);
6821
6951
  const todos = await listTodos(args);
6822
6952
  return {
6823
- content: [{ type: "text", text: JSON.stringify(todos, null, 2) }],
6953
+ content: [{ type: "text", text: JSON.stringify(todos) }],
6824
6954
  };
6825
6955
  }
6826
6956
  case "mark_todo_done": {
6827
6957
  const args = MarkTodoDoneSchema.parse(params.arguments);
6828
6958
  const todo = await markTodoDone(args.id);
6829
6959
  return {
6830
- content: [{ type: "text", text: JSON.stringify(todo, null, 2) }],
6960
+ content: [{ type: "text", text: JSON.stringify(todo) }],
6831
6961
  };
6832
6962
  }
6833
6963
  case "mark_all_todos_done": {
@@ -6861,7 +6991,7 @@ async function handleToolCall(params) {
6861
6991
  content: [
6862
6992
  {
6863
6993
  type: "text",
6864
- text: JSON.stringify(mergeRequestWithDeploymentSummary, null, 2),
6994
+ text: JSON.stringify(mergeRequestWithDeploymentSummary),
6865
6995
  },
6866
6996
  ],
6867
6997
  };
@@ -6871,14 +7001,14 @@ async function handleToolCall(params) {
6871
7001
  const diffs = await getMergeRequestDiffs(args.project_id, args.merge_request_iid, args.source_branch, args.view);
6872
7002
  const filteredDiffs = filterDiffsByPatterns(diffs, args.excluded_file_patterns);
6873
7003
  return {
6874
- content: [{ type: "text", text: JSON.stringify(filteredDiffs, null, 2) }],
7004
+ content: [{ type: "text", text: JSON.stringify(filteredDiffs) }],
6875
7005
  };
6876
7006
  }
6877
7007
  case "list_merge_request_changed_files": {
6878
7008
  const args = ListMergeRequestChangedFilesSchema.parse(params.arguments);
6879
7009
  const files = await listMergeRequestChangedFiles(args.project_id, args.merge_request_iid, args.source_branch, args.excluded_file_patterns);
6880
7010
  return {
6881
- content: [{ type: "text", text: JSON.stringify(files, null, 2) }],
7011
+ content: [{ type: "text", text: JSON.stringify(files) }],
6882
7012
  };
6883
7013
  }
6884
7014
  case "list_merge_request_pipelines": {
@@ -6886,35 +7016,35 @@ async function handleToolCall(params) {
6886
7016
  const { project_id, merge_request_iid, ...options } = args;
6887
7017
  const pipelines = await listMergeRequestPipelines(project_id, merge_request_iid, options);
6888
7018
  return {
6889
- content: [{ type: "text", text: JSON.stringify(pipelines, null, 2) }],
7019
+ content: [{ type: "text", text: JSON.stringify(pipelines) }],
6890
7020
  };
6891
7021
  }
6892
7022
  case "list_merge_request_diffs": {
6893
7023
  const args = ListMergeRequestDiffsSchema.parse(params.arguments);
6894
7024
  const changes = await listMergeRequestDiffs(args.project_id, args.merge_request_iid, args.source_branch, args.page, args.per_page, args.unidiff);
6895
7025
  return {
6896
- content: [{ type: "text", text: JSON.stringify(changes, null, 2) }],
7026
+ content: [{ type: "text", text: JSON.stringify(changes) }],
6897
7027
  };
6898
7028
  }
6899
7029
  case "get_merge_request_file_diff": {
6900
7030
  const args = GetMergeRequestFileDiffSchema.parse(params.arguments);
6901
7031
  const fileDiff = await getMergeRequestFileDiff(args.project_id, args.file_paths, args.merge_request_iid, args.source_branch, args.unidiff);
6902
7032
  return {
6903
- content: [{ type: "text", text: JSON.stringify(fileDiff, null, 2) }],
7033
+ content: [{ type: "text", text: JSON.stringify(fileDiff) }],
6904
7034
  };
6905
7035
  }
6906
7036
  case "list_merge_request_versions": {
6907
7037
  const args = ListMergeRequestVersionsSchema.parse(params.arguments);
6908
7038
  const versions = await listMergeRequestVersions(args.project_id, args.merge_request_iid);
6909
7039
  return {
6910
- content: [{ type: "text", text: JSON.stringify(versions, null, 2) }],
7040
+ content: [{ type: "text", text: JSON.stringify(versions) }],
6911
7041
  };
6912
7042
  }
6913
7043
  case "get_merge_request_version": {
6914
7044
  const args = GetMergeRequestVersionSchema.parse(params.arguments);
6915
7045
  const version = await getMergeRequestVersion(args.project_id, args.merge_request_iid, args.version_id, args.unidiff);
6916
7046
  return {
6917
- content: [{ type: "text", text: JSON.stringify(version, null, 2) }],
7047
+ content: [{ type: "text", text: JSON.stringify(version) }],
6918
7048
  };
6919
7049
  }
6920
7050
  case "update_merge_request": {
@@ -6922,7 +7052,7 @@ async function handleToolCall(params) {
6922
7052
  const { project_id, merge_request_iid, source_branch, ...options } = args;
6923
7053
  const mergeRequest = await updateMergeRequest(project_id, options, merge_request_iid, source_branch);
6924
7054
  return {
6925
- content: [{ type: "text", text: JSON.stringify(mergeRequest, null, 2) }],
7055
+ content: [{ type: "text", text: JSON.stringify(mergeRequest) }],
6926
7056
  };
6927
7057
  }
6928
7058
  case "merge_merge_request": {
@@ -6930,35 +7060,35 @@ async function handleToolCall(params) {
6930
7060
  const { project_id, merge_request_iid, ...options } = args;
6931
7061
  const mergeRequest = await mergeMergeRequest(project_id, options, merge_request_iid);
6932
7062
  return {
6933
- content: [{ type: "text", text: JSON.stringify(mergeRequest, null, 2) }],
7063
+ content: [{ type: "text", text: JSON.stringify(mergeRequest) }],
6934
7064
  };
6935
7065
  }
6936
7066
  case "approve_merge_request": {
6937
7067
  const args = ApproveMergeRequestSchema.parse(params.arguments);
6938
7068
  const approvalState = await approveMergeRequest(args.project_id, args.merge_request_iid, args.sha, args.approval_password);
6939
7069
  return {
6940
- content: [{ type: "text", text: JSON.stringify(approvalState, null, 2) }],
7070
+ content: [{ type: "text", text: JSON.stringify(approvalState) }],
6941
7071
  };
6942
7072
  }
6943
7073
  case "unapprove_merge_request": {
6944
7074
  const args = UnapproveMergeRequestSchema.parse(params.arguments);
6945
7075
  const approvalState = await unapproveMergeRequest(args.project_id, args.merge_request_iid);
6946
7076
  return {
6947
- content: [{ type: "text", text: JSON.stringify(approvalState, null, 2) }],
7077
+ content: [{ type: "text", text: JSON.stringify(approvalState) }],
6948
7078
  };
6949
7079
  }
6950
7080
  case "get_merge_request_approval_state": {
6951
7081
  const args = GetMergeRequestApprovalStateSchema.parse(params.arguments);
6952
7082
  const approvalState = await getMergeRequestApprovalState(args.project_id, args.merge_request_iid);
6953
7083
  return {
6954
- content: [{ type: "text", text: JSON.stringify(approvalState, null, 2) }],
7084
+ content: [{ type: "text", text: JSON.stringify(approvalState) }],
6955
7085
  };
6956
7086
  }
6957
7087
  case "get_merge_request_conflicts": {
6958
7088
  const args = GetMergeRequestConflictsSchema.parse(params.arguments);
6959
7089
  const conflicts = await getMergeRequestConflicts(args.project_id, args.merge_request_iid);
6960
7090
  return {
6961
- content: [{ type: "text", text: JSON.stringify(conflicts, null, 2) }],
7091
+ content: [{ type: "text", text: JSON.stringify(conflicts) }],
6962
7092
  };
6963
7093
  }
6964
7094
  case "mr_discussions": {
@@ -6966,7 +7096,7 @@ async function handleToolCall(params) {
6966
7096
  const { project_id, merge_request_iid, ...options } = args;
6967
7097
  const discussions = await listMergeRequestDiscussions(project_id, merge_request_iid, options);
6968
7098
  return {
6969
- content: [{ type: "text", text: JSON.stringify(discussions, null, 2) }],
7099
+ content: [{ type: "text", text: JSON.stringify(discussions) }],
6970
7100
  };
6971
7101
  }
6972
7102
  case "list_namespaces": {
@@ -6991,7 +7121,7 @@ async function handleToolCall(params) {
6991
7121
  const data = await response.json();
6992
7122
  const namespaces = z.array(GitLabNamespaceSchema).parse(data);
6993
7123
  return {
6994
- content: [{ type: "text", text: JSON.stringify(namespaces, null, 2) }],
7124
+ content: [{ type: "text", text: JSON.stringify(namespaces) }],
6995
7125
  };
6996
7126
  }
6997
7127
  case "get_namespace": {
@@ -7004,7 +7134,7 @@ async function handleToolCall(params) {
7004
7134
  const data = await response.json();
7005
7135
  const namespace = GitLabNamespaceSchema.parse(data);
7006
7136
  return {
7007
- content: [{ type: "text", text: JSON.stringify(namespace, null, 2) }],
7137
+ content: [{ type: "text", text: JSON.stringify(namespace) }],
7008
7138
  };
7009
7139
  }
7010
7140
  case "verify_namespace": {
@@ -7019,7 +7149,7 @@ async function handleToolCall(params) {
7019
7149
  const data = await response.json();
7020
7150
  const namespaceExists = GitLabNamespaceExistsResponseSchema.parse(data);
7021
7151
  return {
7022
- content: [{ type: "text", text: JSON.stringify(namespaceExists, null, 2) }],
7152
+ content: [{ type: "text", text: JSON.stringify(namespaceExists) }],
7023
7153
  };
7024
7154
  }
7025
7155
  case "get_project": {
@@ -7040,14 +7170,29 @@ async function handleToolCall(params) {
7040
7170
  const data = await response.json();
7041
7171
  // Return raw data without parsing through our schema to avoid type mismatches in tests
7042
7172
  return {
7043
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
7173
+ content: [{ type: "text", text: JSON.stringify(data) }],
7044
7174
  };
7045
7175
  }
7046
7176
  case "list_projects": {
7047
7177
  const args = ListProjectsSchema.parse(params.arguments);
7048
7178
  const projects = await listProjects(args);
7049
7179
  return {
7050
- content: [{ type: "text", text: JSON.stringify(projects, null, 2) }],
7180
+ content: [{ type: "text", text: JSON.stringify(projects) }],
7181
+ };
7182
+ }
7183
+ case "update_project": {
7184
+ const { project_id, ...updates } = UpdateProjectSchema.parse(params.arguments);
7185
+ const effectiveProjectId = getEffectiveProjectId(project_id);
7186
+ const body = Object.fromEntries(Object.entries(updates).filter(([, value]) => value !== undefined));
7187
+ const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}`, {
7188
+ ...getFetchConfig(),
7189
+ method: "PUT",
7190
+ body: JSON.stringify(body),
7191
+ });
7192
+ await handleGitLabError(response);
7193
+ const data = await response.json();
7194
+ return {
7195
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
7051
7196
  };
7052
7197
  }
7053
7198
  case "list_project_members": {
@@ -7055,14 +7200,14 @@ async function handleToolCall(params) {
7055
7200
  const { project_id, ...options } = args;
7056
7201
  const members = await listProjectMembers(project_id, options);
7057
7202
  return {
7058
- content: [{ type: "text", text: JSON.stringify(members, null, 2) }],
7203
+ content: [{ type: "text", text: JSON.stringify(members) }],
7059
7204
  };
7060
7205
  }
7061
7206
  case "get_users": {
7062
7207
  const args = GetUsersSchema.parse(params.arguments);
7063
7208
  const usersMap = await getUsers(args.usernames);
7064
7209
  return {
7065
- content: [{ type: "text", text: JSON.stringify(usersMap, null, 2) }],
7210
+ content: [{ type: "text", text: JSON.stringify(usersMap) }],
7066
7211
  };
7067
7212
  }
7068
7213
  case "get_user": {
@@ -7075,7 +7220,7 @@ async function handleToolCall(params) {
7075
7220
  const data = await response.json();
7076
7221
  const user = GitLabUserFullSchema.parse(data);
7077
7222
  return {
7078
- content: [{ type: "text", text: JSON.stringify(user, null, 2) }],
7223
+ content: [{ type: "text", text: JSON.stringify(user) }],
7079
7224
  };
7080
7225
  }
7081
7226
  case "whoami": {
@@ -7088,7 +7233,7 @@ async function handleToolCall(params) {
7088
7233
  const data = await response.json();
7089
7234
  const user = GitLabCurrentUserSchema.parse(data);
7090
7235
  return {
7091
- content: [{ type: "text", text: JSON.stringify(user, null, 2) }],
7236
+ content: [{ type: "text", text: JSON.stringify(user) }],
7092
7237
  };
7093
7238
  }
7094
7239
  case "create_note": {
@@ -7096,7 +7241,7 @@ async function handleToolCall(params) {
7096
7241
  const { project_id, noteable_type, noteable_iid, body } = args;
7097
7242
  const note = await createNote(project_id, noteable_type, noteable_iid, body);
7098
7243
  return {
7099
- content: [{ type: "text", text: JSON.stringify(note, null, 2) }],
7244
+ content: [{ type: "text", text: JSON.stringify(note) }],
7100
7245
  };
7101
7246
  }
7102
7247
  case "get_draft_note": {
@@ -7104,7 +7249,7 @@ async function handleToolCall(params) {
7104
7249
  const { project_id, merge_request_iid, draft_note_id } = args;
7105
7250
  const draftNote = await getDraftNote(project_id, merge_request_iid, draft_note_id);
7106
7251
  return {
7107
- content: [{ type: "text", text: JSON.stringify(draftNote, null, 2) }],
7252
+ content: [{ type: "text", text: JSON.stringify(draftNote) }],
7108
7253
  };
7109
7254
  }
7110
7255
  case "list_draft_notes": {
@@ -7112,15 +7257,15 @@ async function handleToolCall(params) {
7112
7257
  const { project_id, merge_request_iid } = args;
7113
7258
  const draftNotes = await listDraftNotes(project_id, merge_request_iid);
7114
7259
  return {
7115
- content: [{ type: "text", text: JSON.stringify(draftNotes, null, 2) }],
7260
+ content: [{ type: "text", text: JSON.stringify(draftNotes) }],
7116
7261
  };
7117
7262
  }
7118
7263
  case "create_draft_note": {
7119
7264
  const args = CreateDraftNoteSchema.parse(params.arguments);
7120
- const { project_id, merge_request_iid, body, in_reply_to_discussion_id, position, resolve_discussion } = args;
7265
+ const { project_id, merge_request_iid, body, in_reply_to_discussion_id, position, resolve_discussion, } = args;
7121
7266
  const draftNote = await createDraftNote(project_id, merge_request_iid, body, in_reply_to_discussion_id, position, resolve_discussion);
7122
7267
  return {
7123
- content: [{ type: "text", text: JSON.stringify(draftNote, null, 2) }],
7268
+ content: [{ type: "text", text: JSON.stringify(draftNote) }],
7124
7269
  };
7125
7270
  }
7126
7271
  case "update_draft_note": {
@@ -7128,7 +7273,7 @@ async function handleToolCall(params) {
7128
7273
  const { project_id, merge_request_iid, draft_note_id, body, position, resolve_discussion } = args;
7129
7274
  const draftNote = await updateDraftNote(project_id, merge_request_iid, draft_note_id, body, position, resolve_discussion);
7130
7275
  return {
7131
- content: [{ type: "text", text: JSON.stringify(draftNote, null, 2) }],
7276
+ content: [{ type: "text", text: JSON.stringify(draftNote) }],
7132
7277
  };
7133
7278
  }
7134
7279
  case "delete_draft_note": {
@@ -7144,7 +7289,7 @@ async function handleToolCall(params) {
7144
7289
  const { project_id, merge_request_iid, draft_note_id } = args;
7145
7290
  const publishedNote = await publishDraftNote(project_id, merge_request_iid, draft_note_id);
7146
7291
  return {
7147
- content: [{ type: "text", text: JSON.stringify(publishedNote, null, 2) }],
7292
+ content: [{ type: "text", text: JSON.stringify(publishedNote) }],
7148
7293
  };
7149
7294
  }
7150
7295
  case "bulk_publish_draft_notes": {
@@ -7152,7 +7297,7 @@ async function handleToolCall(params) {
7152
7297
  const { project_id, merge_request_iid } = args;
7153
7298
  const publishedNotes = await bulkPublishDraftNotes(project_id, merge_request_iid);
7154
7299
  return {
7155
- content: [{ type: "text", text: JSON.stringify(publishedNotes, null, 2) }],
7300
+ content: [{ type: "text", text: JSON.stringify(publishedNotes) }],
7156
7301
  };
7157
7302
  }
7158
7303
  case "create_merge_request_thread": {
@@ -7160,7 +7305,7 @@ async function handleToolCall(params) {
7160
7305
  const { project_id, merge_request_iid, body, position, created_at } = args;
7161
7306
  const thread = await createMergeRequestThread(project_id, merge_request_iid, body, position, created_at);
7162
7307
  return {
7163
- content: [{ type: "text", text: JSON.stringify(thread, null, 2) }],
7308
+ content: [{ type: "text", text: JSON.stringify(thread) }],
7164
7309
  };
7165
7310
  }
7166
7311
  case "resolve_merge_request_thread": {
@@ -7177,21 +7322,21 @@ async function handleToolCall(params) {
7177
7322
  const cleanedOptions = cleanMutuallyExclusiveIdUsernameOptions(options);
7178
7323
  const issues = await listIssues(project_id, cleanedOptions);
7179
7324
  return {
7180
- content: [{ type: "text", text: JSON.stringify(issues, null, 2) }],
7325
+ content: [{ type: "text", text: JSON.stringify(issues) }],
7181
7326
  };
7182
7327
  }
7183
7328
  case "my_issues": {
7184
7329
  const args = MyIssuesSchema.parse(params.arguments);
7185
7330
  const issues = await myIssues(args);
7186
7331
  return {
7187
- content: [{ type: "text", text: JSON.stringify(issues, null, 2) }],
7332
+ content: [{ type: "text", text: JSON.stringify(issues) }],
7188
7333
  };
7189
7334
  }
7190
7335
  case "get_issue": {
7191
7336
  const args = GetIssueSchema.parse(params.arguments);
7192
7337
  const issue = await getIssue(args.project_id, args.issue_iid);
7193
7338
  return {
7194
- content: [{ type: "text", text: JSON.stringify(issue, null, 2) }],
7339
+ content: [{ type: "text", text: JSON.stringify(issue) }],
7195
7340
  };
7196
7341
  }
7197
7342
  case "update_issue": {
@@ -7199,7 +7344,7 @@ async function handleToolCall(params) {
7199
7344
  const { project_id, issue_iid, ...options } = args;
7200
7345
  const issue = await updateIssue(project_id, issue_iid, options);
7201
7346
  return {
7202
- content: [{ type: "text", text: JSON.stringify(issue, null, 2) }],
7347
+ content: [{ type: "text", text: JSON.stringify(issue) }],
7203
7348
  };
7204
7349
  }
7205
7350
  case "update_issue_description_patch": {
@@ -7293,7 +7438,7 @@ async function handleToolCall(params) {
7293
7438
  const args = ListIssueLinksSchema.parse(params.arguments);
7294
7439
  const links = await listIssueLinks(args.project_id, args.issue_iid);
7295
7440
  return {
7296
- content: [{ type: "text", text: JSON.stringify(links, null, 2) }],
7441
+ content: [{ type: "text", text: JSON.stringify(links) }],
7297
7442
  };
7298
7443
  }
7299
7444
  case "list_issue_discussions": {
@@ -7301,21 +7446,21 @@ async function handleToolCall(params) {
7301
7446
  const { project_id, issue_iid, ...options } = args;
7302
7447
  const discussions = await listIssueDiscussions(project_id, issue_iid, options);
7303
7448
  return {
7304
- content: [{ type: "text", text: JSON.stringify(discussions, null, 2) }],
7449
+ content: [{ type: "text", text: JSON.stringify(discussions) }],
7305
7450
  };
7306
7451
  }
7307
7452
  case "get_issue_link": {
7308
7453
  const args = GetIssueLinkSchema.parse(params.arguments);
7309
7454
  const link = await getIssueLink(args.project_id, args.issue_iid, args.issue_link_id);
7310
7455
  return {
7311
- content: [{ type: "text", text: JSON.stringify(link, null, 2) }],
7456
+ content: [{ type: "text", text: JSON.stringify(link) }],
7312
7457
  };
7313
7458
  }
7314
7459
  case "create_issue_link": {
7315
7460
  const args = CreateIssueLinkSchema.parse(params.arguments);
7316
7461
  const link = await createIssueLink(args.project_id, args.issue_iid, args.target_project_id, args.target_issue_iid, args.link_type);
7317
7462
  return {
7318
- content: [{ type: "text", text: JSON.stringify(link, null, 2) }],
7463
+ content: [{ type: "text", text: JSON.stringify(link) }],
7319
7464
  };
7320
7465
  }
7321
7466
  case "delete_issue_link": {
@@ -7337,7 +7482,7 @@ async function handleToolCall(params) {
7337
7482
  const args = GetWorkItemSchema.parse(params.arguments);
7338
7483
  const result = await getWorkItem(args.project_id, args.iid);
7339
7484
  return {
7340
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
7485
+ content: [{ type: "text", text: JSON.stringify(result) }],
7341
7486
  };
7342
7487
  }
7343
7488
  case "list_work_items": {
@@ -7345,7 +7490,7 @@ async function handleToolCall(params) {
7345
7490
  const { project_id, ...options } = args;
7346
7491
  const result = await listWorkItems(project_id, options);
7347
7492
  return {
7348
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
7493
+ content: [{ type: "text", text: JSON.stringify(result) }],
7349
7494
  };
7350
7495
  }
7351
7496
  case "create_work_item": {
@@ -7353,7 +7498,7 @@ async function handleToolCall(params) {
7353
7498
  const { project_id, ...options } = args;
7354
7499
  const result = await createWorkItem(project_id, options);
7355
7500
  return {
7356
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
7501
+ content: [{ type: "text", text: JSON.stringify(result) }],
7357
7502
  };
7358
7503
  }
7359
7504
  case "update_work_item": {
@@ -7361,117 +7506,117 @@ async function handleToolCall(params) {
7361
7506
  const { project_id, iid, ...options } = args;
7362
7507
  const result = await updateWorkItem(project_id, iid, options);
7363
7508
  return {
7364
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
7509
+ content: [{ type: "text", text: JSON.stringify(result) }],
7365
7510
  };
7366
7511
  }
7367
7512
  case "convert_work_item_type": {
7368
7513
  const args = ConvertWorkItemTypeSchema.parse(params.arguments);
7369
7514
  const result = await convertIssueType(args.project_id, args.iid, args.new_type);
7370
7515
  return {
7371
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
7516
+ content: [{ type: "text", text: JSON.stringify(result) }],
7372
7517
  };
7373
7518
  }
7374
7519
  case "list_work_item_statuses": {
7375
7520
  const args = ListWorkItemStatusesSchema.parse(params.arguments);
7376
7521
  const result = await listIssueStatuses(args.project_id, args.work_item_type);
7377
7522
  return {
7378
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
7523
+ content: [{ type: "text", text: JSON.stringify(result) }],
7379
7524
  };
7380
7525
  }
7381
7526
  case "list_custom_field_definitions": {
7382
7527
  const args = ListCustomFieldDefinitionsSchema.parse(params.arguments);
7383
7528
  const result = await listCustomFieldDefinitions(args.project_id, args.work_item_type);
7384
7529
  return {
7385
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
7530
+ content: [{ type: "text", text: JSON.stringify(result) }],
7386
7531
  };
7387
7532
  }
7388
7533
  case "move_work_item": {
7389
7534
  const args = MoveWorkItemSchema.parse(params.arguments);
7390
7535
  const result = await moveWorkItem(args.project_id, args.iid, args.target_project_id);
7391
7536
  return {
7392
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
7537
+ content: [{ type: "text", text: JSON.stringify(result) }],
7393
7538
  };
7394
7539
  }
7395
7540
  case "list_work_item_notes": {
7396
7541
  const args = ListWorkItemNotesSchema.parse(params.arguments);
7397
7542
  const result = await listWorkItemNotes(args.project_id, args.iid, args);
7398
7543
  return {
7399
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
7544
+ content: [{ type: "text", text: JSON.stringify(result) }],
7400
7545
  };
7401
7546
  }
7402
7547
  case "create_work_item_note": {
7403
7548
  const args = CreateWorkItemNoteSchema.parse(params.arguments);
7404
7549
  const result = await createWorkItemNote(args.project_id, args.iid, args.body, args);
7405
7550
  return {
7406
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
7551
+ content: [{ type: "text", text: JSON.stringify(result) }],
7407
7552
  };
7408
7553
  }
7409
7554
  case "list_work_item_emoji_reactions": {
7410
7555
  const args = ListWorkItemEmojiReactionsSchema.parse(params.arguments);
7411
7556
  const { workItemGID } = await resolveWorkItemGID(args.project_id, args.iid);
7412
7557
  const result = await listGraphQLAwardEmoji(workItemGID);
7413
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
7558
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
7414
7559
  }
7415
7560
  case "list_work_item_note_emoji_reactions": {
7416
7561
  const args = ListWorkItemNoteEmojiReactionsSchema.parse(params.arguments);
7417
7562
  const result = await listGraphQLAwardEmoji(args.note_id);
7418
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
7563
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
7419
7564
  }
7420
7565
  case "create_work_item_emoji_reaction": {
7421
7566
  const args = CreateWorkItemEmojiReactionSchema.parse(params.arguments);
7422
7567
  const { workItemGID } = await resolveWorkItemGID(args.project_id, args.iid);
7423
7568
  const result = await addGraphQLAwardEmoji(workItemGID, args.name);
7424
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
7569
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
7425
7570
  }
7426
7571
  case "delete_work_item_emoji_reaction": {
7427
7572
  const args = DeleteWorkItemEmojiReactionSchema.parse(params.arguments);
7428
7573
  const { workItemGID } = await resolveWorkItemGID(args.project_id, args.iid);
7429
7574
  const result = await removeGraphQLAwardEmoji(workItemGID, args.name);
7430
- return { content: [{ type: "text", text: JSON.stringify(result ?? { status: "success", message: "Work item emoji reaction removed" }, null, 2) }] };
7575
+ return { content: [{ type: "text", text: JSON.stringify(result ?? { status: "success", message: "Work item emoji reaction removed" }) }] };
7431
7576
  }
7432
7577
  case "create_work_item_note_emoji_reaction": {
7433
7578
  const args = CreateWorkItemNoteEmojiReactionSchema.parse(params.arguments);
7434
7579
  const result = await addGraphQLAwardEmoji(args.note_id, args.name);
7435
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
7580
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
7436
7581
  }
7437
7582
  case "delete_work_item_note_emoji_reaction": {
7438
7583
  const args = DeleteWorkItemNoteEmojiReactionSchema.parse(params.arguments);
7439
7584
  const result = await removeGraphQLAwardEmoji(args.note_id, args.name);
7440
- return { content: [{ type: "text", text: JSON.stringify(result ?? { status: "success", message: "Work item note emoji reaction removed" }, null, 2) }] };
7585
+ return { content: [{ type: "text", text: JSON.stringify(result ?? { status: "success", message: "Work item note emoji reaction removed" }) }] };
7441
7586
  }
7442
7587
  case "get_timeline_events": {
7443
7588
  const args = GetTimelineEventsSchema.parse(params.arguments);
7444
7589
  const result = await getTimelineEvents(args.project_id, args.incident_iid);
7445
7590
  return {
7446
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
7591
+ content: [{ type: "text", text: JSON.stringify(result) }],
7447
7592
  };
7448
7593
  }
7449
7594
  case "create_timeline_event": {
7450
7595
  const args = CreateTimelineEventSchema.parse(params.arguments);
7451
7596
  const result = await createTimelineEvent(args.project_id, args.incident_iid, args.note, args.occurred_at, args.tag_names);
7452
7597
  return {
7453
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
7598
+ content: [{ type: "text", text: JSON.stringify(result) }],
7454
7599
  };
7455
7600
  }
7456
7601
  case "list_labels": {
7457
7602
  const args = ListLabelsSchema.parse(params.arguments);
7458
7603
  const labels = await listLabels(args.project_id, args);
7459
7604
  return {
7460
- content: [{ type: "text", text: JSON.stringify(labels, null, 2) }],
7605
+ content: [{ type: "text", text: JSON.stringify(labels) }],
7461
7606
  };
7462
7607
  }
7463
7608
  case "get_label": {
7464
7609
  const args = GetLabelSchema.parse(params.arguments);
7465
7610
  const label = await getLabel(args.project_id, args.label_id, args.include_ancestor_groups);
7466
7611
  return {
7467
- content: [{ type: "text", text: JSON.stringify(label, null, 2) }],
7612
+ content: [{ type: "text", text: JSON.stringify(label) }],
7468
7613
  };
7469
7614
  }
7470
7615
  case "create_label": {
7471
7616
  const args = CreateLabelSchema.parse(params.arguments);
7472
7617
  const label = await createLabel(args.project_id, args);
7473
7618
  return {
7474
- content: [{ type: "text", text: JSON.stringify(label, null, 2) }],
7619
+ content: [{ type: "text", text: JSON.stringify(label) }],
7475
7620
  };
7476
7621
  }
7477
7622
  case "update_label": {
@@ -7479,7 +7624,7 @@ async function handleToolCall(params) {
7479
7624
  const { project_id, label_id, ...options } = args;
7480
7625
  const label = await updateLabel(project_id, label_id, options);
7481
7626
  return {
7482
- content: [{ type: "text", text: JSON.stringify(label, null, 2) }],
7627
+ content: [{ type: "text", text: JSON.stringify(label) }],
7483
7628
  };
7484
7629
  }
7485
7630
  case "delete_label": {
@@ -7498,7 +7643,7 @@ async function handleToolCall(params) {
7498
7643
  const args = ListGroupProjectsSchema.parse(params.arguments);
7499
7644
  const projects = await listGroupProjects(args);
7500
7645
  return {
7501
- content: [{ type: "text", text: JSON.stringify(projects, null, 2) }],
7646
+ content: [{ type: "text", text: JSON.stringify(projects) }],
7502
7647
  };
7503
7648
  }
7504
7649
  case "list_wiki_pages": {
@@ -7509,28 +7654,28 @@ async function handleToolCall(params) {
7509
7654
  with_content,
7510
7655
  });
7511
7656
  return {
7512
- content: [{ type: "text", text: JSON.stringify(wikiPages, null, 2) }],
7657
+ content: [{ type: "text", text: JSON.stringify(wikiPages) }],
7513
7658
  };
7514
7659
  }
7515
7660
  case "get_wiki_page": {
7516
7661
  const { project_id, slug } = GetWikiPageSchema.parse(params.arguments);
7517
7662
  const wikiPage = await getWikiPage(project_id, slug);
7518
7663
  return {
7519
- content: [{ type: "text", text: JSON.stringify(wikiPage, null, 2) }],
7664
+ content: [{ type: "text", text: JSON.stringify(wikiPage) }],
7520
7665
  };
7521
7666
  }
7522
7667
  case "create_wiki_page": {
7523
7668
  const { project_id, title, content, format } = CreateWikiPageSchema.parse(params.arguments);
7524
7669
  const wikiPage = await createWikiPage(project_id, title, content, format);
7525
7670
  return {
7526
- content: [{ type: "text", text: JSON.stringify(wikiPage, null, 2) }],
7671
+ content: [{ type: "text", text: JSON.stringify(wikiPage) }],
7527
7672
  };
7528
7673
  }
7529
7674
  case "update_wiki_page": {
7530
7675
  const { project_id, slug, title, content, format } = UpdateWikiPageSchema.parse(params.arguments);
7531
7676
  const wikiPage = await updateWikiPage(project_id, slug, title, content, format);
7532
7677
  return {
7533
- content: [{ type: "text", text: JSON.stringify(wikiPage, null, 2) }],
7678
+ content: [{ type: "text", text: JSON.stringify(wikiPage) }],
7534
7679
  };
7535
7680
  }
7536
7681
  case "delete_wiki_page": {
@@ -7556,28 +7701,28 @@ async function handleToolCall(params) {
7556
7701
  with_content,
7557
7702
  });
7558
7703
  return {
7559
- content: [{ type: "text", text: JSON.stringify(wikiPages, null, 2) }],
7704
+ content: [{ type: "text", text: JSON.stringify(wikiPages) }],
7560
7705
  };
7561
7706
  }
7562
7707
  case "get_group_wiki_page": {
7563
7708
  const { group_id, slug } = GetGroupWikiPageSchema.parse(params.arguments);
7564
7709
  const wikiPage = await getGroupWikiPage(group_id, slug);
7565
7710
  return {
7566
- content: [{ type: "text", text: JSON.stringify(wikiPage, null, 2) }],
7711
+ content: [{ type: "text", text: JSON.stringify(wikiPage) }],
7567
7712
  };
7568
7713
  }
7569
7714
  case "create_group_wiki_page": {
7570
7715
  const { group_id, title, content, format } = CreateGroupWikiPageSchema.parse(params.arguments);
7571
7716
  const wikiPage = await createGroupWikiPage(group_id, title, content, format);
7572
7717
  return {
7573
- content: [{ type: "text", text: JSON.stringify(wikiPage, null, 2) }],
7718
+ content: [{ type: "text", text: JSON.stringify(wikiPage) }],
7574
7719
  };
7575
7720
  }
7576
7721
  case "update_group_wiki_page": {
7577
7722
  const { group_id, slug, title, content, format } = UpdateGroupWikiPageSchema.parse(params.arguments);
7578
7723
  const wikiPage = await updateGroupWikiPage(group_id, slug, title, content, format);
7579
7724
  return {
7580
- content: [{ type: "text", text: JSON.stringify(wikiPage, null, 2) }],
7725
+ content: [{ type: "text", text: JSON.stringify(wikiPage) }],
7581
7726
  };
7582
7727
  }
7583
7728
  case "delete_group_wiki_page": {
@@ -7608,7 +7753,7 @@ async function handleToolCall(params) {
7608
7753
  }
7609
7754
  : items;
7610
7755
  return {
7611
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
7756
+ content: [{ type: "text", text: JSON.stringify(result) }],
7612
7757
  };
7613
7758
  }
7614
7759
  case "list_pipelines": {
@@ -7616,7 +7761,7 @@ async function handleToolCall(params) {
7616
7761
  const { project_id, ...options } = args;
7617
7762
  const pipelines = await listPipelines(project_id, options);
7618
7763
  return {
7619
- content: [{ type: "text", text: JSON.stringify(pipelines, null, 2) }],
7764
+ content: [{ type: "text", text: JSON.stringify(pipelines) }],
7620
7765
  };
7621
7766
  }
7622
7767
  case "get_pipeline": {
@@ -7626,7 +7771,7 @@ async function handleToolCall(params) {
7626
7771
  content: [
7627
7772
  {
7628
7773
  type: "text",
7629
- text: JSON.stringify(pipeline, null, 2),
7774
+ text: JSON.stringify(pipeline),
7630
7775
  },
7631
7776
  ],
7632
7777
  };
@@ -7636,14 +7781,14 @@ async function handleToolCall(params) {
7636
7781
  const { project_id, ...options } = args;
7637
7782
  const deployments = await listDeployments(project_id, options);
7638
7783
  return {
7639
- content: [{ type: "text", text: JSON.stringify(deployments, null, 2) }],
7784
+ content: [{ type: "text", text: JSON.stringify(deployments) }],
7640
7785
  };
7641
7786
  }
7642
7787
  case "get_deployment": {
7643
7788
  const { project_id, deployment_id } = GetDeploymentSchema.parse(params.arguments);
7644
7789
  const deployment = await getDeployment(project_id, deployment_id);
7645
7790
  return {
7646
- content: [{ type: "text", text: JSON.stringify(deployment, null, 2) }],
7791
+ content: [{ type: "text", text: JSON.stringify(deployment) }],
7647
7792
  };
7648
7793
  }
7649
7794
  case "list_environments": {
@@ -7651,14 +7796,14 @@ async function handleToolCall(params) {
7651
7796
  const { project_id, ...options } = args;
7652
7797
  const environments = await listEnvironments(project_id, options);
7653
7798
  return {
7654
- content: [{ type: "text", text: JSON.stringify(environments, null, 2) }],
7799
+ content: [{ type: "text", text: JSON.stringify(environments) }],
7655
7800
  };
7656
7801
  }
7657
7802
  case "get_environment": {
7658
7803
  const { project_id, environment_id } = GetEnvironmentSchema.parse(params.arguments);
7659
7804
  const environment = await getEnvironment(project_id, environment_id);
7660
7805
  return {
7661
- content: [{ type: "text", text: JSON.stringify(environment, null, 2) }],
7806
+ content: [{ type: "text", text: JSON.stringify(environment) }],
7662
7807
  };
7663
7808
  }
7664
7809
  case "list_pipeline_jobs": {
@@ -7668,7 +7813,7 @@ async function handleToolCall(params) {
7668
7813
  content: [
7669
7814
  {
7670
7815
  type: "text",
7671
- text: JSON.stringify(jobs, null, 2),
7816
+ text: JSON.stringify(jobs),
7672
7817
  },
7673
7818
  ],
7674
7819
  };
@@ -7680,7 +7825,7 @@ async function handleToolCall(params) {
7680
7825
  content: [
7681
7826
  {
7682
7827
  type: "text",
7683
- text: JSON.stringify(triggerJobs, null, 2),
7828
+ text: JSON.stringify(triggerJobs),
7684
7829
  },
7685
7830
  ],
7686
7831
  };
@@ -7692,7 +7837,7 @@ async function handleToolCall(params) {
7692
7837
  content: [
7693
7838
  {
7694
7839
  type: "text",
7695
- text: JSON.stringify(jobDetails, null, 2),
7840
+ text: JSON.stringify(jobDetails),
7696
7841
  },
7697
7842
  ],
7698
7843
  };
@@ -7714,7 +7859,7 @@ async function handleToolCall(params) {
7714
7859
  const { project_id, ...options } = args;
7715
7860
  const result = await validateCiLint(project_id, options);
7716
7861
  return {
7717
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
7862
+ content: [{ type: "text", text: JSON.stringify(result) }],
7718
7863
  };
7719
7864
  }
7720
7865
  case "validate_project_ci_lint": {
@@ -7722,8 +7867,130 @@ async function handleToolCall(params) {
7722
7867
  const { project_id, ...options } = args;
7723
7868
  const result = await validateProjectCiLint(project_id, options);
7724
7869
  return {
7725
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
7726
- };
7870
+ content: [{ type: "text", text: JSON.stringify(result) }],
7871
+ };
7872
+ }
7873
+ case "list_ci_catalog_resources": {
7874
+ const args = ListCiCatalogResourcesSchema.parse(params.arguments);
7875
+ const result = await executeGitLabGraphQL(`query ListCiCatalogResources(
7876
+ $search: String,
7877
+ $first: Int,
7878
+ $after: String,
7879
+ $groupIds: [GroupID!],
7880
+ $scope: CiCatalogResourceScope,
7881
+ $sort: CiCatalogResourceSort,
7882
+ $topics: [String!],
7883
+ $verificationLevel: CiCatalogResourceVerificationLevel
7884
+ ) {
7885
+ ciCatalogResources(
7886
+ search: $search,
7887
+ first: $first,
7888
+ after: $after,
7889
+ groupIds: $groupIds,
7890
+ scope: $scope,
7891
+ sort: $sort,
7892
+ topics: $topics,
7893
+ verificationLevel: $verificationLevel
7894
+ ) {
7895
+ nodes {
7896
+ id
7897
+ name
7898
+ description
7899
+ fullPath
7900
+ icon
7901
+ starCount
7902
+ topics
7903
+ verificationLevel
7904
+ visibilityLevel
7905
+ webPath
7906
+ latestReleasedAt
7907
+ last30DayUsageCount
7908
+ }
7909
+ pageInfo { hasNextPage endCursor }
7910
+ }
7911
+ }`, {
7912
+ search: args.search,
7913
+ first: args.first ?? 20,
7914
+ after: args.after,
7915
+ groupIds: args.group_ids,
7916
+ scope: args.scope,
7917
+ sort: args.sort,
7918
+ topics: args.topics,
7919
+ verificationLevel: args.verification_level,
7920
+ });
7921
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
7922
+ }
7923
+ case "get_ci_catalog_resource": {
7924
+ const args = GetCiCatalogResourceSchema.parse(params.arguments);
7925
+ const result = await executeGitLabGraphQL(`query GetCiCatalogResource(
7926
+ $id: CiCatalogResourceID,
7927
+ $fullPath: ID,
7928
+ $versionLimit: Int!,
7929
+ $componentLimit: Int!,
7930
+ $includeReadme: Boolean!
7931
+ ) {
7932
+ ciCatalogResource(id: $id, fullPath: $fullPath) {
7933
+ id
7934
+ name
7935
+ description
7936
+ fullPath
7937
+ icon
7938
+ starCount
7939
+ topics
7940
+ verificationLevel
7941
+ visibilityLevel
7942
+ webPath
7943
+ latestReleasedAt
7944
+ last30DayUsageCount
7945
+ versions(first: $versionLimit) {
7946
+ nodes {
7947
+ id
7948
+ name
7949
+ path
7950
+ createdAt
7951
+ releasedAt
7952
+ readme @include(if: $includeReadme)
7953
+ semver { major minor patch }
7954
+ components(first: $componentLimit) {
7955
+ nodes {
7956
+ id
7957
+ name
7958
+ description
7959
+ includePath
7960
+ last30DayUsageCount
7961
+ inputs {
7962
+ name
7963
+ description
7964
+ type
7965
+ required
7966
+ default
7967
+ options
7968
+ regex
7969
+ }
7970
+ }
7971
+ pageInfo { hasNextPage endCursor }
7972
+ }
7973
+ }
7974
+ pageInfo { hasNextPage endCursor }
7975
+ }
7976
+ }
7977
+ }`, {
7978
+ id: args.id,
7979
+ fullPath: args.full_path,
7980
+ versionLimit: args.version_limit ?? 5,
7981
+ componentLimit: args.component_limit ?? 20,
7982
+ includeReadme: args.include_readme ?? false,
7983
+ });
7984
+ if (args.component_name) {
7985
+ const resource = result?.data?.ciCatalogResource;
7986
+ for (const version of resource?.versions?.nodes ?? []) {
7987
+ const components = version?.components?.nodes;
7988
+ if (Array.isArray(components)) {
7989
+ version.components.nodes = components.filter(component => component?.name === args.component_name);
7990
+ }
7991
+ }
7992
+ }
7993
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
7727
7994
  }
7728
7995
  case "create_pipeline": {
7729
7996
  const { project_id, ref, variables, inputs } = CreatePipelineSchema.parse(params.arguments);
@@ -7804,7 +8071,7 @@ async function handleToolCall(params) {
7804
8071
  content: [
7805
8072
  {
7806
8073
  type: "text",
7807
- text: JSON.stringify(artifacts, null, 2),
8074
+ text: JSON.stringify(artifacts),
7808
8075
  },
7809
8076
  ],
7810
8077
  };
@@ -7817,7 +8084,7 @@ async function handleToolCall(params) {
7817
8084
  }
7818
8085
  const downloadUrl = buildDownloadUrl("job-artifacts", { project_id, job_id });
7819
8086
  return {
7820
- content: [{ type: "text", text: JSON.stringify({ download_url: downloadUrl, filename: `artifacts_job_${job_id}.zip` }, null, 2) }],
8087
+ content: [{ type: "text", text: JSON.stringify({ download_url: downloadUrl, filename: `artifacts_job_${job_id}.zip` }) }],
7821
8088
  };
7822
8089
  }
7823
8090
  const filePath = await downloadJobArtifacts(project_id, job_id, local_path);
@@ -7825,7 +8092,7 @@ async function handleToolCall(params) {
7825
8092
  content: [
7826
8093
  {
7827
8094
  type: "text",
7828
- text: JSON.stringify({ success: true, file_path: filePath }, null, 2),
8095
+ text: JSON.stringify({ success: true, file_path: filePath }),
7829
8096
  },
7830
8097
  ],
7831
8098
  };
@@ -7847,7 +8114,7 @@ async function handleToolCall(params) {
7847
8114
  const cleanedOptions = cleanMutuallyExclusiveIdUsernameOptions(options, LIST_MERGE_REQUESTS_ID_USERNAME_PAIRS);
7848
8115
  const mergeRequests = await listMergeRequests(project_id, cleanedOptions);
7849
8116
  return {
7850
- content: [{ type: "text", text: JSON.stringify(mergeRequests, null, 2) }],
8117
+ content: [{ type: "text", text: JSON.stringify(mergeRequests) }],
7851
8118
  };
7852
8119
  }
7853
8120
  case "list_milestones": {
@@ -7857,7 +8124,7 @@ async function handleToolCall(params) {
7857
8124
  content: [
7858
8125
  {
7859
8126
  type: "text",
7860
- text: JSON.stringify(milestones, null, 2),
8127
+ text: JSON.stringify(milestones),
7861
8128
  },
7862
8129
  ],
7863
8130
  };
@@ -7869,7 +8136,7 @@ async function handleToolCall(params) {
7869
8136
  content: [
7870
8137
  {
7871
8138
  type: "text",
7872
- text: JSON.stringify(milestone, null, 2),
8139
+ text: JSON.stringify(milestone),
7873
8140
  },
7874
8141
  ],
7875
8142
  };
@@ -7881,7 +8148,7 @@ async function handleToolCall(params) {
7881
8148
  content: [
7882
8149
  {
7883
8150
  type: "text",
7884
- text: JSON.stringify(milestone, null, 2),
8151
+ text: JSON.stringify(milestone),
7885
8152
  },
7886
8153
  ],
7887
8154
  };
@@ -7893,7 +8160,7 @@ async function handleToolCall(params) {
7893
8160
  content: [
7894
8161
  {
7895
8162
  type: "text",
7896
- text: JSON.stringify(milestone, null, 2),
8163
+ text: JSON.stringify(milestone),
7897
8164
  },
7898
8165
  ],
7899
8166
  };
@@ -7920,7 +8187,7 @@ async function handleToolCall(params) {
7920
8187
  content: [
7921
8188
  {
7922
8189
  type: "text",
7923
- text: JSON.stringify(issues, null, 2),
8190
+ text: JSON.stringify(issues),
7924
8191
  },
7925
8192
  ],
7926
8193
  };
@@ -7932,7 +8199,7 @@ async function handleToolCall(params) {
7932
8199
  content: [
7933
8200
  {
7934
8201
  type: "text",
7935
- text: JSON.stringify(mergeRequests, null, 2),
8202
+ text: JSON.stringify(mergeRequests),
7936
8203
  },
7937
8204
  ],
7938
8205
  };
@@ -7944,7 +8211,7 @@ async function handleToolCall(params) {
7944
8211
  content: [
7945
8212
  {
7946
8213
  type: "text",
7947
- text: JSON.stringify(milestone, null, 2),
8214
+ text: JSON.stringify(milestone),
7948
8215
  },
7949
8216
  ],
7950
8217
  };
@@ -7956,7 +8223,7 @@ async function handleToolCall(params) {
7956
8223
  content: [
7957
8224
  {
7958
8225
  type: "text",
7959
- text: JSON.stringify(events, null, 2),
8226
+ text: JSON.stringify(events),
7960
8227
  },
7961
8228
  ],
7962
8229
  };
@@ -7965,21 +8232,21 @@ async function handleToolCall(params) {
7965
8232
  const args = ListCommitsSchema.parse(params.arguments);
7966
8233
  const commits = await listCommits(args.project_id, args);
7967
8234
  return {
7968
- content: [{ type: "text", text: JSON.stringify(commits, null, 2) }],
8235
+ content: [{ type: "text", text: JSON.stringify(commits) }],
7969
8236
  };
7970
8237
  }
7971
8238
  case "get_commit": {
7972
8239
  const args = GetCommitSchema.parse(params.arguments);
7973
8240
  const commit = await getCommit(args.project_id, args.sha, args.stats);
7974
8241
  return {
7975
- content: [{ type: "text", text: JSON.stringify(commit, null, 2) }],
8242
+ content: [{ type: "text", text: JSON.stringify(commit) }],
7976
8243
  };
7977
8244
  }
7978
8245
  case "get_commit_diff": {
7979
8246
  const args = GetCommitDiffSchema.parse(params.arguments);
7980
8247
  const diff = await getCommitDiff(args.project_id, args.sha, args.full_diff);
7981
8248
  return {
7982
- content: [{ type: "text", text: JSON.stringify(diff, null, 2) }],
8249
+ content: [{ type: "text", text: JSON.stringify(diff) }],
7983
8250
  };
7984
8251
  }
7985
8252
  case "get_file_blame": {
@@ -7987,7 +8254,7 @@ async function handleToolCall(params) {
7987
8254
  const { project_id, ...options } = args;
7988
8255
  const blame = await getFileBlame(project_id, options);
7989
8256
  return {
7990
- content: [{ type: "text", text: JSON.stringify(blame, null, 2) }],
8257
+ content: [{ type: "text", text: JSON.stringify(blame) }],
7991
8258
  };
7992
8259
  }
7993
8260
  case "list_commit_statuses": {
@@ -7995,7 +8262,7 @@ async function handleToolCall(params) {
7995
8262
  const { project_id, sha, ...options } = args;
7996
8263
  const statuses = await listCommitStatuses(project_id, sha, options);
7997
8264
  return {
7998
- content: [{ type: "text", text: JSON.stringify(statuses, null, 2) }],
8265
+ content: [{ type: "text", text: JSON.stringify(statuses) }],
7999
8266
  };
8000
8267
  }
8001
8268
  case "create_commit_status": {
@@ -8003,14 +8270,14 @@ async function handleToolCall(params) {
8003
8270
  const { project_id, sha, ...options } = args;
8004
8271
  const status = await createCommitStatus(project_id, sha, options);
8005
8272
  return {
8006
- content: [{ type: "text", text: JSON.stringify(status, null, 2) }],
8273
+ content: [{ type: "text", text: JSON.stringify(status) }],
8007
8274
  };
8008
8275
  }
8009
8276
  case "list_group_iterations": {
8010
8277
  const args = ListGroupIterationsSchema.parse(params.arguments);
8011
8278
  const iterations = await listGroupIterations(args.group_id, args);
8012
8279
  return {
8013
- content: [{ type: "text", text: JSON.stringify(iterations, null, 2) }],
8280
+ content: [{ type: "text", text: JSON.stringify(iterations) }],
8014
8281
  };
8015
8282
  }
8016
8283
  // --- CI/CD Variables ---
@@ -8018,24 +8285,24 @@ async function handleToolCall(params) {
8018
8285
  const args = ListProjectVariablesSchema.parse(params.arguments);
8019
8286
  const { project_id, ...options } = args;
8020
8287
  const variables = await listProjectVariables(project_id, options);
8021
- return { content: [{ type: "text", text: JSON.stringify(variables, null, 2) }] };
8288
+ return { content: [{ type: "text", text: JSON.stringify(variables) }] };
8022
8289
  }
8023
8290
  case "get_project_variable": {
8024
8291
  const args = GetProjectVariableSchema.parse(params.arguments);
8025
8292
  const variable = await getProjectVariable(args.project_id, args.key, args.filter);
8026
- return { content: [{ type: "text", text: JSON.stringify(variable, null, 2) }] };
8293
+ return { content: [{ type: "text", text: JSON.stringify(variable) }] };
8027
8294
  }
8028
8295
  case "create_project_variable": {
8029
8296
  const args = CreateProjectVariableSchema.parse(params.arguments);
8030
8297
  const { project_id, ...options } = args;
8031
8298
  const variable = await createProjectVariable(project_id, options);
8032
- return { content: [{ type: "text", text: JSON.stringify(variable, null, 2) }] };
8299
+ return { content: [{ type: "text", text: JSON.stringify(variable) }] };
8033
8300
  }
8034
8301
  case "update_project_variable": {
8035
8302
  const args = UpdateProjectVariableSchema.parse(params.arguments);
8036
8303
  const { project_id, key, ...options } = args;
8037
8304
  const variable = await updateProjectVariable(project_id, key, options);
8038
- return { content: [{ type: "text", text: JSON.stringify(variable, null, 2) }] };
8305
+ return { content: [{ type: "text", text: JSON.stringify(variable) }] };
8039
8306
  }
8040
8307
  case "delete_project_variable": {
8041
8308
  const args = DeleteProjectVariableSchema.parse(params.arguments);
@@ -8054,27 +8321,27 @@ async function handleToolCall(params) {
8054
8321
  const args = ListGroupVariablesSchema.parse(params.arguments);
8055
8322
  const { group_id, ...options } = args;
8056
8323
  const variables = await listGroupVariables(group_id, options);
8057
- return { content: [{ type: "text", text: JSON.stringify(variables, null, 2) }] };
8324
+ return { content: [{ type: "text", text: JSON.stringify(variables) }] };
8058
8325
  }
8059
8326
  case "get_group_variable": {
8060
8327
  rejectIfProjectScopedDeployment("get_group_variable");
8061
8328
  const args = GetGroupVariableSchema.parse(params.arguments);
8062
8329
  const variable = await getGroupVariable(args.group_id, args.key, args.filter);
8063
- return { content: [{ type: "text", text: JSON.stringify(variable, null, 2) }] };
8330
+ return { content: [{ type: "text", text: JSON.stringify(variable) }] };
8064
8331
  }
8065
8332
  case "create_group_variable": {
8066
8333
  rejectIfProjectScopedDeployment("create_group_variable");
8067
8334
  const args = CreateGroupVariableSchema.parse(params.arguments);
8068
8335
  const { group_id, ...options } = args;
8069
8336
  const variable = await createGroupVariable(group_id, options);
8070
- return { content: [{ type: "text", text: JSON.stringify(variable, null, 2) }] };
8337
+ return { content: [{ type: "text", text: JSON.stringify(variable) }] };
8071
8338
  }
8072
8339
  case "update_group_variable": {
8073
8340
  rejectIfProjectScopedDeployment("update_group_variable");
8074
8341
  const args = UpdateGroupVariableSchema.parse(params.arguments);
8075
8342
  const { group_id, key, ...options } = args;
8076
8343
  const variable = await updateGroupVariable(group_id, key, options);
8077
- return { content: [{ type: "text", text: JSON.stringify(variable, null, 2) }] };
8344
+ return { content: [{ type: "text", text: JSON.stringify(variable) }] };
8078
8345
  }
8079
8346
  case "delete_group_variable": {
8080
8347
  rejectIfProjectScopedDeployment("delete_group_variable");
@@ -8094,7 +8361,7 @@ async function handleToolCall(params) {
8094
8361
  const args = GetDependencyProxySettingsSchema.parse(params.arguments);
8095
8362
  const settings = await getDependencyProxySettings(args.group_id);
8096
8363
  return {
8097
- content: [{ type: "text", text: JSON.stringify(settings, null, 2) }],
8364
+ content: [{ type: "text", text: JSON.stringify(settings) }],
8098
8365
  };
8099
8366
  }
8100
8367
  case "update_dependency_proxy_settings": {
@@ -8103,7 +8370,7 @@ async function handleToolCall(params) {
8103
8370
  const { group_id, ...options } = args;
8104
8371
  const settings = await updateDependencyProxySettings(group_id, options);
8105
8372
  return {
8106
- content: [{ type: "text", text: JSON.stringify(settings, null, 2) }],
8373
+ content: [{ type: "text", text: JSON.stringify(settings) }],
8107
8374
  };
8108
8375
  }
8109
8376
  case "list_dependency_proxy_blobs": {
@@ -8112,7 +8379,7 @@ async function handleToolCall(params) {
8112
8379
  const { group_id, ...options } = args;
8113
8380
  const result = await listDependencyProxyBlobs(group_id, options);
8114
8381
  return {
8115
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
8382
+ content: [{ type: "text", text: JSON.stringify(result) }],
8116
8383
  };
8117
8384
  }
8118
8385
  case "purge_dependency_proxy_cache": {
@@ -8133,13 +8400,13 @@ async function handleToolCall(params) {
8133
8400
  const args = MarkdownUploadRemoteSchema.parse(params.arguments);
8134
8401
  const upload = await markdownUpload(args.project_id, undefined, args.content, args.filename);
8135
8402
  return {
8136
- content: [{ type: "text", text: JSON.stringify(upload, null, 2) }],
8403
+ content: [{ type: "text", text: JSON.stringify(upload) }],
8137
8404
  };
8138
8405
  }
8139
8406
  const args = MarkdownUploadSchema.parse(params.arguments);
8140
8407
  const upload = await markdownUpload(args.project_id, args.file_path);
8141
8408
  return {
8142
- content: [{ type: "text", text: JSON.stringify(upload, null, 2) }],
8409
+ content: [{ type: "text", text: JSON.stringify(upload) }],
8143
8410
  };
8144
8411
  }
8145
8412
  case "download_attachment": {
@@ -8151,10 +8418,12 @@ async function handleToolCall(params) {
8151
8418
  const mimeType = getImageMimeType(args.filename);
8152
8419
  if (IS_REMOTE && !mimeType) {
8153
8420
  const downloadUrl = buildDownloadUrl("attachment", {
8154
- project_id: args.project_id, secret: args.secret, filename: args.filename,
8421
+ project_id: args.project_id,
8422
+ secret: args.secret,
8423
+ filename: args.filename,
8155
8424
  });
8156
8425
  return {
8157
- content: [{ type: "text", text: JSON.stringify({ download_url: downloadUrl, filename: args.filename }, null, 2) }],
8426
+ content: [{ type: "text", text: JSON.stringify({ download_url: downloadUrl, filename: args.filename }) }],
8158
8427
  };
8159
8428
  }
8160
8429
  const result = await downloadAttachment(args.project_id, args.secret, args.filename, args.local_path);
@@ -8166,7 +8435,7 @@ async function handleToolCall(params) {
8166
8435
  { type: "image", data: base64, mimeType: result.mimeType },
8167
8436
  {
8168
8437
  type: "text",
8169
- text: JSON.stringify({ filename: result.filename, mimeType: result.mimeType }, null, 2),
8438
+ text: JSON.stringify({ filename: result.filename, mimeType: result.mimeType }),
8170
8439
  },
8171
8440
  ],
8172
8441
  };
@@ -8175,7 +8444,7 @@ async function handleToolCall(params) {
8175
8444
  content: [
8176
8445
  {
8177
8446
  type: "text",
8178
- text: JSON.stringify({ success: true, file_path: result.savedPath }, null, 2),
8447
+ text: JSON.stringify({ success: true, file_path: result.savedPath }),
8179
8448
  },
8180
8449
  ],
8181
8450
  };
@@ -8184,7 +8453,7 @@ async function handleToolCall(params) {
8184
8453
  const args = ListEventsSchema.parse(params.arguments);
8185
8454
  const events = await listEvents(args);
8186
8455
  return {
8187
- content: [{ type: "text", text: JSON.stringify(events, null, 2) }],
8456
+ content: [{ type: "text", text: JSON.stringify(events) }],
8188
8457
  };
8189
8458
  }
8190
8459
  case "get_project_events": {
@@ -8192,7 +8461,7 @@ async function handleToolCall(params) {
8192
8461
  const { project_id, ...options } = args;
8193
8462
  const events = await getProjectEvents(project_id, options);
8194
8463
  return {
8195
- content: [{ type: "text", text: JSON.stringify(events, null, 2) }],
8464
+ content: [{ type: "text", text: JSON.stringify(events) }],
8196
8465
  };
8197
8466
  }
8198
8467
  case "list_releases": {
@@ -8200,14 +8469,14 @@ async function handleToolCall(params) {
8200
8469
  const { project_id, ...options } = args;
8201
8470
  const releases = await listReleases(project_id, options);
8202
8471
  return {
8203
- content: [{ type: "text", text: JSON.stringify(releases, null, 2) }],
8472
+ content: [{ type: "text", text: JSON.stringify(releases) }],
8204
8473
  };
8205
8474
  }
8206
8475
  case "get_release": {
8207
8476
  const args = GetReleaseSchema.parse(params.arguments);
8208
8477
  const release = await getRelease(args.project_id, args.tag_name, args.include_html_description);
8209
8478
  return {
8210
- content: [{ type: "text", text: JSON.stringify(release, null, 2) }],
8479
+ content: [{ type: "text", text: JSON.stringify(release) }],
8211
8480
  };
8212
8481
  }
8213
8482
  case "create_release": {
@@ -8215,7 +8484,7 @@ async function handleToolCall(params) {
8215
8484
  const { project_id, ...options } = args;
8216
8485
  const release = await createRelease(project_id, options);
8217
8486
  return {
8218
- content: [{ type: "text", text: JSON.stringify(release, null, 2) }],
8487
+ content: [{ type: "text", text: JSON.stringify(release) }],
8219
8488
  };
8220
8489
  }
8221
8490
  case "update_release": {
@@ -8223,7 +8492,7 @@ async function handleToolCall(params) {
8223
8492
  const { project_id, tag_name, ...options } = args;
8224
8493
  const release = await updateRelease(project_id, tag_name, options);
8225
8494
  return {
8226
- content: [{ type: "text", text: JSON.stringify(release, null, 2) }],
8495
+ content: [{ type: "text", text: JSON.stringify(release) }],
8227
8496
  };
8228
8497
  }
8229
8498
  case "delete_release": {
@@ -8254,10 +8523,12 @@ async function handleToolCall(params) {
8254
8523
  const args = DownloadReleaseAssetSchema.parse(params.arguments);
8255
8524
  if (IS_REMOTE) {
8256
8525
  const downloadUrl = buildDownloadUrl("release-asset", {
8257
- project_id: args.project_id, tag_name: args.tag_name, direct_asset_path: args.direct_asset_path,
8526
+ project_id: args.project_id,
8527
+ tag_name: args.tag_name,
8528
+ direct_asset_path: args.direct_asset_path,
8258
8529
  });
8259
8530
  return {
8260
- content: [{ type: "text", text: JSON.stringify({ download_url: downloadUrl, filename: args.direct_asset_path.split("/").pop() || args.direct_asset_path }, null, 2) }],
8531
+ content: [{ type: "text", text: JSON.stringify({ download_url: downloadUrl, filename: args.direct_asset_path.split("/").pop() || args.direct_asset_path }) }],
8261
8532
  };
8262
8533
  }
8263
8534
  const assetContent = await downloadReleaseAsset(args.project_id, args.tag_name, args.direct_asset_path);
@@ -8270,14 +8541,14 @@ async function handleToolCall(params) {
8270
8541
  const { project_id, ...options } = args;
8271
8542
  const tags = await listTags(project_id, options);
8272
8543
  return {
8273
- content: [{ type: "text", text: JSON.stringify(tags, null, 2) }],
8544
+ content: [{ type: "text", text: JSON.stringify(tags) }],
8274
8545
  };
8275
8546
  }
8276
8547
  case "get_tag": {
8277
8548
  const args = GetTagSchema.parse(params.arguments);
8278
8549
  const tag = await getTag(args.project_id, args.tag_name);
8279
8550
  return {
8280
- content: [{ type: "text", text: JSON.stringify(tag, null, 2) }],
8551
+ content: [{ type: "text", text: JSON.stringify(tag) }],
8281
8552
  };
8282
8553
  }
8283
8554
  case "create_tag": {
@@ -8285,7 +8556,7 @@ async function handleToolCall(params) {
8285
8556
  const { project_id, ...options } = args;
8286
8557
  const tag = await createTag(project_id, options);
8287
8558
  return {
8288
- content: [{ type: "text", text: JSON.stringify(tag, null, 2) }],
8559
+ content: [{ type: "text", text: JSON.stringify(tag) }],
8289
8560
  };
8290
8561
  }
8291
8562
  case "delete_tag": {
@@ -8304,30 +8575,28 @@ async function handleToolCall(params) {
8304
8575
  const args = GetTagSignatureSchema.parse(params.arguments);
8305
8576
  const signature = await getTagSignature(args.project_id, args.tag_name);
8306
8577
  return {
8307
- content: [{ type: "text", text: JSON.stringify(signature, null, 2) }],
8578
+ content: [{ type: "text", text: JSON.stringify(signature) }],
8308
8579
  };
8309
8580
  }
8310
8581
  case "list_webhooks": {
8311
8582
  const args = ListWebhooksSchema.parse(params.arguments);
8312
8583
  const webhooks = await listWebhooks(args);
8313
8584
  return {
8314
- content: [{ type: "text", text: JSON.stringify(webhooks, null, 2) }],
8585
+ content: [{ type: "text", text: JSON.stringify(webhooks) }],
8315
8586
  };
8316
8587
  }
8317
8588
  case "list_webhook_events": {
8318
8589
  const args = ListWebhookEventsSchema.parse(params.arguments);
8319
8590
  const events = await listWebhookEvents(args);
8320
8591
  return {
8321
- content: [{ type: "text", text: JSON.stringify(events, null, 2) }],
8592
+ content: [{ type: "text", text: JSON.stringify(events) }],
8322
8593
  };
8323
8594
  }
8324
8595
  case "get_webhook_event": {
8325
8596
  const args = GetWebhookEventSchema.parse(params.arguments);
8326
8597
  const event = await getWebhookEvent(args);
8327
8598
  if (!event) {
8328
- const searchScope = args.page
8329
- ? `on page ${args.page}`
8330
- : "in the 500 most recent events";
8599
+ const searchScope = args.page ? `on page ${args.page}` : "in the 500 most recent events";
8331
8600
  return {
8332
8601
  content: [
8333
8602
  {
@@ -8338,7 +8607,7 @@ async function handleToolCall(params) {
8338
8607
  };
8339
8608
  }
8340
8609
  return {
8341
- content: [{ type: "text", text: JSON.stringify(event, null, 2) }],
8610
+ content: [{ type: "text", text: JSON.stringify(event) }],
8342
8611
  };
8343
8612
  }
8344
8613
  case "health_check": {
@@ -8346,13 +8615,24 @@ async function handleToolCall(params) {
8346
8615
  const url = new URL(`${getEffectiveApiUrl()}/user`);
8347
8616
  const response = await fetch(url.toString(), getFetchConfig());
8348
8617
  let authenticated = response.ok;
8349
- if (!authenticated && (response.status === 401 || response.status === 403) && (GITLAB_JOB_TOKEN || usesJobTokenHeader())) {
8618
+ if (!authenticated &&
8619
+ (response.status === 401 || response.status === 403) &&
8620
+ (GITLAB_JOB_TOKEN || usesJobTokenHeader())) {
8350
8621
  const jobUrl = new URL(`${getEffectiveApiUrl()}/job`);
8351
8622
  const jobResponse = await fetch(jobUrl.toString(), getFetchConfig());
8352
8623
  authenticated = jobResponse.ok;
8353
8624
  }
8354
8625
  return {
8355
- content: [{ type: "text", text: JSON.stringify({ status: authenticated ? "ok" : "error", authenticated, gitlab_url: getEffectiveApiUrl() }) }],
8626
+ content: [
8627
+ {
8628
+ type: "text",
8629
+ text: JSON.stringify({
8630
+ status: authenticated ? "ok" : "error",
8631
+ authenticated,
8632
+ gitlab_url: getEffectiveApiUrl(),
8633
+ }),
8634
+ },
8635
+ ],
8356
8636
  };
8357
8637
  }
8358
8638
  case "get_branch": {
@@ -8366,7 +8646,7 @@ async function handleToolCall(params) {
8366
8646
  const data = await response.json();
8367
8647
  const branch = GitLabBranchSchema.parse(data);
8368
8648
  return {
8369
- content: [{ type: "text", text: JSON.stringify(branch, null, 2) }],
8649
+ content: [{ type: "text", text: JSON.stringify(branch) }],
8370
8650
  };
8371
8651
  }
8372
8652
  case "list_branches": {
@@ -8389,7 +8669,7 @@ async function handleToolCall(params) {
8389
8669
  const data = await response.json();
8390
8670
  const branches = z.array(GitLabBranchSchema).parse(data);
8391
8671
  return {
8392
- content: [{ type: "text", text: JSON.stringify(branches, null, 2) }],
8672
+ content: [{ type: "text", text: JSON.stringify(branches) }],
8393
8673
  };
8394
8674
  }
8395
8675
  case "delete_branch": {
@@ -8402,7 +8682,7 @@ async function handleToolCall(params) {
8402
8682
  });
8403
8683
  await handleGitLabError(response);
8404
8684
  return {
8405
- content: [{ type: "text", text: JSON.stringify({ status: "deleted", branch: args.branch_name }, null, 2) }],
8685
+ content: [{ type: "text", text: JSON.stringify({ status: "deleted", branch: args.branch_name }) }],
8406
8686
  };
8407
8687
  }
8408
8688
  case "list_protected_branches": {
@@ -8422,7 +8702,7 @@ async function handleToolCall(params) {
8422
8702
  await handleGitLabError(response);
8423
8703
  const data = z.array(GitLabProtectedBranchSchema).parse(await response.json());
8424
8704
  return {
8425
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
8705
+ content: [{ type: "text", text: JSON.stringify(data) }],
8426
8706
  };
8427
8707
  }
8428
8708
  case "get_protected_branch": {
@@ -8436,7 +8716,7 @@ async function handleToolCall(params) {
8436
8716
  await handleGitLabError(response);
8437
8717
  const data = GitLabProtectedBranchSchema.parse(await response.json());
8438
8718
  return {
8439
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
8719
+ content: [{ type: "text", text: JSON.stringify(data) }],
8440
8720
  };
8441
8721
  }
8442
8722
  case "protect_branch": {
@@ -8463,7 +8743,7 @@ async function handleToolCall(params) {
8463
8743
  await handleGitLabError(response);
8464
8744
  const data = GitLabProtectedBranchSchema.parse(await response.json());
8465
8745
  return {
8466
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
8746
+ content: [{ type: "text", text: JSON.stringify(data) }],
8467
8747
  };
8468
8748
  }
8469
8749
  case "unprotect_branch": {
@@ -8477,7 +8757,7 @@ async function handleToolCall(params) {
8477
8757
  });
8478
8758
  await handleGitLabError(response);
8479
8759
  return {
8480
- content: [{ type: "text", text: JSON.stringify({ status: "unprotected", branch: args.branch_name }, null, 2) }],
8760
+ content: [{ type: "text", text: JSON.stringify({ status: "unprotected", branch: args.branch_name }) }],
8481
8761
  };
8482
8762
  }
8483
8763
  case "update_default_branch": {
@@ -8493,7 +8773,7 @@ async function handleToolCall(params) {
8493
8773
  await handleGitLabError(response);
8494
8774
  const data = await response.json();
8495
8775
  return {
8496
- content: [{ type: "text", text: JSON.stringify({ status: "updated", default_branch: args.default_branch, project: data }, null, 2) }],
8776
+ content: [{ type: "text", text: JSON.stringify({ status: "updated", default_branch: args.default_branch, project: data }) }],
8497
8777
  };
8498
8778
  }
8499
8779
  default:
@@ -8631,18 +8911,15 @@ function registerDownloadProxy(app, maxRequestsPerMinute = Number.parseInt(proce
8631
8911
  return;
8632
8912
  }
8633
8913
  // API URL: prefer token-embedded URL, then X-GitLab-API-URL header, then default
8634
- let apiUrl = tokenApiUrl || GITLAB_API_URL;
8635
- if (!tokenApiUrl) {
8636
- const dynamicApiUrl = req.headers["x-gitlab-api-url"]?.trim();
8637
- if (ENABLE_DYNAMIC_API_URL && dynamicApiUrl) {
8638
- try {
8639
- new URL(dynamicApiUrl);
8640
- apiUrl = normalizeGitLabApiUrl(dynamicApiUrl);
8641
- }
8642
- catch {
8643
- res.status(400).json({ error: "Invalid X-GitLab-API-URL" });
8644
- return;
8645
- }
8914
+ let apiUrl = GITLAB_API_URL;
8915
+ const requestedApiUrl = tokenApiUrl || req.headers["x-gitlab-api-url"]?.trim();
8916
+ if (ENABLE_DYNAMIC_API_URL && requestedApiUrl) {
8917
+ try {
8918
+ apiUrl = resolveTrustedGitLabApiUrl(requestedApiUrl);
8919
+ }
8920
+ catch {
8921
+ res.status(400).json({ error: "Invalid X-GitLab-API-URL" });
8922
+ return;
8646
8923
  }
8647
8924
  }
8648
8925
  const { type } = req.params;
@@ -8656,7 +8933,7 @@ function registerDownloadProxy(app, maxRequestsPerMinute = Number.parseInt(proce
8656
8933
  return;
8657
8934
  }
8658
8935
  const effectiveProjectId = getEffectiveProjectId(decodeURIComponent(project_id));
8659
- gitlabUrl = `${apiUrl}/projects/${encodeURIComponent(effectiveProjectId)}/jobs/${job_id}/artifacts`;
8936
+ gitlabUrl = `${apiUrl}/projects/${encodeURIComponent(effectiveProjectId)}/jobs/${encodeGitLabPathSegment(job_id)}/artifacts`;
8660
8937
  break;
8661
8938
  }
8662
8939
  case "attachment": {
@@ -8666,17 +8943,19 @@ function registerDownloadProxy(app, maxRequestsPerMinute = Number.parseInt(proce
8666
8943
  return;
8667
8944
  }
8668
8945
  const effectiveProjectId = getEffectiveProjectId(decodeURIComponent(project_id));
8669
- gitlabUrl = `${apiUrl}/projects/${encodeURIComponent(effectiveProjectId)}/uploads/${secret}/${filename}`;
8946
+ gitlabUrl = `${apiUrl}/projects/${encodeURIComponent(effectiveProjectId)}/uploads/${encodeGitLabPathSegment(secret)}/${encodeGitLabPath(filename)}`;
8670
8947
  break;
8671
8948
  }
8672
8949
  case "release-asset": {
8673
8950
  const { project_id, tag_name, direct_asset_path } = req.query;
8674
8951
  if (!project_id || !tag_name || !direct_asset_path) {
8675
- res.status(400).json({ error: "project_id, tag_name, and direct_asset_path are required" });
8952
+ res
8953
+ .status(400)
8954
+ .json({ error: "project_id, tag_name, and direct_asset_path are required" });
8676
8955
  return;
8677
8956
  }
8678
8957
  const effectiveProjectId = getEffectiveProjectId(decodeURIComponent(project_id));
8679
- gitlabUrl = `${apiUrl}/projects/${encodeURIComponent(effectiveProjectId)}/releases/${encodeURIComponent(tag_name)}/downloads/${direct_asset_path}`;
8958
+ gitlabUrl = `${apiUrl}/projects/${encodeURIComponent(effectiveProjectId)}/releases/${encodeURIComponent(tag_name)}/downloads/${encodeGitLabPath(direct_asset_path)}`;
8680
8959
  break;
8681
8960
  }
8682
8961
  default:
@@ -8728,12 +9007,21 @@ function registerDownloadProxy(app, maxRequestsPerMinute = Number.parseInt(proce
8728
9007
  */
8729
9008
  async function startSSEServer() {
8730
9009
  const app = express();
9010
+ const sseAuthToken = getConfig("sse-auth-token", "SSE_AUTH_TOKEN");
8731
9011
  if (MCP_TRUST_PROXY) {
8732
9012
  app.set("trust proxy", 1);
8733
9013
  }
9014
+ const requireSseAuth = (req, res, next) => {
9015
+ if (!sseAuthToken)
9016
+ return next();
9017
+ const match = /^Bearer\s+(\S+)$/i.exec(req.headers.authorization || "");
9018
+ if (match?.[1] === sseAuthToken)
9019
+ return next();
9020
+ res.status(401).json({ error: "SSE authentication required" });
9021
+ };
8734
9022
  const transports = {};
8735
9023
  let shuttingDown = false;
8736
- app.get("/sse", async (_, res) => {
9024
+ app.get("/sse", requireSseAuth, async (_, res) => {
8737
9025
  const serverInstance = createServer();
8738
9026
  const transport = new SSEServerTransport("/messages", res);
8739
9027
  transports[transport.sessionId] = transport;
@@ -8742,7 +9030,7 @@ async function startSSEServer() {
8742
9030
  });
8743
9031
  await serverInstance.connect(transport);
8744
9032
  });
8745
- app.post("/messages", async (req, res) => {
9033
+ app.post("/messages", requireSseAuth, async (req, res) => {
8746
9034
  const sessionId = req.query.sessionId;
8747
9035
  const transport = transports[sessionId];
8748
9036
  if (transport) {
@@ -8866,12 +9154,11 @@ async function startStreamableHTTPServer() {
8866
9154
  // Only process dynamic URL if the feature is enabled
8867
9155
  if (ENABLE_DYNAMIC_API_URL && dynamicApiUrl) {
8868
9156
  try {
8869
- new URL(dynamicApiUrl); // Ensure it's a valid URL format
8870
- apiUrl = normalizeGitLabApiUrl(dynamicApiUrl);
9157
+ apiUrl = resolveTrustedGitLabApiUrl(dynamicApiUrl);
8871
9158
  }
8872
9159
  catch {
8873
9160
  logger.warn(`Invalid X-GitLab-API-URL provided: ${dynamicApiUrl}. Auth will fail.`);
8874
- return null; // Reject if URL is malformed
9161
+ return null; // Reject if URL is malformed or not allowed
8875
9162
  }
8876
9163
  }
8877
9164
  // Extract token — priority: Private-Token > JOB-TOKEN > Authorization Bearer
@@ -9079,7 +9366,7 @@ async function startStreamableHTTPServer() {
9079
9366
  token: effective.token,
9080
9367
  lastUsed: Date.now(),
9081
9368
  apiUrl: effective.apiUrl,
9082
- publicBaseUrl: getForwardedPublicBaseUrl(req),
9369
+ publicBaseUrl: getForwardedPublicBaseUrl(req, MCP_TRUST_PROXY),
9083
9370
  };
9084
9371
  // Step 4: create a fresh transport per request.
9085
9372
  const transport = isInit
@@ -9273,16 +9560,14 @@ async function startStreamableHTTPServer() {
9273
9560
  // Streamable HTTP endpoint - handles both session creation and message handling
9274
9561
  app.post("/mcp", mcpBearerAuth, async (req, res) => {
9275
9562
  const sessionId = readMcpSessionIdHeader(req);
9276
- const publicBaseUrl = getForwardedPublicBaseUrl(req);
9563
+ const publicBaseUrl = getForwardedPublicBaseUrl(req, MCP_TRUST_PROXY);
9277
9564
  // Track request
9278
9565
  metrics.requestsProcessed++;
9279
9566
  // Stateless-mode branch: bypass authBySession / streamableTransports
9280
9567
  // entirely and derive the session auth from either the current request
9281
9568
  // headers (init) or a sealed Mcp-Session-Id (subsequent requests).
9282
9569
  // Rate limiting is disabled here because there is no shared counter.
9283
- if (OAUTH_STATELESS_MODE &&
9284
- STATELESS_MATERIAL &&
9285
- (REMOTE_AUTHORIZATION || GITLAB_MCP_OAUTH)) {
9570
+ if (OAUTH_STATELESS_MODE && STATELESS_MATERIAL && (REMOTE_AUTHORIZATION || GITLAB_MCP_OAUTH)) {
9286
9571
  await handleStatelessMcpRequest(req, res, STATELESS_MATERIAL, OAUTH_STATELESS_SESSION_TTL_SECONDS);
9287
9572
  return;
9288
9573
  }
@@ -9307,9 +9592,11 @@ async function startStreamableHTTPServer() {
9307
9592
  // Handle remote authorization: extract and store auth headers per session
9308
9593
  if (REMOTE_AUTHORIZATION) {
9309
9594
  const authData = parseAuthHeaders(req);
9595
+ const allowUnauthenticatedDiscovery = GITLAB_ALLOW_UNAUTHENTICATED_TOOL_DISCOVERY &&
9596
+ isUnauthenticatedDiscoveryRequestBody(req.body);
9310
9597
  if (sessionId && !authBySession[sessionId]) {
9311
- // New session: require auth headers
9312
- if (!authData) {
9598
+ // New session: require auth headers unless public discovery was explicitly enabled.
9599
+ if (!authData && !allowUnauthenticatedDiscovery) {
9313
9600
  metrics.authFailures++;
9314
9601
  res.status(401).json({
9315
9602
  error: "Missing Private-Token, JOB-TOKEN, or Authorization header",
@@ -9317,10 +9604,16 @@ async function startStreamableHTTPServer() {
9317
9604
  });
9318
9605
  return;
9319
9606
  }
9320
- // Store auth for this session
9321
- authBySession[sessionId] = withPublicBaseUrl(authData, publicBaseUrl);
9322
- logger.info(`Session ${sessionId}: stored ${authData.header} header`);
9323
- setAuthTimeout(sessionId);
9607
+ // Store auth only when provided. Public discovery intentionally leaves the session unauthenticated.
9608
+ if (authData) {
9609
+ authBySession[sessionId] = withPublicBaseUrl(authData, publicBaseUrl);
9610
+ logger.info(`Session ${sessionId}: stored ${authData.header} header`);
9611
+ setAuthTimeout(sessionId);
9612
+ }
9613
+ else if (allowUnauthenticatedDiscovery) {
9614
+ // Schedule cleanup for unauthenticated discovery sessions to prevent slot exhaustion
9615
+ setAuthTimeout(sessionId);
9616
+ }
9324
9617
  }
9325
9618
  else if (sessionId && authData) {
9326
9619
  // Existing session: allow auth rotation/update
@@ -9493,25 +9786,112 @@ async function startStreamableHTTPServer() {
9493
9786
  message: "GET /mcp is not supported when STREAMABLE_HTTP is enabled. Use POST to communicate with the MCP server.",
9494
9787
  });
9495
9788
  });
9789
+ const getMetricsSnapshot = () => ({
9790
+ ...metrics,
9791
+ activeSessions: Object.keys(streamableTransports).length,
9792
+ authenticatedSessions: Object.keys(authBySession).length,
9793
+ gitlabClientPool: clientPool.getStats(),
9794
+ uptime: process.uptime(),
9795
+ memoryUsage: process.memoryUsage(),
9796
+ config: {
9797
+ maxSessions: MAX_SESSIONS,
9798
+ maxRequestsPerMinute: MAX_REQUESTS_PER_MINUTE,
9799
+ sessionTimeoutSeconds: SESSION_TIMEOUT_SECONDS,
9800
+ remoteAuthEnabled: REMOTE_AUTHORIZATION,
9801
+ mcpOAuthEnabled: GITLAB_MCP_OAUTH,
9802
+ statelessModeEnabled: OAUTH_STATELESS_MODE && STATELESS_MATERIAL !== null,
9803
+ statelessRotationKey: OAUTH_STATELESS_MODE && STATELESS_MATERIAL?.previous != null,
9804
+ },
9805
+ });
9806
+ const escapePrometheusLabel = (value) => String(value).replace(/\\/g, "\\\\").replace(/\n/g, "\\n").replace(/"/g, '\\"');
9807
+ const formatPrometheusMetrics = () => {
9808
+ const snapshot = getMetricsSnapshot();
9809
+ const configLabels = Object.entries({
9810
+ max_sessions: snapshot.config.maxSessions,
9811
+ max_requests_per_minute: snapshot.config.maxRequestsPerMinute,
9812
+ session_timeout_seconds: snapshot.config.sessionTimeoutSeconds,
9813
+ remote_auth_enabled: snapshot.config.remoteAuthEnabled,
9814
+ mcp_oauth_enabled: snapshot.config.mcpOAuthEnabled,
9815
+ stateless_mode_enabled: snapshot.config.statelessModeEnabled,
9816
+ stateless_rotation_key: snapshot.config.statelessRotationKey,
9817
+ })
9818
+ .map(([key, value]) => `${key}="${escapePrometheusLabel(value)}"`)
9819
+ .join(",");
9820
+ return [
9821
+ "# HELP gitlab_mcp_requests_processed_total Total MCP requests processed",
9822
+ "# TYPE gitlab_mcp_requests_processed_total counter",
9823
+ `gitlab_mcp_requests_processed_total ${snapshot.requestsProcessed}`,
9824
+ "",
9825
+ "# HELP gitlab_mcp_requests_rejected_total Requests rejected, by reason",
9826
+ "# TYPE gitlab_mcp_requests_rejected_total counter",
9827
+ `gitlab_mcp_requests_rejected_total{reason="rate_limit"} ${snapshot.rejectedByRateLimit}`,
9828
+ `gitlab_mcp_requests_rejected_total{reason="capacity"} ${snapshot.rejectedByCapacity}`,
9829
+ "",
9830
+ "# HELP gitlab_mcp_auth_failures_total Authentication failures",
9831
+ "# TYPE gitlab_mcp_auth_failures_total counter",
9832
+ `gitlab_mcp_auth_failures_total ${snapshot.authFailures}`,
9833
+ "",
9834
+ "# HELP gitlab_mcp_sessions_total Total sessions created",
9835
+ "# TYPE gitlab_mcp_sessions_total counter",
9836
+ `gitlab_mcp_sessions_total ${snapshot.totalSessions}`,
9837
+ "",
9838
+ "# HELP gitlab_mcp_sessions_expired_total Sessions expired due to inactivity",
9839
+ "# TYPE gitlab_mcp_sessions_expired_total counter",
9840
+ `gitlab_mcp_sessions_expired_total ${snapshot.expiredSessions}`,
9841
+ "",
9842
+ "# HELP gitlab_mcp_active_sessions Currently active sessions",
9843
+ "# TYPE gitlab_mcp_active_sessions gauge",
9844
+ `gitlab_mcp_active_sessions ${snapshot.activeSessions}`,
9845
+ "",
9846
+ "# HELP gitlab_mcp_authenticated_sessions Currently authenticated sessions",
9847
+ "# TYPE gitlab_mcp_authenticated_sessions gauge",
9848
+ `gitlab_mcp_authenticated_sessions ${snapshot.authenticatedSessions}`,
9849
+ "",
9850
+ "# HELP gitlab_mcp_client_pool_size Current GitLab client pool size",
9851
+ "# TYPE gitlab_mcp_client_pool_size gauge",
9852
+ `gitlab_mcp_client_pool_size ${snapshot.gitlabClientPool.size}`,
9853
+ "",
9854
+ "# HELP gitlab_mcp_client_pool_max_size Maximum GitLab client pool size",
9855
+ "# TYPE gitlab_mcp_client_pool_max_size gauge",
9856
+ `gitlab_mcp_client_pool_max_size ${snapshot.gitlabClientPool.maxSize}`,
9857
+ "",
9858
+ "# HELP gitlab_mcp_uptime_seconds Process uptime in seconds",
9859
+ "# TYPE gitlab_mcp_uptime_seconds gauge",
9860
+ `gitlab_mcp_uptime_seconds ${snapshot.uptime}`,
9861
+ "",
9862
+ "# HELP gitlab_mcp_memory_usage_bytes Node.js memory usage by type",
9863
+ "# TYPE gitlab_mcp_memory_usage_bytes gauge",
9864
+ ...Object.entries(snapshot.memoryUsage).map(([key, value]) => `gitlab_mcp_memory_usage_bytes{type="${escapePrometheusLabel(key)}"} ${value}`),
9865
+ "",
9866
+ "# HELP gitlab_mcp_stateless_requests_total Stateless MCP requests processed",
9867
+ "# TYPE gitlab_mcp_stateless_requests_total counter",
9868
+ `gitlab_mcp_stateless_requests_total ${snapshot.statelessRequests}`,
9869
+ "",
9870
+ "# HELP gitlab_mcp_stateless_auth_total Stateless auth successes, by source",
9871
+ "# TYPE gitlab_mcp_stateless_auth_total counter",
9872
+ `gitlab_mcp_stateless_auth_total{source="header"} ${snapshot.statelessAuthFromHeader}`,
9873
+ `gitlab_mcp_stateless_auth_total{source="sealed_session_id"} ${snapshot.statelessAuthFromSealedSid}`,
9874
+ "",
9875
+ "# HELP gitlab_mcp_stateless_auth_failures_total Stateless auth failures",
9876
+ "# TYPE gitlab_mcp_stateless_auth_failures_total counter",
9877
+ `gitlab_mcp_stateless_auth_failures_total ${snapshot.statelessAuthFailures}`,
9878
+ "",
9879
+ "# HELP gitlab_mcp_stateless_session_id_rotations_total Stateless session id rotations",
9880
+ "# TYPE gitlab_mcp_stateless_session_id_rotations_total counter",
9881
+ `gitlab_mcp_stateless_session_id_rotations_total ${snapshot.statelessSidRotated}`,
9882
+ "",
9883
+ "# HELP gitlab_mcp_config_info Static configuration (value is always 1)",
9884
+ "# TYPE gitlab_mcp_config_info gauge",
9885
+ `gitlab_mcp_config_info{${configLabels}} 1`,
9886
+ "",
9887
+ ].join("\n");
9888
+ };
9496
9889
  // Metrics endpoint
9497
9890
  app.get("/metrics", (_req, res) => {
9498
- res.json({
9499
- ...metrics,
9500
- activeSessions: Object.keys(streamableTransports).length,
9501
- authenticatedSessions: Object.keys(authBySession).length,
9502
- gitlabClientPool: clientPool.getStats(),
9503
- uptime: process.uptime(),
9504
- memoryUsage: process.memoryUsage(),
9505
- config: {
9506
- maxSessions: MAX_SESSIONS,
9507
- maxRequestsPerMinute: MAX_REQUESTS_PER_MINUTE,
9508
- sessionTimeoutSeconds: SESSION_TIMEOUT_SECONDS,
9509
- remoteAuthEnabled: REMOTE_AUTHORIZATION,
9510
- mcpOAuthEnabled: GITLAB_MCP_OAUTH,
9511
- statelessModeEnabled: OAUTH_STATELESS_MODE && STATELESS_MATERIAL !== null,
9512
- statelessRotationKey: OAUTH_STATELESS_MODE && STATELESS_MATERIAL?.previous != null,
9513
- },
9514
- });
9891
+ res.type("text/plain; version=0.0.4").send(formatPrometheusMetrics());
9892
+ });
9893
+ app.get("/metrics.json", (_req, res) => {
9894
+ res.json(getMetricsSnapshot());
9515
9895
  });
9516
9896
  // Health check endpoint
9517
9897
  app.get("/health", (_req, res) => {