@zereight/mcp-gitlab 2.1.70 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/README.ko.md +3 -1
  2. package/README.md +6 -2
  3. package/README.pt-BR.md +1 -1
  4. package/README.zh-CN.md +3 -1
  5. package/build/config.js +12 -4
  6. package/build/deprecated-env.js +47 -0
  7. package/build/index.js +369 -184
  8. package/build/oauth-device-flow.js +80 -0
  9. package/build/oauth.js +135 -18
  10. package/build/schemas.js +1 -1
  11. package/build/server/remote-http-stack.js +39 -0
  12. package/build/tools/discover-activation.js +38 -0
  13. package/build/tools/exposed-tools.js +35 -0
  14. package/build/tools/listed-tools.js +76 -0
  15. package/build/tools/registry.js +226 -57
  16. package/build/tools/tool-profile.js +66 -0
  17. package/build/utils/compact-tool-allowlist.js +14 -0
  18. package/build/utils/logger.js +34 -12
  19. package/package.json +3 -3
  20. package/build/scripts/check-skill-sync.js +0 -143
  21. package/build/scripts/generate-server-card.js +0 -39
  22. package/build/scripts/generate-tool-coverage.js +0 -58
  23. package/build/scripts/generate-tool-docs.js +0 -430
  24. package/build/scripts/tool-coverage/coverage.js +0 -341
  25. package/build/scripts/tool-coverage/coverage.test.js +0 -142
  26. package/build/test/callback-proxy-tests.js +0 -321
  27. package/build/test/cli/args.test.js +0 -175
  28. package/build/test/cli/boolean-flags.test.js +0 -106
  29. package/build/test/cli/compact-mcp-result.test.js +0 -93
  30. package/build/test/cli/credentials.test.js +0 -153
  31. package/build/test/cli/exposure.test.js +0 -104
  32. package/build/test/cli/load-config-snapshot.js +0 -5
  33. package/build/test/cli/output.test.js +0 -74
  34. package/build/test/cli/replay-command.test.js +0 -37
  35. package/build/test/cli/router.test.js +0 -183
  36. package/build/test/client-pool-test.js +0 -109
  37. package/build/test/clients/client.js +0 -34
  38. package/build/test/clients/custom-header-client.js +0 -122
  39. package/build/test/clients/sse-client.js +0 -95
  40. package/build/test/clients/stdio-client.js +0 -113
  41. package/build/test/clients/streamable-http-client.js +0 -95
  42. package/build/test/config-allowed-groups.test.js +0 -97
  43. package/build/test/create-commit-actions.test.js +0 -102
  44. package/build/test/draft-note-schema.test.js +0 -44
  45. package/build/test/dynamic-api-url-allowlist.test.js +0 -121
  46. package/build/test/dynamic-api-url-test.js +0 -304
  47. package/build/test/dynamic-routing-tests.js +0 -495
  48. package/build/test/get-merge-request-discussion-schema.test.js +0 -30
  49. package/build/test/gitlab-api-tool-errors.test.js +0 -112
  50. package/build/test/gitlab-artifact-entry-schema.test.js +0 -44
  51. package/build/test/gitlab-diff-schema.test.js +0 -36
  52. package/build/test/gitlab-pipeline-schedule-schema.test.js +0 -32
  53. package/build/test/gitlab-tag-schema.test.js +0 -51
  54. package/build/test/group-milestone-schema.test.js +0 -45
  55. package/build/test/mcp-oauth-tests.js +0 -905
  56. package/build/test/mcp-server-name.test.js +0 -87
  57. package/build/test/merge-merge-request-schema.test.js +0 -75
  58. package/build/test/multi-server-test.js +0 -190
  59. package/build/test/no-proxy-integration-test.js +0 -183
  60. package/build/test/no-proxy-test.js +0 -149
  61. package/build/test/nullable-gitlab-response-fields.test.js +0 -37
  62. package/build/test/nullish-tool-arguments-schema.test.js +0 -118
  63. package/build/test/oauth-device-flow-tests.js +0 -450
  64. package/build/test/oauth-download-url.test.js +0 -157
  65. package/build/test/oauth-startup.test.js +0 -204
  66. package/build/test/oauth-tests.js +0 -428
  67. package/build/test/path-segment-encoding.test.js +0 -98
  68. package/build/test/remote-auth-simple-test.js +0 -277
  69. package/build/test/remote-auth-tests.js +0 -314
  70. package/build/test/response-masking.test.js +0 -445
  71. package/build/test/schema-tests.js +0 -1679
  72. package/build/test/server/metrics.test.js +0 -47
  73. package/build/test/server/request-helpers.test.js +0 -48
  74. package/build/test/server/version.test.js +0 -13
  75. package/build/test/sse-auth-guard.test.js +0 -96
  76. package/build/test/sse-session-limits.test.js +0 -273
  77. package/build/test/stateless/callback-proxy.test.js +0 -510
  78. package/build/test/stateless/client-id.test.js +0 -177
  79. package/build/test/stateless/codec.test.js +0 -328
  80. package/build/test/stateless/config-ttl.test.js +0 -149
  81. package/build/test/stateless/consumed-proxy-code-cache.test.js +0 -150
  82. package/build/test/stateless/session-id-integration.test.js +0 -675
  83. package/build/test/stateless/session-id.test.js +0 -157
  84. package/build/test/streamable-http-concurrent-session.test.js +0 -171
  85. package/build/test/streamable-http-dns-rebinding.test.js +0 -185
  86. package/build/test/streamable-http-sse-stream.test.js +0 -350
  87. package/build/test/streamable-http-static-token-auth.test.js +0 -157
  88. package/build/test/streamable-http-unauthenticated-discovery.test.js +0 -207
  89. package/build/test/test-all-transport-server.js +0 -255
  90. package/build/test/test-auth-retry.js +0 -195
  91. package/build/test/test-blank-filters.js +0 -156
  92. package/build/test/test-ci-catalog.js +0 -177
  93. package/build/test/test-ci-lint.js +0 -191
  94. package/build/test/test-ci-variables.js +0 -309
  95. package/build/test/test-create-repository.js +0 -120
  96. package/build/test/test-dependency-proxy.js +0 -195
  97. package/build/test/test-deployment-tools.js +0 -463
  98. package/build/test/test-download-attachment.js +0 -190
  99. package/build/test/test-dynamic-project-scope.js +0 -524
  100. package/build/test/test-empty-toolsets.js +0 -535
  101. package/build/test/test-get-file-blame.js +0 -145
  102. package/build/test/test-get-file-contents.js +0 -128
  103. package/build/test/test-geteffectiveprojectid.js +0 -804
  104. package/build/test/test-health-check.js +0 -115
  105. package/build/test/test-issue-description-patch.js +0 -256
  106. package/build/test/test-job-artifacts.js +0 -275
  107. package/build/test/test-json-schema.js +0 -166
  108. package/build/test/test-list-group-members.js +0 -150
  109. package/build/test/test-list-group-merge-requests.js +0 -165
  110. package/build/test/test-list-issues.js +0 -170
  111. package/build/test/test-list-merge-requests.js +0 -201
  112. package/build/test/test-list-milestones.js +0 -145
  113. package/build/test/test-list-project-members.js +0 -132
  114. package/build/test/test-merge-request-approval-state-tools.js +0 -213
  115. package/build/test/test-merge-request-approvals.js +0 -187
  116. package/build/test/test-merge-request-pipelines.js +0 -106
  117. package/build/test/test-mr-diffs-filter.js +0 -132
  118. package/build/test/test-mr-file-diffs.js +0 -251
  119. package/build/test/test-oauth-proxy-rate-limit.js +0 -133
  120. package/build/test/test-orbit.js +0 -155
  121. package/build/test/test-permission-mode.js +0 -424
  122. package/build/test/test-pipeline-schedules.js +0 -484
  123. package/build/test/test-protected-branches.js +0 -174
  124. package/build/test/test-remote-downloads.js +0 -510
  125. package/build/test/test-search-code.js +0 -275
  126. package/build/test/test-snippets.js +0 -735
  127. package/build/test/test-tags.js +0 -220
  128. package/build/test/test-todos.js +0 -195
  129. package/build/test/test-token-optimizations.js +0 -691
  130. package/build/test/test-toolset-filtering.js +0 -499
  131. package/build/test/test-update-issue-slim.js +0 -141
  132. package/build/test/test-update-project.js +0 -112
  133. package/build/test/test-upload-markdown.js +0 -206
  134. package/build/test/test-vulnerabilities.js +0 -399
  135. package/build/test/test-webhooks.js +0 -218
  136. package/build/test/tool-description-quality.test.js +0 -50
  137. package/build/test/utils/bulk-publish-options.test.js +0 -32
  138. package/build/test/utils/download-token.test.js +0 -35
  139. package/build/test/utils/forwarded-public-base-url.test.js +0 -46
  140. package/build/test/utils/gitlab-instance-version.test.js +0 -32
  141. package/build/test/utils/gitlab-version-gate.test.js +0 -74
  142. package/build/test/utils/graphql-query.test.js +0 -168
  143. package/build/test/utils/helpers-path.test.js +0 -104
  144. package/build/test/utils/jmespath-tool-result.test.js +0 -91
  145. package/build/test/utils/masking.test.js +0 -384
  146. package/build/test/utils/merge-request-position.test.js +0 -57
  147. package/build/test/utils/mock-gitlab-server.js +0 -614
  148. package/build/test/utils/proxy-client-ip.test.js +0 -28
  149. package/build/test/utils/redact-sensitive.test.js +0 -32
  150. package/build/test/utils/safe-redirect-fetch.test.js +0 -463
  151. package/build/test/utils/server-launcher.js +0 -231
  152. package/build/test/utils/tool-args.test.js +0 -210
  153. package/build/test/utils/url.test.js +0 -141
  154. package/build/test/utils/version-check.test.js +0 -239
  155. package/build/test/utils/wiki-title.test.js +0 -21
  156. package/build/test-note.js +0 -54
  157. package/build/test-resolve-issue-note.js +0 -127
package/build/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { getConfig, ENABLE_DYNAMIC_API_URL, ENABLE_DYNAMIC_PROJECT_SCOPE, ENABLE_STRICT_PROJECT_SCOPE, 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_DISABLE_VERSION_CHECK, GITLAB_READ_ONLY_MODE, GITLAB_MASKING_ENABLED, GITLAB_MASKING_CONFIG, GITLAB_MASKING_POLICY_FILE, GITLAB_MASKING_WORKSPACE_DIR, GITLAB_PERMISSION_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, GITLAB_MCP_COMPACT_RESULTS, GITLAB_MCP_COMPACT_RESULT_CHARS, } from "./config.js";
2
+ import { getConfig, ENABLE_DYNAMIC_API_URL, ENABLE_DYNAMIC_PROJECT_SCOPE, ENABLE_STRICT_PROJECT_SCOPE, 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_DISABLE_VERSION_CHECK, GITLAB_READ_ONLY_MODE, GITLAB_MASKING_ENABLED, GITLAB_MASKING_CONFIG, GITLAB_MASKING_POLICY_FILE, GITLAB_MASKING_WORKSPACE_DIR, GITLAB_PERMISSION_MODE, GITLAB_PERMISSION_MODE_RAW, 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_GITLAB_WIKI_RAW, USE_MILESTONE, USE_MILESTONE_RAW, USE_OAUTH, USE_PIPELINE, USE_PIPELINE_RAW, GITLAB_TOOL_POLICY_APPROVE_RAW, GITLAB_TOOL_POLICY_HIDDEN_RAW, GITLAB_OAUTH_ALLOWED_GROUPS_RAW, GITLAB_ALLOWED_GROUPS_RAW, GITLAB_OAUTH_ALLOWED_GROUPS, GITLAB_MCP_COMPACT_RESULTS, GITLAB_MCP_COMPACT_RESULT_CHARS, GITLAB_MCP_COMPACT_TOOL_NAMES, GITLAB_TOOL_PROFILE, } from "./config.js";
3
+ import { getDeprecatedEnvWarnings } from "./deprecated-env.js";
3
4
  /** True when the server is running in remote/network mode (SSE or StreamableHTTP transport). */
4
5
  const IS_REMOTE = SSE || STREAMABLE_HTTP;
5
6
  const STREAMABLE_HTTP_AUTH_TOKEN = getConfig("streamable-http-auth-token", "STREAMABLE_HTTP_AUTH_TOKEN");
@@ -60,17 +61,14 @@ function isConstantTimeSecretMatch(provided, expected) {
60
61
  }
61
62
  import { loadKeyMaterialFromEnv, looksLikeStatelessSessionId, mintSessionId, openSessionId, } from "./stateless/index.js";
62
63
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
63
- import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
64
64
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
65
- import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
66
65
  import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
67
66
  import { AsyncLocalStorage } from "node:async_hooks";
68
- import express from "express";
69
67
  import fetchCookie from "fetch-cookie";
70
68
  import fs from "node:fs";
71
69
  import { pipeline as streamPipeline } from "node:stream/promises";
72
70
  import os from "node:os";
73
- import { fetch as undiciFetch, File, FormData } from "undici";
71
+ import { fetch as undiciFetch, File, FormData, } from "undici";
74
72
  import path from "node:path";
75
73
  import { CookieJar, parse as parseCookie } from "tough-cookie";
76
74
  import { URL } from "node:url";
@@ -83,12 +81,11 @@ import { formatCliError, formatToolOutput } from "./cli/output.js";
83
81
  import { compactMcpToolResult, readOwnRecord } from "./cli/compact-mcp-result.js";
84
82
  import { compileDeniedToolsRegex } from "./tools/denied-regex.js";
85
83
  import { createGitLabOAuthProvider } from "./oauth-proxy.js";
86
- import { mcpAuthRouter } from "@modelcontextprotocol/sdk/server/auth/router.js";
87
- import rateLimit, { ipKeyGenerator } from "express-rate-limit";
88
84
  import { normalizeProxyClientIpForRateLimit } from "./utils/proxy-client-ip.js";
89
85
  import { getForwardedPublicBaseUrl, getForwardedRequestHost, } from "./utils/forwarded-public-base-url.js";
90
86
  import { registerDownloadProxy } from "./downloads/proxy.js";
91
87
  import { createDownloadToken } from "./utils/download-token.js";
88
+ import { loadRemoteHttpStack } from "./server/remote-http-stack.js";
92
89
  import { determineTransportMode, TransportMode } from "./server/transport-mode.js";
93
90
  import { formatPrometheusMetrics } from "./server/metrics.js";
94
91
  import { SERVER_VERSION } from "./server/version.js";
@@ -109,9 +106,13 @@ import { buildBulkPublishDraftNotesBody, needsGitLab19_2BulkPublish, } from "./u
109
106
  import { appendFilterParam, appendFilterParams, cleanMutuallyExclusiveIdUsernameOptions, dropBlankArrayEntries, isBlankFilterValue, LIST_MERGE_REQUESTS_ID_USERNAME_PAIRS, sanitizeToolArguments, } from "./utils/tool-args.js";
110
107
  import { applyJmespathToToolResult, getJmespathSyntaxError, JMESPATH_TOOL_ARGUMENT, JMESPATH_TOOL_ARGUMENT_DESCRIPTION, omitJmespathArgument, readJmespathExpression, } from "./utils/jmespath-tool-result.js";
111
108
  import { parseSearchReplaceBlocks, applySearchReplace, applyUnifiedDiff, } from "./utils/patch-helper.js";
112
- import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js";
113
109
  import { GitLabClientPool } from "./gitlab-client-pool.js";
114
- import { allTools, readOnlyTools, destructiveTools, deleteTools, parseEnabledToolsets, parseIndividualTools, buildFeatureFlagOverrides, isToolInEnabledToolset, TOOLSET_DEFINITIONS, ALL_TOOLSET_IDS, } from "./tools/registry.js";
110
+ import { advertisesReadOnly, allTools, readOnlyTools, destructiveTools, deleteTools, parseEnabledToolsets, parseIndividualTools, buildFeatureFlagOverrides, isToolInEnabledToolset, findSmallestToolsetForTool, isToolsetFullyActive, listDiscoverableCategories, TOOLSET_DEFINITIONS, ALL_TOOLSET_IDS, } from "./tools/registry.js";
111
+ import { buildUnavailableToolMessage, isSkippedByDiscoverActivation, } from "./tools/discover-activation.js";
112
+ import { selectExposedTools } from "./tools/exposed-tools.js";
113
+ import { createListedToolsCache, decorateToolsForList, } from "./tools/listed-tools.js";
114
+ import { isExcludedBySlimProfile, shouldApplySlimToolProfile } from "./tools/tool-profile.js";
115
+ import { shouldCompactToolResult } from "./utils/compact-tool-allowlist.js";
115
116
  import { BulkPublishDraftNotesSchema, CancelPipelineJobSchema, ErasePipelineJobSchema, CancelPipelineSchema, CreateBranchSchema, CreateDraftNoteSchema, CreateIssueLinkSchema, CreateIssueNoteSchema, CreateIssueSchema, CreateIssueEmojiReactionSchema, CreateIssueNoteEmojiReactionSchema, ListIssueEmojiReactionsSchema, ListIssueNoteEmojiReactionsSchema, CreateLabelSchema, // Added
116
117
  CreateMergeRequestNoteSchema, CreateMergeRequestDiscussionNoteSchema, CreateMergeRequestEmojiReactionSchema, CreateMergeRequestNoteEmojiReactionSchema, ListMergeRequestEmojiReactionsSchema, ListMergeRequestNoteEmojiReactionsSchema, CreateMergeRequestSchema, CreateMergeRequestThreadSchema, CreateNoteSchema, CreateCommitStatusSchema, CreateOrUpdateFileSchema, CreatePipelineSchema, CreateProjectMilestoneSchema, CreateGroupMilestoneSchema, CreateRepositorySchema, CreateGroupSchema, CreateWikiPageSchema, CreateGroupWikiPageSchema, DeleteBranchSchema, GetProtectedBranchSchema, ListProtectedBranchesSchema, ProtectBranchSchema, UnprotectBranchSchema, UpdateDefaultBranchSchema, DeleteDraftNoteSchema, DeleteGroupMilestoneSchema, DeleteGroupWikiPageSchema, DeleteIssueLinkSchema, DeleteIssueSchema, DeleteIssueEmojiReactionSchema, DeleteIssueNoteEmojiReactionSchema, DeleteLabelSchema, DeleteProjectMilestoneSchema, DeleteWikiPageSchema, DeleteMergeRequestNoteSchema, DeleteMergeRequestEmojiReactionSchema, DeleteMergeRequestNoteEmojiReactionSchema, EditProjectMilestoneSchema, EditGroupMilestoneSchema, ForkRepositorySchema, GetBranchDiffsSchema, GetBranchSchema, GetCommitDiffSchema, GetCommitSchema, GetFileBlameSchema, GitLabBlameEntrySchema, GetDraftNoteSchema, GetFileContentsSchema, GetIssueLinkSchema, GetIssueSchema, GetLabelSchema, GetMergeRequestDiffsSchema, GetMergeRequestSchema, GetMilestoneBurndownEventsSchema, GetMilestoneIssuesSchema, GetMilestoneMergeRequestsSchema, GetGroupMilestoneSchema, GetGroupMilestoneIssuesSchema, GetGroupMilestoneMergeRequestsSchema, GetGroupMilestoneBurndownEventsSchema, GetDeploymentSchema, CreateDeploymentSchema, UpdateDeploymentSchema, DeploymentApprovalSchema, ListDeploymentMergeRequestsSchema, GetEnvironmentSchema, UpdateEnvironmentSchema, StopEnvironmentSchema, StopStaleEnvironmentsSchema, DeleteReviewAppEnvironmentsSchema, PipelineTriggerIdSchema, ListPipelineTriggersSchema, CreatePipelineTriggerSchema, UpdatePipelineTriggerSchema, TriggerPipelineSchema, GetNamespaceSchema, GitLabCiLintResultSchema, GetPipelineJobOutputSchema, PipelineJobControlSchema, GetPipelineSchema, GetPipelineVariablesSchema, UpdatePipelineMetadataSchema, DeletePipelineSchema, PipelineReportSchema, WaitForPipelineSchema, WaitForPipelineJobSchema, PlayPipelineJobsSchema, GetProjectMilestoneSchema, GetProjectSchema, GetRepositoryTreeSchema, GetUsersSchema, GetUserSchema, GitLabUserFullSchema, WhoAmISchema, GitLabCurrentUserSchema, GetWikiPageSchema, GitLabCommitSchema, GitLabCommitStatusSchema, GitLabCompareResultSchema, GitLabContentSchema, GitLabCreateUpdateFileResponseSchema, GitLabDiffSchema,
117
118
  // Discussion Schemas
@@ -149,7 +150,7 @@ const MCP_PROMPTS = [
149
150
  render: (args) => `Review merge request !${args.merge_request_iid ?? "<iid>"} in project ${args.project_id ?? "<project_id>"}:\n` +
150
151
  `1. Call list_merge_request_changed_files to get the changed file list (use excluded_file_patterns for lockfiles/dist/generated files).\n` +
151
152
  `2. Call get_merge_request_file_diff in batches of 3-5 files, prioritizing source over config/tests.\n` +
152
- `3. Leave findings with create_merge_request_thread (inline, position: new_path/new_line) or create_merge_request_note (general).\n` +
153
+ `3. Leave inline findings with create_merge_request_thread (position: new_path/new_line). For a general note, call discover_tools with category "merge_requests", then create_merge_request_note.\n` +
153
154
  `4. Summarize the review verdict in a final top-level note.`,
154
155
  },
155
156
  {
@@ -179,6 +180,20 @@ const MCP_PROMPTS = [
179
180
  `3. Propose a remediation order, calling out any critical/high items with no available fix.`,
180
181
  },
181
182
  ];
183
+ /**
184
+ * Error text for a call to a tool that tools/list omits.
185
+ * Suggests `discover_tools` only when that call would add the tool.
186
+ * Regex-denied tools stay blocked, and slim-excluded tools are not activated.
187
+ */
188
+ function buildToolUnavailableMessage(toolName) {
189
+ return buildUnavailableToolMessage({
190
+ toolName,
191
+ toolset: findSmallestToolsetForTool(toolName),
192
+ deniedByRegex: GITLAB_DENIED_TOOLS_REGEX?.test(toolName) === true,
193
+ excludedBySlimProfile: isExcludedFromToolsetListing(toolName),
194
+ enabledByCurrentToolsets: isToolInEnabledToolset(toolName, enabledToolsets),
195
+ });
196
+ }
182
197
  /**
183
198
  * Create a new MCP Server instance with request handlers registered.
184
199
  * Each transport connection gets its own Server instance to prevent
@@ -186,44 +201,26 @@ const MCP_PROMPTS = [
186
201
  */
187
202
  /** Builds an MCP server with the tool list filtered by toolsets, permission mode, and policy. */
188
203
  function createServer() {
189
- // Precompute filtered tool list once at server creation (Steps 1–5 are static)
190
- // Step 1: Toolset filter — keep tools in enabled toolsets
191
- const toolsAfterToolsets = allTools.filter(tool => isToolInEnabledToolset(tool.name, enabledToolsets));
192
- // Step 2: Add GITLAB_TOOLS (individual tools bypass toolset filter)
193
- const toolsetToolNames = new Set(toolsAfterToolsets.map(t => t.name));
194
- const toolsAfterIndividual = [
195
- ...toolsAfterToolsets,
196
- ...allTools.filter(tool => individuallyEnabledTools.has(tool.name) && !toolsetToolNames.has(tool.name)),
197
- ];
198
- // Step 3: Add legacy flag overrides (USE_PIPELINE, USE_MILESTONE, USE_GITLAB_WIKI)
199
- const afterIndividualNames = new Set(toolsAfterIndividual.map(t => t.name));
200
- const toolsAfterLegacy = [
201
- ...toolsAfterIndividual,
202
- ...allTools.filter(tool => featureFlagOverrides.has(tool.name) && !afterIndividualNames.has(tool.name)),
203
- ];
204
- // Step 4: Permission mode filter (readonly / modify / full)
205
- const toolsAfterReadOnly = GITLAB_PERMISSION_MODE === "full"
206
- ? toolsAfterLegacy
207
- : toolsAfterLegacy.filter(tool => isToolAllowedByPermissionMode(tool.name));
208
- // Step 5: Regex denial filter
209
- let filteredTools = GITLAB_DENIED_TOOLS_REGEX
210
- ? toolsAfterReadOnly.filter(tool => !GITLAB_DENIED_TOOLS_REGEX.test(tool.name))
211
- : [...toolsAfterReadOnly];
212
- // Step 5.5: Always include discover_tools meta-tool (bypasses toolset filter)
213
- const discoverTool = allTools.find(t => t.name === "discover_tools");
214
- const filteredToolNames = new Set(filteredTools.map(t => t.name));
215
- if (discoverTool && !filteredToolNames.has("discover_tools")) {
216
- // Respect permission mode and regex denial filters
217
- const passesPermissionMode = isToolAllowedByPermissionMode("discover_tools");
218
- const passesRegex = !GITLAB_DENIED_TOOLS_REGEX?.test("discover_tools");
219
- if (passesPermissionMode && passesRegex) {
220
- filteredTools.push(discoverTool);
221
- }
222
- }
223
- // Step 5.7: Remove hidden policy tools
224
- if (hiddenToolSet.size > 0) {
225
- filteredTools = filteredTools.filter(tool => !hiddenToolSet.has(tool.name));
226
- }
204
+ const filteredTools = selectExposedTools({
205
+ tools: allTools,
206
+ isInEnabledToolset: toolName => isToolInEnabledToolset(toolName, enabledToolsets),
207
+ individuallyEnabledTools,
208
+ featureFlagOverrides,
209
+ isAllowedByPermissionMode: isToolAllowedByPermissionMode,
210
+ deniedToolsRegex: GITLAB_DENIED_TOOLS_REGEX,
211
+ hiddenToolNames: hiddenToolSet,
212
+ applySlimProfile,
213
+ });
214
+ const listedToolDecoration = {
215
+ readOnlyToolNames: new Set([...readOnlyTools].filter(toolName => advertisesReadOnly(toolName))),
216
+ destructiveToolNames: destructiveTools,
217
+ approveToolNames: approveToolSet,
218
+ jmespathArgument: JMESPATH_TOOL_ARGUMENT,
219
+ jmespathArgumentDescription: JMESPATH_TOOL_ARGUMENT_DESCRIPTION,
220
+ };
221
+ const listedToolsCache = { revision: 0 };
222
+ const decorateListedTools = (tools) => decorateToolsForList(tools, listedToolDecoration);
223
+ const getListedTools = createListedToolsCache(decorateListedTools);
227
224
  const mcpServer = new McpServer({
228
225
  name: SERVER_NAME,
229
226
  version: SERVER_VERSION,
@@ -262,57 +259,8 @@ function createServer() {
262
259
  };
263
260
  });
264
261
  mcpServer.server.setRequestHandler(ListToolsRequestSchema, async () => {
265
- // Step 6: Gemini $schema cleanup + annotations (only dynamic step per request)
266
- // <<< START: Remove $schema for Gemini compatibility >>>
267
- const tools = filteredTools.map(tool => {
268
- const modified = { ...tool };
269
- // Safety net: remove $schema if present (toJSONSchema strips it for zod schemas,
270
- // but manually-defined schemas like discover_tools may still have it)
271
- if (modified.inputSchema &&
272
- typeof modified.inputSchema === "object" &&
273
- modified.inputSchema !== null) {
274
- if ("$schema" in modified.inputSchema) {
275
- modified.inputSchema = { ...modified.inputSchema };
276
- delete modified.inputSchema.$schema;
277
- }
278
- }
279
- // Add MCP tool annotations
280
- modified.annotations = {
281
- ...(readOnlyTools.has(tool.name) ? { readOnlyHint: true } : {}),
282
- ...(destructiveTools.has(tool.name) ? { destructiveHint: true } : {}),
283
- ...(approveToolSet.has(tool.name) ? { confirmationHint: true } : {}),
284
- openWorldHint: true,
285
- };
286
- // Inject _confirmed optional parameter for approve-policy tools
287
- if (approveToolSet.has(tool.name) && modified.inputSchema?.properties) {
288
- modified.inputSchema = {
289
- ...modified.inputSchema,
290
- properties: {
291
- ...modified.inputSchema.properties,
292
- _confirmed: {
293
- type: "boolean",
294
- description: "Set to true to confirm execution of this approval-required tool.",
295
- },
296
- },
297
- };
298
- }
299
- if (modified.inputSchema && typeof modified.inputSchema === "object") {
300
- const properties = modified.inputSchema.properties && typeof modified.inputSchema.properties === "object"
301
- ? { ...modified.inputSchema.properties }
302
- : {};
303
- if (!(JMESPATH_TOOL_ARGUMENT in properties)) {
304
- properties[JMESPATH_TOOL_ARGUMENT] = {
305
- type: "string",
306
- description: JMESPATH_TOOL_ARGUMENT_DESCRIPTION,
307
- };
308
- modified.inputSchema = { ...modified.inputSchema, properties };
309
- }
310
- }
311
- return modified;
312
- });
313
- // <<< END: Remove $schema for Gemini compatibility >>>
314
262
  return {
315
- tools, // return tool list with $schema removed
263
+ tools: getListedTools(filteredTools, listedToolsCache.revision),
316
264
  };
317
265
  });
318
266
  mcpServer.server.setRequestHandler(CallToolRequestSchema, async (request) => {
@@ -331,9 +279,7 @@ function createServer() {
331
279
  (toolName === "download_release_asset" && !IS_REMOTE);
332
280
  const filtered = applyJmespathToToolResult(result, jmespathExpression, {
333
281
  ...completionOptions,
334
- maskValue: maskingEngine
335
- ? (value) => maskingEngine.maskValue(value)
336
- : undefined,
282
+ maskValue: maskingEngine ? (value) => maskingEngine.maskValue(value) : undefined,
337
283
  });
338
284
  const masked = maskingEngine
339
285
  ? maskingEngine.maskToolResult(filtered, {
@@ -342,7 +288,7 @@ function createServer() {
342
288
  : filtered;
343
289
  return compactMcpToolResult({
344
290
  result: masked,
345
- enabled: GITLAB_MCP_COMPACT_RESULTS,
291
+ enabled: shouldCompactToolResult(toolName, GITLAB_MCP_COMPACT_RESULTS, GITLAB_MCP_COMPACT_TOOL_NAMES),
346
292
  maxChars: GITLAB_MCP_COMPACT_RESULT_CHARS,
347
293
  toolName,
348
294
  args: readOwnRecord(request.params.arguments),
@@ -419,25 +365,34 @@ function createServer() {
419
365
  if (isToolAllowedByPermissionMode(toolName) &&
420
366
  !hiddenAndNotDenied &&
421
367
  !filteredTools.some(t => t.name === toolName)) {
422
- throw new Error(`Tool "${toolName}" is not available on this server`);
368
+ throw new Error(buildToolUnavailableMessage(toolName));
423
369
  }
424
370
  // Handle discover_tools meta-tool directly (needs access to mcpServer and filteredTools)
425
371
  if (toolName === "discover_tools") {
426
372
  const category = request.params.arguments?.category?.trim()?.toLowerCase();
427
373
  const currentToolNames = new Set(filteredTools.map(t => t.name));
374
+ const isNotActivatable = (name) => isSkippedByDiscoverActivation({
375
+ allowedByPermissionMode: isToolAllowedByPermissionMode(name),
376
+ deniedByRegex: GITLAB_DENIED_TOOLS_REGEX?.test(name) === true,
377
+ hidden: hiddenToolSet.has(name),
378
+ excludedBySlimProfile: isExcludedFromToolsetListing(name),
379
+ });
428
380
  if (!category) {
429
- // List available categories with activation status
430
- const categories = TOOLSET_DEFINITIONS.map(def => ({
431
- id: def.id,
432
- toolCount: def.tools.size,
433
- active: [...def.tools].some(t => currentToolNames.has(t)),
434
- isDefault: def.isDefault,
435
- }));
381
+ // Fully listed only. Partial overlap with `core` must stay inactive so
382
+ // clients still call discover_tools to add the missing tools. Tools this
383
+ // handler skips (slim profile, regex, permission mode, hidden) do not
384
+ // keep a toolset inactive.
385
+ const categories = listDiscoverableCategories(currentToolNames, isNotActivatable);
436
386
  return logCompletion({
437
- content: [{
387
+ content: [
388
+ {
438
389
  type: "text",
439
- text: JSON.stringify({ categories, hint: "Call discover_tools with a category name to activate it" }),
440
- }],
390
+ text: JSON.stringify({
391
+ categories,
392
+ hint: "Call discover_tools with a category name to activate it",
393
+ }),
394
+ },
395
+ ],
441
396
  });
442
397
  }
443
398
  if (!ALL_TOOLSET_IDS.has(category)) {
@@ -458,8 +413,8 @@ function createServer() {
458
413
  isError: true,
459
414
  });
460
415
  }
461
- // Check if already fully active
462
- const alreadyActive = [...toolsetDef.tools].every(t => currentToolNames.has(t));
416
+ // Same predicate as the category listing `active` flag.
417
+ const alreadyActive = isToolsetFullyActive(toolsetDef.tools, currentToolNames, isNotActivatable);
463
418
  if (alreadyActive) {
464
419
  return logCompletion({
465
420
  content: [
@@ -477,11 +432,7 @@ function createServer() {
477
432
  continue;
478
433
  if (currentToolNames.has(tool.name))
479
434
  continue;
480
- if (!isToolAllowedByPermissionMode(tool.name))
481
- continue;
482
- if (GITLAB_DENIED_TOOLS_REGEX?.test(tool.name))
483
- continue;
484
- if (hiddenToolSet.has(tool.name))
435
+ if (isNotActivatable(tool.name))
485
436
  continue;
486
437
  newTools.push(tool);
487
438
  }
@@ -496,6 +447,7 @@ function createServer() {
496
447
  });
497
448
  }
498
449
  filteredTools.push(...newTools);
450
+ listedToolsCache.revision += 1;
499
451
  // Notify client that tool list has changed
500
452
  try {
501
453
  await mcpServer.server.sendToolListChanged();
@@ -506,14 +458,16 @@ function createServer() {
506
458
  const addedNames = newTools.map(t => t.name);
507
459
  logger.info({ event: "toolset_activated", category, toolCount: addedNames.length }, `Activated toolset: ${category} (+${addedNames.length} tools)`);
508
460
  return logCompletion({
509
- content: [{
461
+ content: [
462
+ {
510
463
  type: "text",
511
464
  text: JSON.stringify({
512
465
  activated: category,
513
466
  addedTools: addedNames,
514
467
  totalTools: filteredTools.length,
515
468
  }),
516
- }],
469
+ },
470
+ ],
517
471
  });
518
472
  }
519
473
  // Check approve policy: tool is exposed but requires explicit confirmation
@@ -570,7 +524,10 @@ function createServer() {
570
524
  * Validate configuration at startup
571
525
  */
572
526
  function isLoopbackBindHost(host) {
573
- const normalized = host.trim().toLowerCase().replace(/^\[|\]$/g, "");
527
+ const normalized = host
528
+ .trim()
529
+ .toLowerCase()
530
+ .replace(/^\[|\]$/g, "");
574
531
  const isIpv4Loopback = /^127(?:\.\d{1,3}){3}$/.test(normalized);
575
532
  return (normalized === "localhost" ||
576
533
  isIpv4Loopback ||
@@ -578,7 +535,10 @@ function isLoopbackBindHost(host) {
578
535
  normalized === "0:0:0:0:0:0:0:1");
579
536
  }
580
537
  function formatHostWithPort(host, port) {
581
- const normalized = host.trim().toLowerCase().replace(/^\[|\]$/g, "");
538
+ const normalized = host
539
+ .trim()
540
+ .toLowerCase()
541
+ .replace(/^\[|\]$/g, "");
582
542
  if (!normalized || normalized === "0.0.0.0" || normalized === "::")
583
543
  return null;
584
544
  if (normalized.includes(":"))
@@ -929,6 +889,12 @@ const hiddenToolSet = new Set((GITLAB_TOOL_POLICY_HIDDEN_RAW || "")
929
889
  logger.warn({ event: "unknown_approve_tool", name }, `GITLAB_TOOL_POLICY_APPROVE contains unknown tool: "${name}"`);
930
890
  }
931
891
  }
892
+ if (GITLAB_MCP_COMPACT_TOOL_NAMES.size > 0) {
893
+ const unknownCompactTools = [...GITLAB_MCP_COMPACT_TOOL_NAMES].filter(name => !knownToolNames.has(name));
894
+ if (unknownCompactTools.length > 0) {
895
+ logger.warn(`Unknown tool names in GITLAB_MCP_COMPACT_TOOLS (will be ignored): ${unknownCompactTools.join(", ")}`);
896
+ }
897
+ }
932
898
  for (const name of hiddenToolSet) {
933
899
  if (!knownToolNames.has(name)) {
934
900
  logger.warn({ event: "unknown_hidden_tool", name }, `GITLAB_TOOL_POLICY_HIDDEN contains unknown tool: "${name}"`);
@@ -1228,6 +1194,13 @@ const downloadRedirectOptions = () => ({
1228
1194
  const enabledToolsets = parseEnabledToolsets(GITLAB_TOOLSETS_RAW);
1229
1195
  const individuallyEnabledTools = parseIndividualTools(GITLAB_TOOLS_RAW);
1230
1196
  const featureFlagOverrides = buildFeatureFlagOverrides();
1197
+ const applySlimProfile = shouldApplySlimToolProfile(GITLAB_TOOL_PROFILE, GITLAB_TOOLSETS_RAW);
1198
+ if (GITLAB_TOOL_PROFILE === "slim" && !applySlimProfile) {
1199
+ logger.warn("GITLAB_TOOL_PROFILE=slim is ignored because GITLAB_TOOLSETS is set");
1200
+ }
1201
+ function isExcludedFromToolsetListing(toolName) {
1202
+ return isExcludedBySlimProfile(toolName, applySlimProfile, individuallyEnabledTools);
1203
+ }
1231
1204
  // Warn about potentially confusing configuration
1232
1205
  if (GITLAB_TOOLSETS_RAW && (USE_PIPELINE || USE_MILESTONE || USE_GITLAB_WIKI)) {
1233
1206
  logger.warn("GITLAB_TOOLSETS is set alongside legacy flags (USE_PIPELINE, USE_MILESTONE, USE_GITLAB_WIKI). " +
@@ -1812,7 +1785,9 @@ async function resolveNamesToIds(projectPath, namespaceKind, labelNames, usernam
1812
1785
  // One alias per label — exact title match via the `title` argument, includes ancestor
1813
1786
  // group labels, single round trip with no pagination needed.
1814
1787
  const varDefs = labelNames.map((_, i) => `$l${i}: String!`).join(", ");
1815
- const aliases = labelNames.map((_, i) => `l${i}: labels(title: $l${i}, includeAncestorGroups: true, first: 1) { nodes { id } }`).join(" ");
1788
+ const aliases = labelNames
1789
+ .map((_, i) => `l${i}: labels(title: $l${i}, includeAncestorGroups: true, first: 1) { nodes { id } }`)
1790
+ .join(" ");
1816
1791
  const rootField = namespaceKind === "group" ? "group" : "project";
1817
1792
  const data = await executeGraphQL(`query($path: ID!, $usernames: [String!]!${varDefs ? `, ${varDefs}` : ""}) {
1818
1793
  ${rootField}(fullPath: $path) { ${aliases || "__typename"} }
@@ -3407,7 +3382,9 @@ async function updateIssueNote(projectId, issueIid, discussionId, noteId, body,
3407
3382
  async function createIssueNote(projectId, issueIid, discussionId, body, createdAt) {
3408
3383
  projectId = decodeURIComponent(projectId); // Decode project ID
3409
3384
  const basePath = `${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/issues/${encodeGitLabPathSegment(issueIid)}`;
3410
- const url = new URL(discussionId ? `${basePath}/discussions/${encodeGitLabPathSegment(discussionId)}/notes` : `${basePath}/notes`);
3385
+ const url = new URL(discussionId
3386
+ ? `${basePath}/discussions/${encodeGitLabPathSegment(discussionId)}/notes`
3387
+ : `${basePath}/notes`);
3411
3388
  const payload = { body };
3412
3389
  if (createdAt) {
3413
3390
  payload.created_at = createdAt;
@@ -5380,7 +5357,8 @@ async function deploymentRequest(projectId, path = "", method = "GET", body, que
5380
5357
  url.searchParams.set(key, String(value));
5381
5358
  }
5382
5359
  const response = await fetch(url.toString(), {
5383
- ...getFetchConfig(), method,
5360
+ ...getFetchConfig(),
5361
+ method,
5384
5362
  body: body === undefined ? undefined : JSON.stringify(body),
5385
5363
  });
5386
5364
  await handleGitLabError(response);
@@ -5430,7 +5408,11 @@ async function environmentRequest(projectId, path, method, body, query) {
5430
5408
  for (const [key, value] of Object.entries(query ?? {}))
5431
5409
  if (value !== undefined)
5432
5410
  url.searchParams.set(key, String(value));
5433
- const response = await fetch(url.toString(), { ...getFetchConfig(), method, body: body === undefined ? undefined : JSON.stringify(body) });
5411
+ const response = await fetch(url.toString(), {
5412
+ ...getFetchConfig(),
5413
+ method,
5414
+ body: body === undefined ? undefined : JSON.stringify(body),
5415
+ });
5434
5416
  await handleGitLabError(response);
5435
5417
  return response.status === 204 ? { success: true } : response.json();
5436
5418
  }
@@ -5450,7 +5432,11 @@ async function triggerPipeline(projectId, token, ref, variables, inputs) {
5450
5432
  body.variables = variables;
5451
5433
  if (inputs)
5452
5434
  body.inputs = inputs;
5453
- const response = await fetch(url.toString(), { ...getFetchConfig(), method: "POST", body: Object.keys(body).length ? JSON.stringify(body) : undefined });
5435
+ const response = await fetch(url.toString(), {
5436
+ ...getFetchConfig(),
5437
+ method: "POST",
5438
+ body: Object.keys(body).length ? JSON.stringify(body) : undefined,
5439
+ });
5454
5440
  await handleGitLabError(response);
5455
5441
  return response.json();
5456
5442
  }
@@ -8021,8 +8007,7 @@ async function handleToolCall(params) {
8021
8007
  }
8022
8008
  case "push_files": {
8023
8009
  const args = PushFilesSchema.parse(params.arguments);
8024
- if (GITLAB_PERMISSION_MODE === "modify" &&
8025
- fileOperationsIncludeDeleteOrMove(args.files)) {
8010
+ if (GITLAB_PERMISSION_MODE === "modify" && fileOperationsIncludeDeleteOrMove(args.files)) {
8026
8011
  throw new Error("push_files does not allow delete or move actions in modify mode");
8027
8012
  }
8028
8013
  const result = await createCommit(args.project_id, args.commit_message, args.branch, args.files.map(f => ({
@@ -8893,7 +8878,14 @@ async function handleToolCall(params) {
8893
8878
  const args = DeleteWorkItemEmojiReactionSchema.parse(params.arguments);
8894
8879
  const { workItemGID } = await resolveWorkItemGID(args.project_id, args.iid);
8895
8880
  const result = await removeGraphQLAwardEmoji(workItemGID, args.name);
8896
- return { content: [{ type: "text", text: JSON.stringify(result ?? { status: "success", message: "Work item emoji reaction removed" }) }] };
8881
+ return {
8882
+ content: [
8883
+ {
8884
+ type: "text",
8885
+ text: JSON.stringify(result ?? { status: "success", message: "Work item emoji reaction removed" }),
8886
+ },
8887
+ ],
8888
+ };
8897
8889
  }
8898
8890
  case "create_work_item_note_emoji_reaction": {
8899
8891
  const args = CreateWorkItemNoteEmojiReactionSchema.parse(params.arguments);
@@ -8903,7 +8895,14 @@ async function handleToolCall(params) {
8903
8895
  case "delete_work_item_note_emoji_reaction": {
8904
8896
  const args = DeleteWorkItemNoteEmojiReactionSchema.parse(params.arguments);
8905
8897
  const result = await removeGraphQLAwardEmoji(args.note_id, args.name);
8906
- return { content: [{ type: "text", text: JSON.stringify(result ?? { status: "success", message: "Work item note emoji reaction removed" }) }] };
8898
+ return {
8899
+ content: [
8900
+ {
8901
+ type: "text",
8902
+ text: JSON.stringify(result ?? { status: "success", message: "Work item note emoji reaction removed" }),
8903
+ },
8904
+ ],
8905
+ };
8907
8906
  }
8908
8907
  case "get_timeline_events": {
8909
8908
  const args = GetTimelineEventsSchema.parse(params.arguments);
@@ -9106,23 +9105,55 @@ async function handleToolCall(params) {
9106
9105
  }
9107
9106
  case "get_pipeline_variables": {
9108
9107
  const { project_id, pipeline_id, page, per_page } = GetPipelineVariablesSchema.parse(params.arguments);
9109
- return { content: [{ type: "text", text: JSON.stringify(await getPipelineVariables(project_id, pipeline_id, page, per_page)) }] };
9108
+ return {
9109
+ content: [
9110
+ {
9111
+ type: "text",
9112
+ text: JSON.stringify(await getPipelineVariables(project_id, pipeline_id, page, per_page)),
9113
+ },
9114
+ ],
9115
+ };
9110
9116
  }
9111
9117
  case "get_pipeline_test_report": {
9112
9118
  const { project_id, pipeline_id, page, per_page } = PipelineReportSchema.parse(params.arguments);
9113
- return { content: [{ type: "text", text: JSON.stringify(await getPipelineReport(project_id, pipeline_id, false, page, per_page)) }] };
9119
+ return {
9120
+ content: [
9121
+ {
9122
+ type: "text",
9123
+ text: JSON.stringify(await getPipelineReport(project_id, pipeline_id, false, page, per_page)),
9124
+ },
9125
+ ],
9126
+ };
9114
9127
  }
9115
9128
  case "get_pipeline_test_report_summary": {
9116
9129
  const { project_id, pipeline_id, page, per_page } = PipelineReportSchema.parse(params.arguments);
9117
- return { content: [{ type: "text", text: JSON.stringify(await getPipelineReport(project_id, pipeline_id, true, page, per_page)) }] };
9130
+ return {
9131
+ content: [
9132
+ {
9133
+ type: "text",
9134
+ text: JSON.stringify(await getPipelineReport(project_id, pipeline_id, true, page, per_page)),
9135
+ },
9136
+ ],
9137
+ };
9118
9138
  }
9119
9139
  case "delete_pipeline": {
9120
9140
  const { project_id, pipeline_id } = DeletePipelineSchema.parse(params.arguments);
9121
- return { content: [{ type: "text", text: JSON.stringify(await deletePipeline(project_id, pipeline_id)) }] };
9141
+ return {
9142
+ content: [
9143
+ { type: "text", text: JSON.stringify(await deletePipeline(project_id, pipeline_id)) },
9144
+ ],
9145
+ };
9122
9146
  }
9123
9147
  case "update_pipeline_metadata": {
9124
9148
  const { project_id, pipeline_id, name } = UpdatePipelineMetadataSchema.parse(params.arguments);
9125
- return { content: [{ type: "text", text: JSON.stringify(await updatePipelineMetadata(project_id, pipeline_id, name)) }] };
9149
+ return {
9150
+ content: [
9151
+ {
9152
+ type: "text",
9153
+ text: JSON.stringify(await updatePipelineMetadata(project_id, pipeline_id, name)),
9154
+ },
9155
+ ],
9156
+ };
9126
9157
  }
9127
9158
  case "list_deployments": {
9128
9159
  const args = ListDeploymentsSchema.parse(params.arguments);
@@ -9141,23 +9172,58 @@ async function handleToolCall(params) {
9141
9172
  }
9142
9173
  case "create_deployment": {
9143
9174
  const { project_id, ...body } = CreateDeploymentSchema.parse(params.arguments);
9144
- return { content: [{ type: "text", text: JSON.stringify(await deploymentRequest(project_id, "", "POST", body)) }] };
9175
+ return {
9176
+ content: [
9177
+ {
9178
+ type: "text",
9179
+ text: JSON.stringify(await deploymentRequest(project_id, "", "POST", body)),
9180
+ },
9181
+ ],
9182
+ };
9145
9183
  }
9146
9184
  case "update_deployment": {
9147
9185
  const { project_id, deployment_id, ...body } = UpdateDeploymentSchema.parse(params.arguments);
9148
- return { content: [{ type: "text", text: JSON.stringify(await deploymentRequest(project_id, `/${encodeGitLabPathSegment(deployment_id)}`, "PUT", body)) }] };
9186
+ return {
9187
+ content: [
9188
+ {
9189
+ type: "text",
9190
+ text: JSON.stringify(await deploymentRequest(project_id, `/${encodeGitLabPathSegment(deployment_id)}`, "PUT", body)),
9191
+ },
9192
+ ],
9193
+ };
9149
9194
  }
9150
9195
  case "delete_deployment": {
9151
9196
  const { project_id, deployment_id } = GetDeploymentSchema.parse(params.arguments);
9152
- return { content: [{ type: "text", text: JSON.stringify(await deploymentRequest(project_id, `/${encodeGitLabPathSegment(deployment_id)}`, "DELETE")) }] };
9197
+ return {
9198
+ content: [
9199
+ {
9200
+ type: "text",
9201
+ text: JSON.stringify(await deploymentRequest(project_id, `/${encodeGitLabPathSegment(deployment_id)}`, "DELETE")),
9202
+ },
9203
+ ],
9204
+ };
9153
9205
  }
9154
9206
  case "list_deployment_merge_requests": {
9155
9207
  const { project_id, deployment_id, ...query } = ListDeploymentMergeRequestsSchema.parse(params.arguments);
9156
- return { content: [{ type: "text", text: JSON.stringify(await deploymentRequest(project_id, `/${encodeGitLabPathSegment(deployment_id)}/merge_requests`, "GET", undefined, query)) }] };
9208
+ return {
9209
+ content: [
9210
+ {
9211
+ type: "text",
9212
+ text: JSON.stringify(await deploymentRequest(project_id, `/${encodeGitLabPathSegment(deployment_id)}/merge_requests`, "GET", undefined, query)),
9213
+ },
9214
+ ],
9215
+ };
9157
9216
  }
9158
9217
  case "approve_deployment": {
9159
9218
  const { project_id, deployment_id, ...body } = DeploymentApprovalSchema.parse(params.arguments);
9160
- return { content: [{ type: "text", text: JSON.stringify(await deploymentRequest(project_id, `/${encodeGitLabPathSegment(deployment_id)}/approval`, "POST", body)) }] };
9219
+ return {
9220
+ content: [
9221
+ {
9222
+ type: "text",
9223
+ text: JSON.stringify(await deploymentRequest(project_id, `/${encodeGitLabPathSegment(deployment_id)}/approval`, "POST", body)),
9224
+ },
9225
+ ],
9226
+ };
9161
9227
  }
9162
9228
  case "list_environments": {
9163
9229
  const args = ListEnvironmentsSchema.parse(params.arguments);
@@ -9176,47 +9242,124 @@ async function handleToolCall(params) {
9176
9242
  }
9177
9243
  case "update_environment": {
9178
9244
  const { project_id, environment_id, ...body } = UpdateEnvironmentSchema.parse(params.arguments);
9179
- return { content: [{ type: "text", text: JSON.stringify(await environmentRequest(project_id, `/${encodeGitLabPathSegment(environment_id)}`, "PUT", body)) }] };
9245
+ return {
9246
+ content: [
9247
+ {
9248
+ type: "text",
9249
+ text: JSON.stringify(await environmentRequest(project_id, `/${encodeGitLabPathSegment(environment_id)}`, "PUT", body)),
9250
+ },
9251
+ ],
9252
+ };
9180
9253
  }
9181
9254
  case "delete_environment": {
9182
9255
  const { project_id, environment_id } = GetEnvironmentSchema.parse(params.arguments);
9183
- return { content: [{ type: "text", text: JSON.stringify(await environmentRequest(project_id, `/${encodeGitLabPathSegment(environment_id)}`, "DELETE")) }] };
9256
+ return {
9257
+ content: [
9258
+ {
9259
+ type: "text",
9260
+ text: JSON.stringify(await environmentRequest(project_id, `/${encodeGitLabPathSegment(environment_id)}`, "DELETE")),
9261
+ },
9262
+ ],
9263
+ };
9184
9264
  }
9185
9265
  case "stop_environment": {
9186
9266
  const { project_id, environment_id, force } = StopEnvironmentSchema.parse(params.arguments);
9187
- return { content: [{ type: "text", text: JSON.stringify(await environmentRequest(project_id, `/${encodeGitLabPathSegment(environment_id)}/stop`, "POST", undefined, { force })) }] };
9267
+ return {
9268
+ content: [
9269
+ {
9270
+ type: "text",
9271
+ text: JSON.stringify(await environmentRequest(project_id, `/${encodeGitLabPathSegment(environment_id)}/stop`, "POST", undefined, { force })),
9272
+ },
9273
+ ],
9274
+ };
9188
9275
  }
9189
9276
  case "stop_stale_environments": {
9190
9277
  const { project_id, before } = StopStaleEnvironmentsSchema.parse(params.arguments);
9191
- return { content: [{ type: "text", text: JSON.stringify(await environmentRequest(project_id, "/stop_stale", "POST", undefined, { before })) }] };
9278
+ return {
9279
+ content: [
9280
+ {
9281
+ type: "text",
9282
+ text: JSON.stringify(await environmentRequest(project_id, "/stop_stale", "POST", undefined, { before })),
9283
+ },
9284
+ ],
9285
+ };
9192
9286
  }
9193
9287
  case "delete_review_app_environments": {
9194
9288
  const { project_id, ...query } = DeleteReviewAppEnvironmentsSchema.parse(params.arguments);
9195
- return { content: [{ type: "text", text: JSON.stringify(await environmentRequest(project_id, "/review_apps", "DELETE", undefined, query)) }] };
9289
+ return {
9290
+ content: [
9291
+ {
9292
+ type: "text",
9293
+ text: JSON.stringify(await environmentRequest(project_id, "/review_apps", "DELETE", undefined, query)),
9294
+ },
9295
+ ],
9296
+ };
9196
9297
  }
9197
9298
  case "list_pipeline_triggers": {
9198
9299
  const { project_id } = ListPipelineTriggersSchema.parse(params.arguments);
9199
- return { content: [{ type: "text", text: JSON.stringify(redactSensitiveGitLabFields(await pipelineTriggerRequest(project_id))) }] };
9300
+ return {
9301
+ content: [
9302
+ {
9303
+ type: "text",
9304
+ text: JSON.stringify(redactSensitiveGitLabFields(await pipelineTriggerRequest(project_id))),
9305
+ },
9306
+ ],
9307
+ };
9200
9308
  }
9201
9309
  case "get_pipeline_trigger": {
9202
9310
  const { project_id, trigger_id } = PipelineTriggerIdSchema.parse(params.arguments);
9203
- return { content: [{ type: "text", text: JSON.stringify(redactSensitiveGitLabFields(await pipelineTriggerRequest(project_id, `/${encodeGitLabPathSegment(trigger_id)}`))) }] };
9311
+ return {
9312
+ content: [
9313
+ {
9314
+ type: "text",
9315
+ text: JSON.stringify(redactSensitiveGitLabFields(await pipelineTriggerRequest(project_id, `/${encodeGitLabPathSegment(trigger_id)}`))),
9316
+ },
9317
+ ],
9318
+ };
9204
9319
  }
9205
9320
  case "create_pipeline_trigger": {
9206
9321
  const { project_id, ...body } = CreatePipelineTriggerSchema.parse(params.arguments);
9207
- return { content: [{ type: "text", text: JSON.stringify(await pipelineTriggerRequest(project_id, "", "POST", body)) }] };
9322
+ return {
9323
+ content: [
9324
+ {
9325
+ type: "text",
9326
+ text: JSON.stringify(await pipelineTriggerRequest(project_id, "", "POST", body)),
9327
+ },
9328
+ ],
9329
+ };
9208
9330
  }
9209
9331
  case "update_pipeline_trigger": {
9210
9332
  const { project_id, trigger_id, ...body } = UpdatePipelineTriggerSchema.parse(params.arguments);
9211
- return { content: [{ type: "text", text: JSON.stringify(redactSensitiveGitLabFields(await pipelineTriggerRequest(project_id, `/${encodeGitLabPathSegment(trigger_id)}`, "PUT", body))) }] };
9333
+ return {
9334
+ content: [
9335
+ {
9336
+ type: "text",
9337
+ text: JSON.stringify(redactSensitiveGitLabFields(await pipelineTriggerRequest(project_id, `/${encodeGitLabPathSegment(trigger_id)}`, "PUT", body))),
9338
+ },
9339
+ ],
9340
+ };
9212
9341
  }
9213
9342
  case "delete_pipeline_trigger": {
9214
9343
  const { project_id, trigger_id } = PipelineTriggerIdSchema.parse(params.arguments);
9215
- return { content: [{ type: "text", text: JSON.stringify(await pipelineTriggerRequest(project_id, `/${encodeGitLabPathSegment(trigger_id)}`, "DELETE")) }] };
9344
+ return {
9345
+ content: [
9346
+ {
9347
+ type: "text",
9348
+ text: JSON.stringify(await pipelineTriggerRequest(project_id, `/${encodeGitLabPathSegment(trigger_id)}`, "DELETE")),
9349
+ },
9350
+ ],
9351
+ };
9216
9352
  }
9217
9353
  case "trigger_pipeline": {
9218
9354
  const { project_id, token, ref, variables, inputs } = TriggerPipelineSchema.parse(params.arguments);
9219
- return { content: [{ type: "text", text: JSON.stringify(await triggerPipeline(project_id, token, ref, variables, inputs)) }] };
9355
+ return {
9356
+ content: [
9357
+ {
9358
+ type: "text",
9359
+ text: JSON.stringify(await triggerPipeline(project_id, token, ref, variables, inputs)),
9360
+ },
9361
+ ],
9362
+ };
9220
9363
  }
9221
9364
  case "list_pipeline_jobs": {
9222
9365
  const { project_id, pipeline_id, ...options } = ListPipelineJobsSchema.parse(params.arguments);
@@ -9583,7 +9726,7 @@ async function handleToolCall(params) {
9583
9726
  };
9584
9727
  }
9585
9728
  case "play_pipeline_jobs": {
9586
- const { project_id, job_ids, job_variables_attributes, timeout_seconds, poll_interval_seconds } = PlayPipelineJobsSchema.parse(params.arguments);
9729
+ const { project_id, job_ids, job_variables_attributes, timeout_seconds, poll_interval_seconds, } = PlayPipelineJobsSchema.parse(params.arguments);
9587
9730
  const jobs = await playPipelineJobs(project_id, job_ids, job_variables_attributes, timeout_seconds, poll_interval_seconds);
9588
9731
  return { content: [{ type: "text", text: JSON.stringify(jobs) }] };
9589
9732
  }
@@ -9613,7 +9756,11 @@ async function handleToolCall(params) {
9613
9756
  }
9614
9757
  case "erase_pipeline_job": {
9615
9758
  const { project_id, job_id } = ErasePipelineJobSchema.parse(params.arguments);
9616
- return { content: [{ type: "text", text: JSON.stringify(await erasePipelineJob(project_id, job_id)) }] };
9759
+ return {
9760
+ content: [
9761
+ { type: "text", text: JSON.stringify(await erasePipelineJob(project_id, job_id)) },
9762
+ ],
9763
+ };
9617
9764
  }
9618
9765
  case "wait_for_pipeline": {
9619
9766
  const { project_id, pipeline_id, timeout_seconds, poll_interval_seconds } = WaitForPipelineSchema.parse(params.arguments);
@@ -9645,7 +9792,15 @@ async function handleToolCall(params) {
9645
9792
  }
9646
9793
  const downloadUrl = buildDownloadUrl("job-artifacts", { project_id, job_id });
9647
9794
  return {
9648
- content: [{ type: "text", text: JSON.stringify({ download_url: downloadUrl, filename: `artifacts_job_${job_id}.zip` }) }],
9795
+ content: [
9796
+ {
9797
+ type: "text",
9798
+ text: JSON.stringify({
9799
+ download_url: downloadUrl,
9800
+ filename: `artifacts_job_${job_id}.zip`,
9801
+ }),
9802
+ },
9803
+ ],
9649
9804
  };
9650
9805
  }
9651
9806
  const filePath = await downloadJobArtifacts(project_id, job_id, local_path);
@@ -10148,7 +10303,12 @@ async function handleToolCall(params) {
10148
10303
  filename: args.filename,
10149
10304
  });
10150
10305
  return {
10151
- content: [{ type: "text", text: JSON.stringify({ download_url: downloadUrl, filename: args.filename }) }],
10306
+ content: [
10307
+ {
10308
+ type: "text",
10309
+ text: JSON.stringify({ download_url: downloadUrl, filename: args.filename }),
10310
+ },
10311
+ ],
10152
10312
  };
10153
10313
  }
10154
10314
  const result = await downloadAttachment(args.project_id, args.secret, args.filename, args.local_path);
@@ -10254,7 +10414,15 @@ async function handleToolCall(params) {
10254
10414
  direct_asset_path: args.direct_asset_path,
10255
10415
  });
10256
10416
  return {
10257
- content: [{ type: "text", text: JSON.stringify({ download_url: downloadUrl, filename: args.direct_asset_path.split("/").pop() || args.direct_asset_path }) }],
10417
+ content: [
10418
+ {
10419
+ type: "text",
10420
+ text: JSON.stringify({
10421
+ download_url: downloadUrl,
10422
+ filename: args.direct_asset_path.split("/").pop() || args.direct_asset_path,
10423
+ }),
10424
+ },
10425
+ ],
10258
10426
  };
10259
10427
  }
10260
10428
  const assetContent = await downloadReleaseAsset(args.project_id, args.tag_name, args.direct_asset_path);
@@ -10445,9 +10613,7 @@ async function handleToolCall(params) {
10445
10613
  const jobResponse = await fetch(jobUrl.toString(), getFetchConfig());
10446
10614
  authenticated = jobResponse.ok;
10447
10615
  }
10448
- const versionMetadata = authenticated
10449
- ? await fetchGitLabVersionMetadata()
10450
- : null;
10616
+ const versionMetadata = authenticated ? await fetchGitLabVersionMetadata() : null;
10451
10617
  return {
10452
10618
  content: [
10453
10619
  {
@@ -10510,7 +10676,9 @@ async function handleToolCall(params) {
10510
10676
  });
10511
10677
  await handleGitLabError(response);
10512
10678
  return {
10513
- content: [{ type: "text", text: JSON.stringify({ status: "deleted", branch: args.branch_name }) }],
10679
+ content: [
10680
+ { type: "text", text: JSON.stringify({ status: "deleted", branch: args.branch_name }) },
10681
+ ],
10514
10682
  };
10515
10683
  }
10516
10684
  case "list_protected_branches": {
@@ -10585,7 +10753,12 @@ async function handleToolCall(params) {
10585
10753
  });
10586
10754
  await handleGitLabError(response);
10587
10755
  return {
10588
- content: [{ type: "text", text: JSON.stringify({ status: "unprotected", branch: args.branch_name }) }],
10756
+ content: [
10757
+ {
10758
+ type: "text",
10759
+ text: JSON.stringify({ status: "unprotected", branch: args.branch_name }),
10760
+ },
10761
+ ],
10589
10762
  };
10590
10763
  }
10591
10764
  case "update_default_branch": {
@@ -10601,7 +10774,16 @@ async function handleToolCall(params) {
10601
10774
  await handleGitLabError(response);
10602
10775
  const data = await response.json();
10603
10776
  return {
10604
- content: [{ type: "text", text: JSON.stringify({ status: "updated", default_branch: args.default_branch, project: data }) }],
10777
+ content: [
10778
+ {
10779
+ type: "text",
10780
+ text: JSON.stringify({
10781
+ status: "updated",
10782
+ default_branch: args.default_branch,
10783
+ project: data,
10784
+ }),
10785
+ },
10786
+ ],
10605
10787
  };
10606
10788
  }
10607
10789
  default:
@@ -10656,6 +10838,7 @@ function buildDownloadProxyDeps() {
10656
10838
  * Start server with traditional SSE transport
10657
10839
  */
10658
10840
  async function startSSEServer() {
10841
+ const { express, rateLimit, ipKeyGenerator, SSEServerTransport } = await loadRemoteHttpStack();
10659
10842
  const app = express();
10660
10843
  const sseAuthToken = getConfig("sse-auth-token", "SSE_AUTH_TOKEN");
10661
10844
  if (MCP_TRUST_PROXY) {
@@ -10692,7 +10875,8 @@ async function startSSEServer() {
10692
10875
  const host = req.headers.host;
10693
10876
  const forwardedHost = MCP_TRUST_PROXY ? getForwardedRequestHost(req, true) : undefined;
10694
10877
  const requestHosts = [host, forwardedHost].filter((value) => Boolean(value));
10695
- if (requestHosts.length === 0 || !requestHosts.some(requestHost => effectiveHosts.has(requestHost))) {
10878
+ if (requestHosts.length === 0 ||
10879
+ !requestHosts.some(requestHost => effectiveHosts.has(requestHost))) {
10696
10880
  res.status(403).json({ error: "Invalid Host header" });
10697
10881
  return;
10698
10882
  }
@@ -10867,6 +11051,7 @@ async function startSSEServer() {
10867
11051
  * Start server with Streamable HTTP transport
10868
11052
  */
10869
11053
  async function startStreamableHTTPServer() {
11054
+ const { express, rateLimit, ipKeyGenerator, StreamableHTTPServerTransport, mcpAuthRouter, requireBearerAuth, } = await loadRemoteHttpStack();
10870
11055
  const app = express();
10871
11056
  // Session IDs are untrusted map keys. A null prototype prevents inherited
10872
11057
  // names such as "constructor" from masquerading as live transports.
@@ -11399,9 +11584,7 @@ async function startStreamableHTTPServer() {
11399
11584
  // ipKeyGenerator for correct IPv6 subnet handling.
11400
11585
  const rateLimitOptions = { keyGenerator: mcpRateLimitKeyGenerator };
11401
11586
  const parsedRegisterLimit = Number(process.env.OAUTH_REGISTER_RATE_LIMIT_PER_HOUR);
11402
- const OAUTH_REGISTER_RATE_LIMIT_PER_HOUR = Number.isInteger(parsedRegisterLimit) && parsedRegisterLimit >= 1
11403
- ? parsedRegisterLimit
11404
- : 20;
11587
+ const OAUTH_REGISTER_RATE_LIMIT_PER_HOUR = Number.isInteger(parsedRegisterLimit) && parsedRegisterLimit >= 1 ? parsedRegisterLimit : 20;
11405
11588
  const clientRegistrationRateLimitOptions = {
11406
11589
  ...rateLimitOptions,
11407
11590
  windowMs: 60 * 60 * 1000, // 1 hour
@@ -11501,9 +11684,7 @@ async function startStreamableHTTPServer() {
11501
11684
  ? staticMcpBearerAuth
11502
11685
  : (_req, _res, next) => next();
11503
11686
  const rejectUnknownStatefulSession = (req, res, next) => {
11504
- const usesStatelessSessions = OAUTH_STATELESS_MODE &&
11505
- STATELESS_MATERIAL &&
11506
- (REMOTE_AUTHORIZATION || GITLAB_MCP_OAUTH);
11687
+ const usesStatelessSessions = OAUTH_STATELESS_MODE && STATELESS_MATERIAL && (REMOTE_AUTHORIZATION || GITLAB_MCP_OAUTH);
11507
11688
  const sessionId = readMcpSessionIdHeader(req);
11508
11689
  if (!usesStatelessSessions && sessionId && !streamableTransports[sessionId]) {
11509
11690
  res.status(404).json({ error: "Session not found" });
@@ -11521,7 +11702,9 @@ async function startStreamableHTTPServer() {
11521
11702
  // entirely and derive the session auth from either the current request
11522
11703
  // headers (init) or a sealed Mcp-Session-Id (subsequent requests).
11523
11704
  // Rate limiting is disabled here because there is no shared counter.
11524
- if (OAUTH_STATELESS_MODE && STATELESS_MATERIAL && (REMOTE_AUTHORIZATION || GITLAB_MCP_OAUTH)) {
11705
+ if (OAUTH_STATELESS_MODE &&
11706
+ STATELESS_MATERIAL &&
11707
+ (REMOTE_AUTHORIZATION || GITLAB_MCP_OAUTH)) {
11525
11708
  await handleStatelessMcpRequest(req, res, STATELESS_MATERIAL, OAUTH_STATELESS_SESSION_TTL_SECONDS);
11526
11709
  return;
11527
11710
  }
@@ -11802,7 +11985,9 @@ async function startStreamableHTTPServer() {
11802
11985
  });
11803
11986
  return;
11804
11987
  }
11805
- if (OAUTH_STATELESS_MODE && STATELESS_MATERIAL && (REMOTE_AUTHORIZATION || GITLAB_MCP_OAUTH)) {
11988
+ if (OAUTH_STATELESS_MODE &&
11989
+ STATELESS_MATERIAL &&
11990
+ (REMOTE_AUTHORIZATION || GITLAB_MCP_OAUTH)) {
11806
11991
  res.setHeader("Allow", "POST");
11807
11992
  res.status(405).json({
11808
11993
  error: "Method Not Allowed",
@@ -12059,16 +12244,16 @@ async function runServer() {
12059
12244
  }
12060
12245
  logger.info(`Configured GitLab API URLs: ${GITLAB_API_URLS.join(", ")}`);
12061
12246
  logger.info(`Default GitLab API URL: ${GITLAB_API_URL}`);
12062
- if (GITLAB_ALLOWED_GROUPS_RAW) {
12063
- if (GITLAB_OAUTH_ALLOWED_GROUPS_RAW) {
12064
- logger.warn("GITLAB_ALLOWED_GROUPS is set but ignored — GITLAB_OAUTH_ALLOWED_GROUPS takes precedence.");
12065
- }
12066
- else {
12067
- logger.warn("GITLAB_ALLOWED_GROUPS is deprecated. Use GITLAB_OAUTH_ALLOWED_GROUPS instead.");
12068
- }
12069
- }
12070
- if (GITLAB_READ_ONLY_MODE) {
12071
- logger.warn("GITLAB_READ_ONLY_MODE is deprecated. Use GITLAB_PERMISSION_MODE=readonly or --permission-mode=readonly instead.");
12247
+ for (const warning of getDeprecatedEnvWarnings({
12248
+ readOnlyMode: GITLAB_READ_ONLY_MODE,
12249
+ permissionModeRaw: GITLAB_PERMISSION_MODE_RAW,
12250
+ allowedGroupsRaw: GITLAB_ALLOWED_GROUPS_RAW,
12251
+ oauthAllowedGroupsRaw: GITLAB_OAUTH_ALLOWED_GROUPS_RAW,
12252
+ useWikiRaw: USE_GITLAB_WIKI_RAW,
12253
+ useMilestoneRaw: USE_MILESTONE_RAW,
12254
+ usePipelineRaw: USE_PIPELINE_RAW,
12255
+ })) {
12256
+ logger.warn(warning);
12072
12257
  }
12073
12258
  if (GITLAB_OAUTH_ALLOWED_GROUPS) {
12074
12259
  logger.info(`Group access control enabled for: ${GITLAB_OAUTH_ALLOWED_GROUPS.join(", ")}`);