@zereight/mcp-gitlab 2.2.1 → 2.2.3

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, 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";
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_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, GITLAB_MCP_COMPACT_RESULTS, GITLAB_MCP_COMPACT_RESULT_CHARS, GITLAB_MCP_COMPACT_TOOL_NAMES, GITLAB_TOOL_PROFILE, buildDeprecatedEnvInput, } from "./config.js";
3
3
  import { getDeprecatedEnvWarnings } from "./deprecated-env.js";
4
4
  /** True when the server is running in remote/network mode (SSE or StreamableHTTP transport). */
5
5
  const IS_REMOTE = SSE || STREAMABLE_HTTP;
@@ -97,6 +97,8 @@ import { graphqlQueryContainsWriteOperation, graphqlQueryContainsDeleteOperation
97
97
  import { resolveNestedWikiUpdateTitle } from "./utils/wiki-title.js";
98
98
  import { encodeRepoFilePayloadContent, fileOperationsIncludeDeleteOrMove, toGitLabCommitActions, } from "./utils/gitlab-commit-actions.js";
99
99
  import { redactSensitiveGitLabFields } from "./utils/redact-sensitive.js";
100
+ import { classifyHealthCheckTransportError, redactGitLabUrlCredentials, } from "./utils/health-check-transport-error.js";
101
+ import { redactUrlSecretsInText } from "./utils/redact-url.js";
100
102
  import { createMaskingPolicyResolver, getManagedMaskingProjectIds, } from "./masking/index.js";
101
103
  import { checkForNewVersion } from "./utils/version-check.js";
102
104
  import { assertGitLabVersionAtLeast } from "./utils/gitlab-version-gate.js";
@@ -119,11 +121,15 @@ CreateMergeRequestNoteSchema, CreateMergeRequestDiscussionNoteSchema, CreateMerg
119
121
  GitLabDiscussionNoteSchema, // Added
120
122
  GitLabDiscussionSchema,
121
123
  // Draft Notes Schemas
122
- GitLabDraftNoteSchema, GitLabForkSchema, GitLabBranchSchema, GitLabProtectedBranchSchema, GitLabGroupSchema, GitLabIssueLinkSchema, GitLabIssueSchema, GitLabIssueWithLinkDetailsSchema, GitLabMarkdownUploadSchema, GitLabMergeRequestPipelineSchema, GitLabMergeRequestSchema, GitLabMilestonesSchema, GitLabGroupMilestonesSchema, GitLabNamespaceExistsResponseSchema, GitLabNamespaceSchema, GitLabPipelineJobSchema, GitLabDeploymentSchema, GitLabEnvironmentSchema, GitLabPipelineSchema, GitLabPipelineScheduleSchema, GitLabPipelineScheduleVariableSchema, GitLabPipelineSchedulePlayResultSchema, ListPipelineSchedulesSchema, GetPipelineScheduleSchema, ListPipelineSchedulePipelinesSchema, CreatePipelineScheduleSchema, UpdatePipelineScheduleSchema, DeletePipelineScheduleSchema, PlayPipelineScheduleSchema, TakeOwnershipPipelineScheduleSchema, GetPipelineScheduleVariableSchema, CreatePipelineScheduleVariableSchema, UpdatePipelineScheduleVariableSchema, DeletePipelineScheduleVariableSchema, 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, ListProjectVulnerabilitiesSchema, GetVulnerabilitySchema, DismissVulnerabilitySchema, ConfirmVulnerabilitySchema, ListIssueDiscussionsSchema, ListIssueLinksSchema, ListIssuesSchema, ListTodosSchema, ListLabelsSchema, ListMergeRequestDiffsSchema, // Added
123
- GetMergeRequestFileDiffSchema, ListMergeRequestChangedFilesSchema, ListMergeRequestDiscussionsSchema, ListMergeRequestPipelinesSchema, ListMergeRequestsSchema, ListGroupMergeRequestsSchema, ListMergeRequestVersionsSchema, GetMergeRequestVersionSchema, GitLabMergeRequestVersionSchema, GitLabMergeRequestVersionDetailSchema, ListNamespacesSchema, ListPipelineJobsSchema, ListPipelinesSchema, ListDeploymentsSchema, ListEnvironmentsSchema, ListPipelineTriggerJobsSchema, ValidateCiLintSchema, ValidateProjectCiLintSchema, ListCiCatalogResourcesSchema, GetCiCatalogResourceSchema, ListProjectMembersSchema, ListGroupMembersSchema, ListProjectMilestonesSchema, ListGroupMilestonesSchema, ListProjectsSchema, ListWikiPagesSchema, GetGroupWikiPageSchema, ListGroupWikiPagesSchema, UpdateGroupWikiPageSchema, MarkdownUploadSchema, MarkdownUploadRemoteSchema, DownloadAttachmentSchema, DownloadJobArtifactsSchema, GetJobArtifactFileSchema, GitLabArtifactEntrySchema, ListJobArtifactsSchema, MergeMergeRequestSchema, ApproveMergeRequestSchema, UnapproveMergeRequestSchema, GetMergeRequestApprovalStateSchema, GetMergeRequestConflictsSchema, GitLabMergeRequestApprovalsResponseSchema, GitLabMergeRequestApprovalStateSchema, MyIssuesSchema, OrbitQuerySchema, OrbitSchemaSchema, OrbitStatusSchema, OrbitToolsSchema, 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, ListSnippetsSchema, GetSnippetSchema, CreateSnippetSchema, UpdateSnippetSchema, DeleteSnippetSchema, GitLabSnippetSchema, GetMergeRequestNotesSchema, GetMergeRequestNoteSchema, GetMergeRequestDiscussionSchema, DeleteMergeRequestDiscussionNoteSchema, ResolveMergeRequestThreadSchema, GetWorkItemSchema, ListWorkItemsSchema, CreateWorkItemSchema, UpdateWorkItemSchema, ConvertWorkItemTypeSchema, ListWorkItemStatusesSchema, ListWorkItemNotesSchema, CreateWorkItemNoteSchema, CreateWorkItemEmojiReactionSchema, CreateWorkItemNoteEmojiReactionSchema, ListWorkItemEmojiReactionsSchema, ListWorkItemNoteEmojiReactionsSchema, DeleteWorkItemEmojiReactionSchema, DeleteWorkItemNoteEmojiReactionSchema, MoveWorkItemSchema, ListCustomFieldDefinitionsSchema, GetTimelineEventsSchema, CreateTimelineEventSchema, ListWebhooksSchema, CreateWebhookSchema, UpdateWebhookSchema, DeleteWebhookSchema, ListWebhookEventsSchema, GetWebhookEventSchema, HealthCheckSchema, } from "./schemas.js";
124
+ GitLabDraftNoteSchema, GitLabForkSchema, GitLabBranchSchema, GitLabProtectedBranchSchema, GitLabProtectedTagSchema, GitLabGroupSchema, GitLabIssueLinkSchema, GitLabIssueSchema, GitLabIssueWithLinkDetailsSchema, GitLabMarkdownUploadSchema, GitLabMergeRequestPipelineSchema, GitLabMergeRequestSchema, GitLabMilestonesSchema, GitLabGroupMilestonesSchema, GitLabNamespaceExistsResponseSchema, GitLabNamespaceSchema, GitLabPipelineJobSchema, GitLabDeploymentSchema, GitLabEnvironmentSchema, GitLabPipelineSchema, GitLabPipelineScheduleSchema, GitLabPipelineScheduleVariableSchema, GitLabPipelineSchedulePlayResultSchema, ListPipelineSchedulesSchema, GetPipelineScheduleSchema, ListPipelineSchedulePipelinesSchema, CreatePipelineScheduleSchema, UpdatePipelineScheduleSchema, DeletePipelineScheduleSchema, PlayPipelineScheduleSchema, TakeOwnershipPipelineScheduleSchema, GetPipelineScheduleVariableSchema, CreatePipelineScheduleVariableSchema, UpdatePipelineScheduleVariableSchema, DeletePipelineScheduleVariableSchema, 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, ListProjectVulnerabilitiesSchema, GetVulnerabilitySchema, DismissVulnerabilitySchema, ConfirmVulnerabilitySchema, ListIssueDiscussionsSchema, ListIssueLinksSchema, ListIssuesSchema, ListTodosSchema, ListLabelsSchema, ListMergeRequestDiffsSchema, // Added
125
+ GetMergeRequestFileDiffSchema, ListMergeRequestChangedFilesSchema, ListMergeRequestDiscussionsSchema, ListMergeRequestPipelinesSchema, ListMergeRequestsSchema, ListGroupMergeRequestsSchema, ListMergeRequestVersionsSchema, GetMergeRequestVersionSchema, GitLabMergeRequestVersionSchema, GitLabMergeRequestVersionDetailSchema, ListNamespacesSchema, ListPipelineJobsSchema, ListPipelinesSchema, ListDeploymentsSchema, ListEnvironmentsSchema, ListProjectRunnersSchema, GetRunnerSchema, ListPipelineTriggerJobsSchema, ValidateCiLintSchema, ValidateProjectCiLintSchema, ListCiCatalogResourcesSchema, GetCiCatalogResourceSchema, ListProjectMembersSchema, ListGroupMembersSchema, ListProjectMilestonesSchema, ListGroupMilestonesSchema, ListProjectsSchema, ListWikiPagesSchema, GetGroupWikiPageSchema, ListGroupWikiPagesSchema, UpdateGroupWikiPageSchema, MarkdownUploadSchema, MarkdownUploadRemoteSchema, DownloadAttachmentSchema, DownloadJobArtifactsSchema, GetJobArtifactFileSchema, GitLabArtifactEntrySchema, ListJobArtifactsSchema, MergeMergeRequestSchema, ApproveMergeRequestSchema, UnapproveMergeRequestSchema, GetMergeRequestApprovalStateSchema, GetMergeRequestConflictsSchema, GitLabMergeRequestApprovalsResponseSchema, GitLabMergeRequestApprovalStateSchema, MyIssuesSchema, OrbitQuerySchema, OrbitSchemaSchema, OrbitStatusSchema, OrbitToolsSchema, 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, ListProtectedTagsSchema, GetProtectedTagSchema, ProtectTagSchema, UnprotectTagSchema, GitLabTagSchema, GitLabTagSignatureSchema, ListSnippetsSchema, GetSnippetSchema, CreateSnippetSchema, UpdateSnippetSchema, DeleteSnippetSchema, GitLabSnippetSchema, GetMergeRequestNotesSchema, GetMergeRequestNoteSchema, GetMergeRequestDiscussionSchema, DeleteMergeRequestDiscussionNoteSchema, ResolveMergeRequestThreadSchema, GetWorkItemSchema, ListWorkItemsSchema, CreateWorkItemSchema, UpdateWorkItemSchema, ConvertWorkItemTypeSchema, ListWorkItemStatusesSchema, ListWorkItemNotesSchema, CreateWorkItemNoteSchema, CreateWorkItemEmojiReactionSchema, CreateWorkItemNoteEmojiReactionSchema, ListWorkItemEmojiReactionsSchema, ListWorkItemNoteEmojiReactionsSchema, DeleteWorkItemEmojiReactionSchema, DeleteWorkItemNoteEmojiReactionSchema, MoveWorkItemSchema, ListCustomFieldDefinitionsSchema, GetTimelineEventsSchema, CreateTimelineEventSchema, ListWebhooksSchema, CreateWebhookSchema, UpdateWebhookSchema, DeleteWebhookSchema, ListWebhookEventsSchema, GetWebhookEventSchema, HealthCheckSchema, } from "./schemas.js";
124
126
  import { randomUUID, timingSafeEqual, } from "node:crypto";
125
- import { createLogger } from "./utils/logger.js";
127
+ import { createLogger, flushBufferedLogs, flushStartupLogs } from "./utils/logger.js";
126
128
  const logger = createLogger();
129
+ function exitAfterFlush(code) {
130
+ flushBufferedLogs(logger);
131
+ process.exit(code);
132
+ }
127
133
  // Construct once at startup. When disabled this is undefined and the result
128
134
  // path below returns the original objects without reading a config file.
129
135
  const maskingPolicyResolver = createMaskingPolicyResolver({
@@ -303,6 +309,7 @@ function createServer() {
303
309
  durationMs,
304
310
  error: safeError instanceof Error ? safeError.message : String(safeError),
305
311
  }, `tool_call_error: ${toolName} (${durationMs}ms)`);
312
+ flushBufferedLogs(logger);
306
313
  return safeError;
307
314
  };
308
315
  try {
@@ -502,7 +509,9 @@ function createServer() {
502
509
  return logCompletion(result);
503
510
  }
504
511
  catch (error) {
505
- const isRecoverableToolError = error instanceof GitLabApiError || error instanceof ToolInputError;
512
+ const isRecoverableToolError = error instanceof GitLabApiError ||
513
+ error instanceof ToolInputError ||
514
+ error instanceof ProjectAccessDeniedError;
506
515
  const safeError = logError(error);
507
516
  if (isRecoverableToolError) {
508
517
  return {
@@ -709,7 +718,7 @@ function validateConfiguration() {
709
718
  new URL(url.trim());
710
719
  }
711
720
  catch {
712
- errors.push(`GITLAB_API_URL contains an invalid URL: ${url.trim()}`);
721
+ errors.push(`GITLAB_API_URL contains an invalid URL: ${redactUrlSecretsInText(url.trim())}`);
713
722
  }
714
723
  }
715
724
  }
@@ -795,7 +804,7 @@ function validateConfiguration() {
795
804
  if (errors.length > 0) {
796
805
  logger.error("Configuration validation failed:");
797
806
  errors.forEach(err => logger.error(` - ${err}`));
798
- process.exit(1);
807
+ exitAfterFlush(1);
799
808
  }
800
809
  logger.info("Configuration validation passed");
801
810
  }
@@ -807,6 +816,20 @@ let oauthClient = null;
807
816
  * switch to signed/sealed opaque values instead of per-pod in-memory caches.
808
817
  */
809
818
  let STATELESS_MATERIAL = null;
819
+ let hasEmittedDeprecatedEnvWarnings = false;
820
+ function emitDeprecatedEnvWarnings() {
821
+ if (hasEmittedDeprecatedEnvWarnings) {
822
+ return;
823
+ }
824
+ hasEmittedDeprecatedEnvWarnings = true;
825
+ const warnings = getDeprecatedEnvWarnings(buildDeprecatedEnvInput());
826
+ for (const warning of warnings) {
827
+ logger.warn(warning);
828
+ }
829
+ if (warnings.length > 0) {
830
+ flushStartupLogs(logger);
831
+ }
832
+ }
810
833
  try {
811
834
  // Drive enablement from the already-resolved config flag so the CLI flag
812
835
  // (--oauth-stateless-mode) is honored. Re-reading env.OAUTH_STATELESS_MODE
@@ -821,6 +844,7 @@ try {
821
844
  }
822
845
  }
823
846
  catch (err) {
847
+ emitDeprecatedEnvWarnings();
824
848
  // eslint-disable-next-line no-console -- startup failure must be visible
825
849
  console.error(`[gitlab-mcp] failed to load stateless secret: ${err.message}`);
826
850
  process.exit(1);
@@ -1064,7 +1088,11 @@ const BASE_HEADERS = {
1064
1088
  function buildAuthHeaders() {
1065
1089
  if (REMOTE_AUTHORIZATION || GITLAB_MCP_OAUTH) {
1066
1090
  const ctx = sessionAuthStore.getStore();
1067
- logger.debug({ sessionId: ctx?.sessionId, header: ctx?.header, apiUrl: ctx?.apiUrl }, "buildAuthHeaders: session context");
1091
+ logger.debug({
1092
+ sessionId: ctx?.sessionId,
1093
+ header: ctx?.header,
1094
+ apiUrl: ctx?.apiUrl === undefined ? undefined : redactUrlSecretsInText(ctx.apiUrl),
1095
+ }, "buildAuthHeaders: session context");
1068
1096
  if (ctx?.token) {
1069
1097
  return {
1070
1098
  [ctx.header]: ctx.header === "Authorization" ? `Bearer ${ctx.token}` : ctx.token,
@@ -1112,7 +1140,11 @@ function getEffectiveApiUrl() {
1112
1140
  if (ctx?.apiUrl) {
1113
1141
  return ctx.apiUrl;
1114
1142
  }
1115
- logger.warn({ sessionId: ctx?.sessionId, header: ctx?.header, apiUrl: ctx?.apiUrl }, "getEffectiveApiUrl: No context or apiUrl found, falling back to default");
1143
+ logger.warn({
1144
+ sessionId: ctx?.sessionId,
1145
+ header: ctx?.header,
1146
+ apiUrl: ctx?.apiUrl === undefined ? undefined : redactUrlSecretsInText(ctx.apiUrl),
1147
+ }, "getEffectiveApiUrl: No context or apiUrl found, falling back to default");
1116
1148
  }
1117
1149
  return GITLAB_API_URL;
1118
1150
  }
@@ -1278,27 +1310,31 @@ const GITLAB_REPO_FILE_ENCODING = getConfig("repo-file-encoding", "GITLAB_REPO_F
1278
1310
  if (REMOTE_AUTHORIZATION) {
1279
1311
  // Remote authorization mode: token comes from HTTP headers
1280
1312
  if (SSE) {
1313
+ emitDeprecatedEnvWarnings();
1281
1314
  logger.error("REMOTE_AUTHORIZATION=true is not compatible with SSE transport mode");
1282
1315
  logger.error("Please use STREAMABLE_HTTP=true instead");
1283
- process.exit(1);
1316
+ exitAfterFlush(1);
1284
1317
  }
1285
1318
  if (!STREAMABLE_HTTP) {
1319
+ emitDeprecatedEnvWarnings();
1286
1320
  logger.error("REMOTE_AUTHORIZATION=true requires STREAMABLE_HTTP=true");
1287
1321
  logger.error("Set STREAMABLE_HTTP=true to enable remote authorization");
1288
- process.exit(1);
1322
+ exitAfterFlush(1);
1289
1323
  }
1290
1324
  logger.info("Remote authorization enabled: tokens will be read from HTTP headers");
1291
1325
  }
1292
1326
  if (GITLAB_MCP_OAUTH) {
1293
1327
  if (SSE) {
1328
+ emitDeprecatedEnvWarnings();
1294
1329
  logger.error("GITLAB_MCP_OAUTH=true is not compatible with SSE transport mode");
1295
1330
  logger.error("Please use STREAMABLE_HTTP=true instead");
1296
- process.exit(1);
1331
+ exitAfterFlush(1);
1297
1332
  }
1298
1333
  if (!STREAMABLE_HTTP) {
1334
+ emitDeprecatedEnvWarnings();
1299
1335
  logger.error("GITLAB_MCP_OAUTH=true requires STREAMABLE_HTTP=true");
1300
1336
  logger.error("Set STREAMABLE_HTTP=true to enable MCP OAuth");
1301
- process.exit(1);
1337
+ exitAfterFlush(1);
1302
1338
  }
1303
1339
  logger.info("MCP OAuth enabled: GitLab OAuth proxy active (Private-Token/JOB-TOKEN headers bypass OAuth)");
1304
1340
  }
@@ -1309,10 +1345,12 @@ if (!isCliInvocation(process.argv) &&
1309
1345
  !GITLAB_PERSONAL_ACCESS_TOKEN &&
1310
1346
  !GITLAB_JOB_TOKEN &&
1311
1347
  !GITLAB_AUTH_COOKIE_PATH) {
1312
- // Standard mode: token must be in environment (unless using OAuth)
1348
+ // Standard mode: token must be in environment (unless using OAuth).
1349
+ // This exit runs before runServer(), so warnings have to be printed here too.
1350
+ emitDeprecatedEnvWarnings();
1313
1351
  logger.error("GITLAB_PERSONAL_ACCESS_TOKEN environment variable is not set");
1314
1352
  logger.info("Either set GITLAB_PERSONAL_ACCESS_TOKEN or enable OAuth with GITLAB_USE_OAUTH=true");
1315
- process.exit(1);
1353
+ exitAfterFlush(1);
1316
1354
  }
1317
1355
  class GitLabApiError extends Error {
1318
1356
  constructor(message) {
@@ -1326,6 +1364,12 @@ class ToolInputError extends Error {
1326
1364
  this.name = "ToolInputError";
1327
1365
  }
1328
1366
  }
1367
+ class ProjectAccessDeniedError extends Error {
1368
+ constructor(message) {
1369
+ super(message);
1370
+ this.name = "ProjectAccessDeniedError";
1371
+ }
1372
+ }
1329
1373
  const MERGE_REQUEST_IDENTIFIER_REQUIRED = "Either merge_request_iid or source_branch must be provided";
1330
1374
  /**
1331
1375
  * Utility function for handling GitLab API errors
@@ -1389,7 +1433,8 @@ async function authenticateWithJobTokenFallbackAsync(response) {
1389
1433
  /**
1390
1434
  * @param {string} projectId - The project ID parameter passed to the function
1391
1435
  * @returns {string} The project ID to use for the API call
1392
- * @throws {Error} If GITLAB_ALLOWED_PROJECT_IDS is set and the requested project is not in the whitelist
1436
+ * @throws {ProjectAccessDeniedError} If the requested project is outside the effective allowlist
1437
+ * @throws {Error} If no project can be selected, or multiple allowed projects require an explicit ID
1393
1438
  */
1394
1439
  function getEffectiveProjectId(projectId) {
1395
1440
  const allowedProjectIds = getEffectiveAllowedProjectIds();
@@ -1400,7 +1445,7 @@ function getEffectiveProjectId(projectId) {
1400
1445
  }
1401
1446
  // If a project ID is provided, check if it's in the whitelist
1402
1447
  if (projectId && !allowedProjectIds.includes(projectId)) {
1403
- throw new Error(`Access denied: Project ${projectId} is not in the allowed project list: ${allowedProjectIds.join(", ")}`);
1448
+ throw new ProjectAccessDeniedError(`Access denied: Project ${projectId} is not in the allowed project list: ${allowedProjectIds.join(", ")}`);
1404
1449
  }
1405
1450
  // If no project ID provided but we have multiple allowed projects, require an explicit choice
1406
1451
  if (!projectId && allowedProjectIds.length > 1) {
@@ -4643,6 +4688,28 @@ async function publishDraftNote(projectId, mergeRequestIid, draftNoteId) {
4643
4688
  };
4644
4689
  }
4645
4690
  }
4691
+ function healthCheckTransportErrorResult(error) {
4692
+ const failure = classifyHealthCheckTransportError(error);
4693
+ return {
4694
+ content: [
4695
+ {
4696
+ type: "text",
4697
+ text: JSON.stringify({
4698
+ status: "error",
4699
+ authenticated: false,
4700
+ gitlab_url: redactGitLabUrlCredentials(getEffectiveApiUrl()),
4701
+ mcp_server_version: SERVER_VERSION,
4702
+ error: {
4703
+ kind: failure.kind,
4704
+ code: failure.code,
4705
+ message: failure.message,
4706
+ ...(failure.hint !== undefined ? { hint: failure.hint } : {}),
4707
+ },
4708
+ }),
4709
+ },
4710
+ ],
4711
+ };
4712
+ }
4646
4713
  async function fetchGitLabVersionMetadata() {
4647
4714
  try {
4648
4715
  const response = await fetch(`${getEffectiveApiUrl()}/version`, {
@@ -5442,6 +5509,132 @@ async function getEnvironment(projectId, environmentId) {
5442
5509
  const data = await response.json();
5443
5510
  return GitLabEnvironmentSchema.parse(data);
5444
5511
  }
5512
+ async function listProjectRunners(projectId, options) {
5513
+ const decodedProjectId = decodeURIComponent(projectId);
5514
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(decodedProjectId))}/runners`);
5515
+ appendFilterParams(url.searchParams, options);
5516
+ const response = await fetch(url.toString(), {
5517
+ ...getFetchConfig(),
5518
+ });
5519
+ await handleGitLabError(response);
5520
+ return redactSensitiveGitLabFields(await response.json());
5521
+ }
5522
+ const PROJECT_RUNNER_LOOKUP_PAGE_SIZE = 100;
5523
+ const MAX_PROJECT_RUNNER_LOOKUP_PAGES = 100;
5524
+ function isJsonObject(value) {
5525
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5526
+ }
5527
+ function runnerEntryId(entry) {
5528
+ if (!isJsonObject(entry)) {
5529
+ return undefined;
5530
+ }
5531
+ const id = entry.id;
5532
+ if (typeof id === "number" && Number.isFinite(id)) {
5533
+ return String(id);
5534
+ }
5535
+ if (typeof id === "string" && id !== "") {
5536
+ return id;
5537
+ }
5538
+ return undefined;
5539
+ }
5540
+ function projectIsInAllowlist(project, allowedProjectIds) {
5541
+ if (!isJsonObject(project)) {
5542
+ return false;
5543
+ }
5544
+ const id = project.id;
5545
+ const pathWithNamespace = project.path_with_namespace;
5546
+ const idText = typeof id === "number" && Number.isFinite(id)
5547
+ ? String(id)
5548
+ : typeof id === "string"
5549
+ ? id
5550
+ : undefined;
5551
+ return ((idText !== undefined && allowedProjectIds.includes(idText)) ||
5552
+ (typeof pathWithNamespace === "string" && allowedProjectIds.includes(pathWithNamespace)));
5553
+ }
5554
+ /**
5555
+ * GET /runners/:id is not project-scoped. Instance runner details are visible to
5556
+ * any authenticated user and can list projects outside GITLAB_ALLOWED_PROJECT_IDS.
5557
+ * Keep only projects on the effective allowlist, and drop groups because a group
5558
+ * path cannot be checked against a project allowlist.
5559
+ */
5560
+ function restrictRunnerDetailsToAllowlist(runner, allowedProjectIds) {
5561
+ if (!isJsonObject(runner)) {
5562
+ return runner;
5563
+ }
5564
+ const restricted = {};
5565
+ for (const [key, value] of Object.entries(runner)) {
5566
+ if (key === "groups") {
5567
+ continue;
5568
+ }
5569
+ if (key === "projects" && Array.isArray(value)) {
5570
+ restricted.projects = value.filter(project => projectIsInAllowlist(project, allowedProjectIds));
5571
+ continue;
5572
+ }
5573
+ restricted[key] = value;
5574
+ }
5575
+ return restricted;
5576
+ }
5577
+ /**
5578
+ * Confirm the runner is among the runners available to an allowed project.
5579
+ * GitLab has no single-runner project read; membership is the paginated
5580
+ * GET /projects/:id/runners list, which includes ancestor group runners and
5581
+ * allowed instance runners. Fail closed when the list cannot be fully scanned.
5582
+ */
5583
+ async function assertRunnerAvailableToProject(projectId, runnerId) {
5584
+ const effectiveProjectId = getEffectiveProjectId(decodeURIComponent(projectId));
5585
+ let page = 1;
5586
+ while (page <= MAX_PROJECT_RUNNER_LOOKUP_PAGES) {
5587
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/runners`);
5588
+ url.searchParams.set("per_page", String(PROJECT_RUNNER_LOOKUP_PAGE_SIZE));
5589
+ url.searchParams.set("page", String(page));
5590
+ const response = await fetch(url.toString(), {
5591
+ ...getFetchConfig(),
5592
+ });
5593
+ await handleGitLabError(response);
5594
+ const data = await response.json();
5595
+ if (!Array.isArray(data)) {
5596
+ throw new GitLabApiError(`Could not verify runner ${runnerId} against project ${effectiveProjectId}`);
5597
+ }
5598
+ if (data.some(entry => runnerEntryId(entry) === runnerId)) {
5599
+ return;
5600
+ }
5601
+ const nextPageHeader = response.headers.get("x-next-page");
5602
+ if (!nextPageHeader) {
5603
+ throw new GitLabApiError("Runner not found");
5604
+ }
5605
+ const nextPage = Number.parseInt(nextPageHeader, 10);
5606
+ if (!Number.isFinite(nextPage) || nextPage <= page) {
5607
+ throw new ProjectAccessDeniedError(`Access denied: Runner ${runnerId} could not be verified against project ${effectiveProjectId}`);
5608
+ }
5609
+ page = nextPage;
5610
+ }
5611
+ throw new ProjectAccessDeniedError(`Access denied: Runner ${runnerId} could not be verified against project ${effectiveProjectId}`);
5612
+ }
5613
+ async function getRunner(runnerId, projectId) {
5614
+ const allowedProjectIds = getEffectiveAllowedProjectIds();
5615
+ const decodedProjectId = projectId === undefined || projectId.trim() === "" ? undefined : decodeURIComponent(projectId);
5616
+ // GITLAB_PROJECT_ID is the default project, same as getEffectiveProjectId.
5617
+ // It is not an allowlist, so runner details stay unfiltered unless
5618
+ // GITLAB_ALLOWED_PROJECT_IDS is set.
5619
+ if (allowedProjectIds.length > 0 ||
5620
+ decodedProjectId !== undefined ||
5621
+ Boolean(GITLAB_PROJECT_ID)) {
5622
+ await assertRunnerAvailableToProject(getEffectiveProjectId(decodedProjectId ?? ""), runnerId);
5623
+ }
5624
+ const url = new URL(`${getEffectiveApiUrl()}/runners/${encodeGitLabPathSegment(runnerId)}`);
5625
+ const response = await fetch(url.toString(), {
5626
+ ...getFetchConfig(),
5627
+ });
5628
+ if (response.status === 404) {
5629
+ throw new GitLabApiError("Runner not found");
5630
+ }
5631
+ await handleGitLabError(response);
5632
+ const details = redactSensitiveGitLabFields(await response.json());
5633
+ if (allowedProjectIds.length === 0) {
5634
+ return details;
5635
+ }
5636
+ return restrictRunnerDetailsToAllowlist(details, allowedProjectIds);
5637
+ }
5445
5638
  async function environmentRequest(projectId, path, method, body, query) {
5446
5639
  projectId = decodeURIComponent(projectId);
5447
5640
  const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(getEffectiveProjectId(projectId))}/environments${path}`);
@@ -7039,7 +7232,7 @@ function assertVulnerabilityProjectAllowed(vulnerabilityId, project) {
7039
7232
  const allowed = (fullPath !== undefined && allowedProjectIds.includes(fullPath)) ||
7040
7233
  (numericId !== undefined && allowedProjectIds.includes(numericId));
7041
7234
  if (!allowed) {
7042
- throw new Error(`Access denied: Vulnerability ${vulnerabilityId} belongs to project ${fullPath ?? numericId ?? "unknown"}, which is not in the allowed project list: ${allowedProjectIds.join(", ")}`);
7235
+ throw new ProjectAccessDeniedError(`Access denied: Vulnerability ${vulnerabilityId} belongs to project ${fullPath ?? numericId ?? "unknown"}, which is not in the allowed project list: ${allowedProjectIds.join(", ")}`);
7043
7236
  }
7044
7237
  }
7045
7238
  /**
@@ -7863,7 +8056,7 @@ async function handleToolCall(params) {
7863
8056
  const controller = new AbortController();
7864
8057
  const timeoutMs = 45000;
7865
8058
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
7866
- logger.info({ endpoint: graphqlUrl }, "execute_graphql request");
8059
+ logger.info({ endpoint: redactUrlSecretsInText(graphqlUrl) }, "execute_graphql request");
7867
8060
  try {
7868
8061
  const response = await fetch(graphqlUrl, {
7869
8062
  ...getFetchConfig(),
@@ -9286,6 +9479,20 @@ async function handleToolCall(params) {
9286
9479
  content: [{ type: "text", text: JSON.stringify(environment) }],
9287
9480
  };
9288
9481
  }
9482
+ case "list_project_runners": {
9483
+ const { project_id, ...options } = ListProjectRunnersSchema.parse(params.arguments);
9484
+ const runners = await listProjectRunners(project_id, options);
9485
+ return {
9486
+ content: [{ type: "text", text: JSON.stringify(runners) }],
9487
+ };
9488
+ }
9489
+ case "get_runner": {
9490
+ const { runner_id, project_id } = GetRunnerSchema.parse(params.arguments);
9491
+ const runner = await getRunner(runner_id, project_id);
9492
+ return {
9493
+ content: [{ type: "text", text: JSON.stringify(runner) }],
9494
+ };
9495
+ }
9289
9496
  case "update_environment": {
9290
9497
  const { project_id, environment_id, ...body } = UpdateEnvironmentSchema.parse(params.arguments);
9291
9498
  return {
@@ -10518,6 +10725,77 @@ async function handleToolCall(params) {
10518
10725
  content: [{ type: "text", text: JSON.stringify(signature) }],
10519
10726
  };
10520
10727
  }
10728
+ case "list_protected_tags": {
10729
+ const args = ListProtectedTagsSchema.parse(params.arguments);
10730
+ const projectId = decodeURIComponent(args.project_id);
10731
+ const effectiveProjectId = getEffectiveProjectId(projectId);
10732
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/protected_tags`);
10733
+ if (args.page)
10734
+ url.searchParams.append("page", String(args.page));
10735
+ if (args.per_page)
10736
+ url.searchParams.append("per_page", String(args.per_page));
10737
+ const response = await fetch(url.toString(), {
10738
+ ...getFetchConfig(),
10739
+ });
10740
+ await handleGitLabError(response);
10741
+ const data = z.array(GitLabProtectedTagSchema).parse(await response.json());
10742
+ return {
10743
+ content: [{ type: "text", text: JSON.stringify(data) }],
10744
+ };
10745
+ }
10746
+ case "get_protected_tag": {
10747
+ const args = GetProtectedTagSchema.parse(params.arguments);
10748
+ const projectId = decodeURIComponent(args.project_id);
10749
+ const effectiveProjectId = getEffectiveProjectId(projectId);
10750
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/protected_tags/${encodeGitLabPathSegment(args.tag_name)}`);
10751
+ const response = await fetch(url.toString(), {
10752
+ ...getFetchConfig(),
10753
+ });
10754
+ await handleGitLabError(response);
10755
+ const data = GitLabProtectedTagSchema.parse(await response.json());
10756
+ return {
10757
+ content: [{ type: "text", text: JSON.stringify(data) }],
10758
+ };
10759
+ }
10760
+ case "protect_tag": {
10761
+ const args = ProtectTagSchema.parse(params.arguments);
10762
+ const projectId = decodeURIComponent(args.project_id);
10763
+ const effectiveProjectId = getEffectiveProjectId(projectId);
10764
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/protected_tags`);
10765
+ const body = { name: args.tag_name };
10766
+ if (args.create_access_level !== undefined) {
10767
+ body.create_access_level = args.create_access_level;
10768
+ }
10769
+ const response = await fetch(url.toString(), {
10770
+ ...getFetchConfig(),
10771
+ method: "POST",
10772
+ body: JSON.stringify(body),
10773
+ });
10774
+ await handleGitLabError(response);
10775
+ const data = GitLabProtectedTagSchema.parse(await response.json());
10776
+ return {
10777
+ content: [{ type: "text", text: JSON.stringify(data) }],
10778
+ };
10779
+ }
10780
+ case "unprotect_tag": {
10781
+ const args = UnprotectTagSchema.parse(params.arguments);
10782
+ const projectId = decodeURIComponent(args.project_id);
10783
+ const effectiveProjectId = getEffectiveProjectId(projectId);
10784
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeGitLabPathSegment(effectiveProjectId)}/protected_tags/${encodeGitLabPathSegment(args.tag_name)}`);
10785
+ const response = await fetch(url.toString(), {
10786
+ ...getFetchConfig(),
10787
+ method: "DELETE",
10788
+ });
10789
+ await handleGitLabError(response);
10790
+ return {
10791
+ content: [
10792
+ {
10793
+ type: "text",
10794
+ text: JSON.stringify({ status: "unprotected", tag: args.tag_name }),
10795
+ },
10796
+ ],
10797
+ };
10798
+ }
10521
10799
  case "list_snippets": {
10522
10800
  const args = ListSnippetsSchema.parse(params.arguments);
10523
10801
  const { project_id, ...options } = args;
@@ -10650,8 +10928,21 @@ async function handleToolCall(params) {
10650
10928
  case "health_check": {
10651
10929
  HealthCheckSchema.parse(params.arguments ?? {});
10652
10930
  const url = new URL(`${getEffectiveApiUrl()}/user`);
10653
- const response = await fetch(url.toString(), getFetchConfig());
10654
- const jobAuthenticated = await authenticateWithJobTokenFallbackAsync(response);
10931
+ const fetchConfig = getFetchConfig();
10932
+ let response;
10933
+ try {
10934
+ response = await fetch(url.toString(), fetchConfig);
10935
+ }
10936
+ catch (error) {
10937
+ return healthCheckTransportErrorResult(error);
10938
+ }
10939
+ let jobAuthenticated;
10940
+ try {
10941
+ jobAuthenticated = await authenticateWithJobTokenFallbackAsync(response);
10942
+ }
10943
+ catch (error) {
10944
+ return healthCheckTransportErrorResult(error);
10945
+ }
10655
10946
  // insufficient_scope and insufficient_granular_scope mean the token was accepted
10656
10947
  // but User API scope is missing.
10657
10948
  // Rate-limit and authentication-ban 403s stay unauthenticated.
@@ -10665,7 +10956,7 @@ async function handleToolCall(params) {
10665
10956
  text: JSON.stringify({
10666
10957
  status: authenticated ? "ok" : "error",
10667
10958
  authenticated,
10668
- gitlab_url: getEffectiveApiUrl(),
10959
+ gitlab_url: redactGitLabUrlCredentials(getEffectiveApiUrl()),
10669
10960
  mcp_server_version: SERVER_VERSION,
10670
10961
  ...(insufficientUserScope ? { user_api_warning: USER_API_HEALTH_WARNING } : {}),
10671
10962
  ...(versionMetadata ?? {}),
@@ -11198,7 +11489,7 @@ async function startStreamableHTTPServer() {
11198
11489
  }
11199
11490
  catch (error) {
11200
11491
  const reason = error instanceof Error ? error.message : "Invalid X-GitLab-API-URL";
11201
- logger.warn(`Invalid X-GitLab-API-URL provided: ${dynamicApiUrl}. ${reason}`);
11492
+ logger.warn(`Invalid X-GitLab-API-URL provided: ${redactUrlSecretsInText(dynamicApiUrl)}. ${reason}`);
11202
11493
  return null; // Reject if URL is malformed or not allowed
11203
11494
  }
11204
11495
  }
@@ -12250,7 +12541,7 @@ function initOAuthClient() {
12250
12541
  return;
12251
12542
  }
12252
12543
  if (isCleartextRemoteUrl(GITLAB_API_URL)) {
12253
- throw new Error(`Refusing to send OAuth tokens over cleartext HTTP to ${GITLAB_API_URL}. ` +
12544
+ throw new Error(`Refusing to send OAuth tokens over cleartext HTTP to ${redactUrlSecretsInText(GITLAB_API_URL)}. ` +
12254
12545
  "Use an https:// GITLAB_API_URL (http:// is only allowed for localhost).");
12255
12546
  }
12256
12547
  oauthClient = createGitLabOAuthClient(GITLAB_API_URL.replace(/\/api\/v4$/, ""));
@@ -12261,6 +12552,9 @@ function initOAuthClient() {
12261
12552
  */
12262
12553
  async function runServer() {
12263
12554
  try {
12555
+ // Print before validateConfiguration(), which process.exit()s on failure.
12556
+ // logger.warn goes to stderr; stdio transport owns stdout for JSON-RPC.
12557
+ emitDeprecatedEnvWarnings();
12264
12558
  // Validate configuration before starting server
12265
12559
  validateConfiguration();
12266
12560
  // Initialize OAuth token if OAuth is enabled
@@ -12272,7 +12566,7 @@ async function runServer() {
12272
12566
  }
12273
12567
  catch (error) {
12274
12568
  logger.error({ err: error }, "OAuth authentication failed");
12275
- process.exit(1);
12569
+ exitAfterFlush(1);
12276
12570
  }
12277
12571
  }
12278
12572
  const transportMode = determineTransportMode();
@@ -12287,35 +12581,26 @@ async function runServer() {
12287
12581
  }
12288
12582
  });
12289
12583
  }
12290
- logger.info(`Configured GitLab API URLs: ${GITLAB_API_URLS.join(", ")}`);
12291
- logger.info(`Default GitLab API URL: ${GITLAB_API_URL}`);
12292
- for (const warning of getDeprecatedEnvWarnings({
12293
- readOnlyMode: GITLAB_READ_ONLY_MODE,
12294
- permissionModeRaw: GITLAB_PERMISSION_MODE_RAW,
12295
- allowedGroupsRaw: GITLAB_ALLOWED_GROUPS_RAW,
12296
- oauthAllowedGroupsRaw: GITLAB_OAUTH_ALLOWED_GROUPS_RAW,
12297
- useWikiRaw: USE_GITLAB_WIKI_RAW,
12298
- useMilestoneRaw: USE_MILESTONE_RAW,
12299
- usePipelineRaw: USE_PIPELINE_RAW,
12300
- })) {
12301
- logger.warn(warning);
12302
- }
12584
+ logger.info(`Configured GitLab API URLs: ${GITLAB_API_URLS.map(redactUrlSecretsInText).join(", ")}`);
12585
+ logger.info(`Default GitLab API URL: ${redactUrlSecretsInText(GITLAB_API_URL)}`);
12303
12586
  if (GITLAB_OAUTH_ALLOWED_GROUPS) {
12304
12587
  logger.info(`Group access control enabled for: ${GITLAB_OAUTH_ALLOWED_GROUPS.join(", ")}`);
12305
12588
  }
12306
12589
  }
12307
12590
  catch (error) {
12308
12591
  logger.error({ err: error }, "Error initializing server");
12309
- process.exit(1);
12592
+ exitAfterFlush(1);
12310
12593
  }
12311
12594
  }
12312
12595
  async function main() {
12313
12596
  const cli = resolveCli(process.argv);
12314
12597
  if (cli.kind === "help") {
12598
+ emitDeprecatedEnvWarnings();
12315
12599
  process.stdout.write(cli.text);
12316
12600
  process.exit(0);
12317
12601
  }
12318
12602
  if (cli.kind === "usage" || cli.kind === "refused") {
12603
+ emitDeprecatedEnvWarnings();
12319
12604
  process.stderr.write(`${cli.message}\n`);
12320
12605
  process.exit(2);
12321
12606
  }
@@ -12328,10 +12613,11 @@ async function main() {
12328
12613
  const message = error instanceof Error ? error.message : String(error);
12329
12614
  process.stderr.write(`${message}\n`);
12330
12615
  logger.error({ err: error }, "auth command failed");
12331
- process.exit(1);
12616
+ exitAfterFlush(1);
12332
12617
  }
12333
12618
  }
12334
12619
  if (cli.kind === "run") {
12620
+ emitDeprecatedEnvWarnings();
12335
12621
  if (!hasCliCredentials()) {
12336
12622
  process.stderr.write("Missing GitLab credentials. Set --token or GITLAB_PERSONAL_ACCESS_TOKEN, or run `auth` with GITLAB_USE_OAUTH=true.\n");
12337
12623
  process.exit(2);
@@ -12354,7 +12640,7 @@ async function main() {
12354
12640
  catch (error) {
12355
12641
  process.stderr.write(formatCliError(error));
12356
12642
  logger.error({ err: error }, "cli command failed");
12357
- process.exit(1);
12643
+ exitAfterFlush(1);
12358
12644
  }
12359
12645
  }
12360
12646
  await runServer();
@@ -12381,5 +12667,5 @@ function maskCliToolResult(toolName, result) {
12381
12667
  }
12382
12668
  main().catch(error => {
12383
12669
  logger.fatal({ err: error }, "Fatal error in main()");
12384
- process.exit(1);
12670
+ exitAfterFlush(1);
12385
12671
  });
@@ -51,6 +51,7 @@ import { looksLikeStatelessClientId, mintClientId, openClientId, } from "./state
51
51
  import { looksLikeStatelessState, mintPendingAuthState, openPendingAuthState, } from "./stateless/pending-auth.js";
52
52
  import { looksLikeStatelessStoredTokensCode, mintStoredTokensCode, openStoredTokensCode, ConsumedProxyCodeCache, PROXY_CODE_CACHE_FULL, } from "./stateless/index.js";
53
53
  import { createLogger } from "./utils/logger.js";
54
+ import { redactUrlSecretsInText } from "./utils/redact-url.js";
54
55
  const logger = createLogger("gitlab-mcp-oauth-proxy");
55
56
  // ---------------------------------------------------------------------------
56
57
  // GitLab OAuth Server Provider
@@ -162,7 +163,7 @@ class GitLabOAuthServerProvider {
162
163
  throw new Error("callbackUrl is required when callbackProxyEnabled is true");
163
164
  }
164
165
  if (callbackProxyEnabled) {
165
- logger.info(`Callback proxy mode enabled — fixed callback URL: ${callbackUrl}`);
166
+ logger.info(`Callback proxy mode enabled — fixed callback URL: ${redactUrlSecretsInText(callbackUrl)}`);
166
167
  }
167
168
  if (stateless) {
168
169
  logger.info(`Stateless mode enabled (client_id TTL: ${stateless.clientTtlSeconds}s, ` +