@zereight/mcp-gitlab 2.2.0 → 2.2.2

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.
@@ -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, ` +
package/build/oauth.js CHANGED
@@ -10,6 +10,7 @@ import { promisify } from "util";
10
10
  import open from "open";
11
11
  import pkceChallenge from "pkce-challenge";
12
12
  import { createLogger } from "./utils/logger.js";
13
+ import { redactUrlSecretsInText } from "./utils/redact-url.js";
13
14
  import { GITLAB_PERMISSION_MODE } from "./config.js";
14
15
  import { formatOAuthScopes, grantedOAuthScopes, oauthScopeMismatchError, oauthTokenNeedsReauthorization, runDeviceAuthorizationGrantAsync, } from "./oauth-device-flow.js";
15
16
  export { oauthTokenNeedsReauthorization };
@@ -329,10 +330,10 @@ export class GitLabOAuth {
329
330
  // Open browser for this new request
330
331
  const authUrl = await this.getAuthorizationUrl(newState);
331
332
  logger.info("Opening browser for new authentication request...");
332
- logger.info(`If browser doesn't open, visit: ${authUrl}`);
333
+ logger.info(`If browser doesn't open, visit: ${redactUrlSecretsInText(authUrl)}`);
333
334
  open(authUrl).catch(err => {
334
335
  logger.error({ err }, "Failed to open browser");
335
- logger.info(`Please manually open: ${authUrl}`);
336
+ logger.info(`Please manually open: ${redactUrlSecretsInText(authUrl)}`);
336
337
  });
337
338
  // Wait for the auth to complete
338
339
  const authPromise = new Promise((authResolve, authReject) => {
@@ -485,10 +486,10 @@ export class GitLabOAuth {
485
486
  logger.info(`Shared OAuth callback server listening on port ${callbackPort}`);
486
487
  const authUrl = await this.getAuthorizationUrl(state);
487
488
  logger.info("Opening browser for authentication...");
488
- logger.info(`If browser doesn't open, visit: ${authUrl}`);
489
+ logger.info(`If browser doesn't open, visit: ${redactUrlSecretsInText(authUrl)}`);
489
490
  open(authUrl).catch(err => {
490
491
  logger.error({ err }, "Failed to open browser");
491
- logger.info(`Please manually open: ${authUrl}`);
492
+ logger.info(`Please manually open: ${redactUrlSecretsInText(authUrl)}`);
492
493
  });
493
494
  });
494
495
  server.on("error", error => {
package/build/schemas.js CHANGED
@@ -259,6 +259,36 @@ export const GetPipelineSchema = z.object({
259
259
  project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
260
260
  pipeline_id: z.coerce.string().describe("The ID of the pipeline"),
261
261
  });
262
+ // https://docs.gitlab.com/api/runners/#list-all-of-a-projects-runners
263
+ export const ListProjectRunnersSchema = z
264
+ .object({
265
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
266
+ type: z
267
+ .enum(["instance_type", "group_type", "project_type"])
268
+ .optional()
269
+ .describe("Runner type: instance_type, group_type, or project_type"),
270
+ status: z
271
+ .enum(["online", "offline", "stale", "never_contacted", "active", "paused"])
272
+ .optional()
273
+ .describe("Runner status: online, offline, stale, or never_contacted. active and paused are deprecated; use the paused filter instead. offline may also include stale runners"),
274
+ paused: z
275
+ .boolean()
276
+ .optional()
277
+ .describe("true returns only runners ignoring new jobs; false returns only runners accepting new jobs"),
278
+ tag_list: z
279
+ .array(z.string())
280
+ .optional()
281
+ .describe("Runner tags that must match. Sent to GitLab as a comma-separated tag_list"),
282
+ })
283
+ .merge(PaginationOptionsSchema);
284
+ // https://docs.gitlab.com/api/runners/#retrieve-runners-details
285
+ export const GetRunnerSchema = z.object({
286
+ runner_id: z.coerce.string().describe("The ID of the runner"),
287
+ project_id: z.coerce
288
+ .string()
289
+ .optional()
290
+ .describe("Project ID or URL-encoded path used to verify the runner is on that project's runner list. Required when GITLAB_ALLOWED_PROJECT_IDS or a session project scope lists more than one project."),
291
+ });
262
292
  export const GetPipelineVariablesSchema = GetPipelineSchema.merge(PaginationOptionsSchema);
263
293
  export const UpdatePipelineMetadataSchema = GetPipelineSchema.extend({
264
294
  name: z.string().min(1).describe("New pipeline name"),
@@ -3820,6 +3850,73 @@ export const GetTagSignatureSchema = z.object({
3820
3850
  project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
3821
3851
  tag_name: z.string().describe("The name of the tag"),
3822
3852
  });
3853
+ // Protected tags: https://docs.gitlab.com/api/protected_tags/
3854
+ // Recognized create access levels are 0 (No access), 30 (Developer), and 40 (Maintainer).
3855
+ const protectedTagAccessLevel = z.coerce
3856
+ .number()
3857
+ .int()
3858
+ .refine(level => [0, 30, 40].includes(level), {
3859
+ message: "Access level must be one of 0 (No access), 30 (Developer), or 40 (Maintainer)",
3860
+ });
3861
+ export const ListProtectedTagsSchema = ProjectParamsSchema.merge(PaginationOptionsSchema);
3862
+ export const GetProtectedTagSchema = ProjectParamsSchema.extend({
3863
+ tag_name: z.string().describe("Name of the protected tag or wildcard"),
3864
+ });
3865
+ function resolveProtectedTagName(tagName, name) {
3866
+ if (tagName) {
3867
+ return tagName;
3868
+ }
3869
+ if (name) {
3870
+ return name;
3871
+ }
3872
+ return "";
3873
+ }
3874
+ // tag_name and name are both optional in the published JSON Schema so clients
3875
+ // that validate before the call can send either field. superRefine still
3876
+ // requires one of them, and the transform copies name onto tag_name.
3877
+ export const ProtectTagSchema = ProjectParamsSchema.extend({
3878
+ tag_name: z
3879
+ .string()
3880
+ .optional()
3881
+ .describe("Tag name or wildcard pattern to protect (for example v*). Required unless name is set."),
3882
+ name: z
3883
+ .string()
3884
+ .optional()
3885
+ .describe("Alias for tag_name. Required unless tag_name is set."),
3886
+ create_access_level: protectedTagAccessLevel
3887
+ .optional()
3888
+ .describe("Access level allowed to create matching tags (0=No access, 30=Developer, 40=Maintainer). GitLab default is 40 when omitted."),
3889
+ })
3890
+ .superRefine((data, ctx) => {
3891
+ if (!data.tag_name && !data.name) {
3892
+ ctx.addIssue({
3893
+ code: z.ZodIssueCode.custom,
3894
+ message: "Either tag_name or name is required",
3895
+ path: ["tag_name"],
3896
+ });
3897
+ }
3898
+ })
3899
+ .transform(data => ({
3900
+ project_id: data.project_id,
3901
+ tag_name: resolveProtectedTagName(data.tag_name, data.name),
3902
+ create_access_level: data.create_access_level,
3903
+ }));
3904
+ export const UnprotectTagSchema = ProjectParamsSchema.extend({
3905
+ tag_name: z.string().describe("Name of the protected tag or wildcard to unprotect"),
3906
+ });
3907
+ export const GitLabProtectedTagAccessLevelSchema = z.object({
3908
+ id: z.number().optional(),
3909
+ access_level: z.number().nullable().optional(),
3910
+ access_level_description: z.string().optional(),
3911
+ deploy_key_id: z.number().nullable().optional(),
3912
+ // GitLab returns null for role-based access levels (not user-/group-specific)
3913
+ user_id: z.number().nullable().optional(),
3914
+ group_id: z.number().nullable().optional(),
3915
+ });
3916
+ export const GitLabProtectedTagSchema = z.object({
3917
+ name: z.string(),
3918
+ create_access_levels: z.array(GitLabProtectedTagAccessLevelSchema).optional(),
3919
+ });
3823
3920
  export const GitLabTagSchema = z.object({
3824
3921
  name: z.string(),
3825
3922
  message: z.string().nullable(),
@@ -1,7 +1,7 @@
1
1
  import { toJSONSchema } from "../utils/schema.js";
2
2
  import { USE_GITLAB_WIKI, USE_MILESTONE, USE_PIPELINE, SSE, STREAMABLE_HTTP } from "../config.js";
3
3
  import { getToolDescription } from "./tool-descriptions.js";
4
- import { ApproveMergeRequestSchema, BulkPublishDraftNotesSchema, CancelPipelineJobSchema, ErasePipelineJobSchema, CancelPipelineSchema, ConvertWorkItemTypeSchema, CreateBranchSchema, CreateDraftNoteSchema, CreateGroupSchema, CreateGroupWikiPageSchema, CreateIssueLinkSchema, CreateIssueNoteSchema, CreateIssueSchema, CreateIssueEmojiReactionSchema, CreateIssueNoteEmojiReactionSchema, ListIssueEmojiReactionsSchema, ListIssueNoteEmojiReactionsSchema, CreateLabelSchema, MarkAllTodosDoneSchema, ListTodosSchema, MarkTodoDoneSchema, CreateMergeRequestDiscussionNoteSchema, CreateMergeRequestEmojiReactionSchema, ListMergeRequestEmojiReactionsSchema, ListMergeRequestNoteEmojiReactionsSchema, CreateMergeRequestNoteSchema, CreateMergeRequestNoteEmojiReactionSchema, CreateMergeRequestSchema, CreateMergeRequestThreadSchema, CreateNoteSchema, CreateCommitStatusSchema, CreateOrUpdateFileSchema, CreatePipelineSchema, CreateProjectMilestoneSchema, CreateGroupMilestoneSchema, CreateReleaseEvidenceSchema, CreateReleaseSchema, CreateRepositorySchema, CreateTagSchema, CreateTimelineEventSchema, CreateWebhookSchema, CreateWikiPageSchema, CreateWorkItemNoteSchema, CreateWorkItemEmojiReactionSchema, CreateWorkItemNoteEmojiReactionSchema, ListWorkItemEmojiReactionsSchema, ListWorkItemNoteEmojiReactionsSchema, CreateWorkItemSchema, DeleteBranchSchema, GetProtectedBranchSchema, ListProtectedBranchesSchema, ProtectBranchSchema, UnprotectBranchSchema, UpdateDefaultBranchSchema, DeleteDraftNoteSchema, DeleteGroupMilestoneSchema, DeleteGroupWikiPageSchema, DeleteIssueLinkSchema, DeleteIssueSchema, DeleteIssueEmojiReactionSchema, DeleteIssueNoteEmojiReactionSchema, DeleteLabelSchema, DeleteMergeRequestDiscussionNoteSchema, DeleteMergeRequestNoteSchema, DeleteMergeRequestEmojiReactionSchema, DeleteMergeRequestNoteEmojiReactionSchema, DeleteProjectMilestoneSchema, DeleteReleaseSchema, DeleteTagSchema, DeleteWebhookSchema, DeleteWikiPageSchema, DeleteWorkItemEmojiReactionSchema, DeleteWorkItemNoteEmojiReactionSchema, DownloadAttachmentSchema, DownloadAttachmentRemoteSchema, DownloadJobArtifactsSchema, DownloadJobArtifactsRemoteSchema, DownloadReleaseAssetSchema, EditProjectMilestoneSchema, EditGroupMilestoneSchema, ExecuteGraphQLSchema, ForkRepositorySchema, HealthCheckSchema, GetBranchSchema, GetBranchDiffsSchema, GetCommitDiffSchema, GetCommitSchema, GetFileBlameSchema, GetDeploymentSchema, CreateDeploymentSchema, UpdateDeploymentSchema, DeploymentApprovalSchema, ListDeploymentMergeRequestsSchema, GetDraftNoteSchema, GetEnvironmentSchema, UpdateEnvironmentSchema, StopEnvironmentSchema, StopStaleEnvironmentsSchema, DeleteReviewAppEnvironmentsSchema, PipelineTriggerIdSchema, ListPipelineTriggersSchema, CreatePipelineTriggerSchema, UpdatePipelineTriggerSchema, TriggerPipelineSchema, GetFileContentsSchema, GetGroupWikiPageSchema, GetIssueLinkSchema, GetIssueSchema, GetJobArtifactFileSchema, GetLabelSchema, GetMergeRequestApprovalStateSchema, GetMergeRequestConflictsSchema, GetMergeRequestDiffsSchema, GetMergeRequestFileDiffSchema, GetMergeRequestDiscussionSchema, GetMergeRequestNoteSchema, GetMergeRequestNotesSchema, GetMergeRequestSchema, GetMergeRequestVersionSchema, GetMilestoneBurndownEventsSchema, GetMilestoneIssuesSchema, GetMilestoneMergeRequestsSchema, GetGroupMilestoneSchema, GetGroupMilestoneIssuesSchema, GetGroupMilestoneMergeRequestsSchema, GetGroupMilestoneBurndownEventsSchema, GetNamespaceSchema, GetPipelineJobOutputSchema, PipelineJobControlSchema, GetPipelineSchema, GetPipelineVariablesSchema, UpdatePipelineMetadataSchema, DeletePipelineSchema, PipelineReportSchema, WaitForPipelineSchema, WaitForPipelineJobSchema, PlayPipelineJobsSchema, GetProjectEventsSchema, GetProjectMilestoneSchema, GetProjectSchema, GetReleaseSchema, GetRepositoryTreeSchema, GetTagSchema, GetTagSignatureSchema, GetTimelineEventsSchema, GetUsersSchema, GetUserSchema, WhoAmISchema, GetWebhookEventSchema, GetWikiPageSchema, GetWorkItemSchema, ListBranchesSchema, ListCommitsSchema, ListCommitStatusesSchema, ListCustomFieldDefinitionsSchema, ListDeploymentsSchema, ListDraftNotesSchema, ListEnvironmentsSchema, ListEventsSchema, ListGroupIterationsSchema, ListGroupMilestonesSchema, ListGroupProjectsSchema, ListGroupWikiPagesSchema, ListIssueDiscussionsSchema, ListIssueLinksSchema, ListIssuesSchema, ListJobArtifactsSchema, ListLabelsSchema, ListMergeRequestChangedFilesSchema, ListMergeRequestDiffsSchema, ListMergeRequestDiscussionsSchema, ListMergeRequestPipelinesSchema, ListMergeRequestVersionsSchema, ListMergeRequestsSchema, ListGroupMergeRequestsSchema, ListNamespacesSchema, ListPipelineJobsSchema, ListPipelineTriggerJobsSchema, ValidateCiLintSchema, ValidateProjectCiLintSchema, ListCiCatalogResourcesSchema, GetCiCatalogResourceSchema, ListPipelinesSchema, ListPipelineSchedulesSchema, GetPipelineScheduleSchema, ListPipelineSchedulePipelinesSchema, CreatePipelineScheduleSchema, UpdatePipelineScheduleSchema, DeletePipelineScheduleSchema, PlayPipelineScheduleSchema, TakeOwnershipPipelineScheduleSchema, GetPipelineScheduleVariableSchema, CreatePipelineScheduleVariableSchema, UpdatePipelineScheduleVariableSchema, DeletePipelineScheduleVariableSchema, ListGroupMembersSchema, ListProjectMembersSchema, ListProjectMilestonesSchema, ListProjectsSchema, ListReleasesSchema, ListTagsSchema, ListWebhookEventsSchema, ListWebhooksSchema, ListWikiPagesSchema, ListWorkItemNotesSchema, ListWorkItemStatusesSchema, ListWorkItemsSchema, MarkdownUploadSchema, MarkdownUploadRemoteSchema, MergeMergeRequestSchema, MoveWorkItemSchema, MyIssuesSchema, OrbitQuerySchema, OrbitSchemaSchema, OrbitStatusSchema, OrbitToolsSchema, PlayPipelineJobSchema, PromoteProjectMilestoneSchema, PublishDraftNoteSchema, PushFilesSchema, ResolveMergeRequestThreadSchema, RetryPipelineJobSchema, RetryPipelineSchema, SearchCodeSchema, SearchGroupCodeSchema, SearchProjectCodeSchema, SearchRepositoriesSchema, ListSnippetsSchema, GetSnippetSchema, CreateSnippetSchema, UpdateSnippetSchema, DeleteSnippetSchema, UnapproveMergeRequestSchema, UpdateDraftNoteSchema, UpdateGroupWikiPageSchema, UpdateIssueNoteSchema, UpdateIssueSchema, UpdateIssueDescriptionPatchSchema, UpdateLabelSchema, UpdateProjectSchema, UpdateMergeRequestDiscussionNoteSchema, UpdateMergeRequestNoteSchema, UpdateMergeRequestSchema, UpdateReleaseSchema, UpdateWebhookSchema, UpdateWikiPageSchema, UpdateWorkItemSchema, VerifyNamespaceSchema, ListProjectVariablesSchema, GetProjectVariableSchema, CreateProjectVariableSchema, UpdateProjectVariableSchema, DeleteProjectVariableSchema, ListGroupVariablesSchema, GetGroupVariableSchema, CreateGroupVariableSchema, UpdateGroupVariableSchema, DeleteGroupVariableSchema, GetDependencyProxySettingsSchema, UpdateDependencyProxySettingsSchema, ListDependencyProxyBlobsSchema, PurgeDependencyProxyCacheSchema, ListProjectVulnerabilitiesSchema, GetVulnerabilitySchema, DismissVulnerabilitySchema, ConfirmVulnerabilitySchema, } from "../schemas.js";
4
+ import { ApproveMergeRequestSchema, BulkPublishDraftNotesSchema, CancelPipelineJobSchema, ErasePipelineJobSchema, CancelPipelineSchema, ConvertWorkItemTypeSchema, CreateBranchSchema, CreateDraftNoteSchema, CreateGroupSchema, CreateGroupWikiPageSchema, CreateIssueLinkSchema, CreateIssueNoteSchema, CreateIssueSchema, CreateIssueEmojiReactionSchema, CreateIssueNoteEmojiReactionSchema, ListIssueEmojiReactionsSchema, ListIssueNoteEmojiReactionsSchema, CreateLabelSchema, MarkAllTodosDoneSchema, ListTodosSchema, MarkTodoDoneSchema, CreateMergeRequestDiscussionNoteSchema, CreateMergeRequestEmojiReactionSchema, ListMergeRequestEmojiReactionsSchema, ListMergeRequestNoteEmojiReactionsSchema, CreateMergeRequestNoteSchema, CreateMergeRequestNoteEmojiReactionSchema, CreateMergeRequestSchema, CreateMergeRequestThreadSchema, CreateNoteSchema, CreateCommitStatusSchema, CreateOrUpdateFileSchema, CreatePipelineSchema, CreateProjectMilestoneSchema, CreateGroupMilestoneSchema, CreateReleaseEvidenceSchema, CreateReleaseSchema, CreateRepositorySchema, CreateTagSchema, CreateTimelineEventSchema, CreateWebhookSchema, CreateWikiPageSchema, CreateWorkItemNoteSchema, CreateWorkItemEmojiReactionSchema, CreateWorkItemNoteEmojiReactionSchema, ListWorkItemEmojiReactionsSchema, ListWorkItemNoteEmojiReactionsSchema, CreateWorkItemSchema, DeleteBranchSchema, GetProtectedBranchSchema, ListProtectedBranchesSchema, ProtectBranchSchema, ProtectTagSchema, UnprotectBranchSchema, UnprotectTagSchema, UpdateDefaultBranchSchema, DeleteDraftNoteSchema, DeleteGroupMilestoneSchema, DeleteGroupWikiPageSchema, DeleteIssueLinkSchema, DeleteIssueSchema, DeleteIssueEmojiReactionSchema, DeleteIssueNoteEmojiReactionSchema, DeleteLabelSchema, DeleteMergeRequestDiscussionNoteSchema, DeleteMergeRequestNoteSchema, DeleteMergeRequestEmojiReactionSchema, DeleteMergeRequestNoteEmojiReactionSchema, DeleteProjectMilestoneSchema, DeleteReleaseSchema, DeleteTagSchema, DeleteWebhookSchema, DeleteWikiPageSchema, DeleteWorkItemEmojiReactionSchema, DeleteWorkItemNoteEmojiReactionSchema, DownloadAttachmentSchema, DownloadAttachmentRemoteSchema, DownloadJobArtifactsSchema, DownloadJobArtifactsRemoteSchema, DownloadReleaseAssetSchema, EditProjectMilestoneSchema, EditGroupMilestoneSchema, ExecuteGraphQLSchema, ForkRepositorySchema, HealthCheckSchema, GetBranchSchema, GetBranchDiffsSchema, GetCommitDiffSchema, GetCommitSchema, GetFileBlameSchema, GetDeploymentSchema, CreateDeploymentSchema, UpdateDeploymentSchema, DeploymentApprovalSchema, ListDeploymentMergeRequestsSchema, GetDraftNoteSchema, GetEnvironmentSchema, UpdateEnvironmentSchema, StopEnvironmentSchema, StopStaleEnvironmentsSchema, DeleteReviewAppEnvironmentsSchema, PipelineTriggerIdSchema, ListPipelineTriggersSchema, CreatePipelineTriggerSchema, UpdatePipelineTriggerSchema, TriggerPipelineSchema, GetFileContentsSchema, GetGroupWikiPageSchema, GetIssueLinkSchema, GetIssueSchema, GetJobArtifactFileSchema, GetLabelSchema, GetMergeRequestApprovalStateSchema, GetMergeRequestConflictsSchema, GetMergeRequestDiffsSchema, GetMergeRequestFileDiffSchema, GetMergeRequestDiscussionSchema, GetMergeRequestNoteSchema, GetMergeRequestNotesSchema, GetMergeRequestSchema, GetMergeRequestVersionSchema, GetMilestoneBurndownEventsSchema, GetMilestoneIssuesSchema, GetMilestoneMergeRequestsSchema, GetGroupMilestoneSchema, GetGroupMilestoneIssuesSchema, GetGroupMilestoneMergeRequestsSchema, GetGroupMilestoneBurndownEventsSchema, GetNamespaceSchema, GetPipelineJobOutputSchema, PipelineJobControlSchema, GetPipelineSchema, GetRunnerSchema, GetPipelineVariablesSchema, UpdatePipelineMetadataSchema, DeletePipelineSchema, PipelineReportSchema, WaitForPipelineSchema, WaitForPipelineJobSchema, PlayPipelineJobsSchema, GetProjectEventsSchema, GetProjectMilestoneSchema, GetProjectSchema, GetReleaseSchema, GetRepositoryTreeSchema, GetTagSchema, GetTagSignatureSchema, GetProtectedTagSchema, GetTimelineEventsSchema, GetUsersSchema, GetUserSchema, WhoAmISchema, GetWebhookEventSchema, GetWikiPageSchema, GetWorkItemSchema, ListBranchesSchema, ListCommitsSchema, ListCommitStatusesSchema, ListCustomFieldDefinitionsSchema, ListDeploymentsSchema, ListDraftNotesSchema, ListEnvironmentsSchema, ListEventsSchema, ListGroupIterationsSchema, ListGroupMilestonesSchema, ListGroupProjectsSchema, ListGroupWikiPagesSchema, ListIssueDiscussionsSchema, ListIssueLinksSchema, ListIssuesSchema, ListJobArtifactsSchema, ListLabelsSchema, ListMergeRequestChangedFilesSchema, ListMergeRequestDiffsSchema, ListMergeRequestDiscussionsSchema, ListMergeRequestPipelinesSchema, ListMergeRequestVersionsSchema, ListMergeRequestsSchema, ListGroupMergeRequestsSchema, ListNamespacesSchema, ListPipelineJobsSchema, ListPipelineTriggerJobsSchema, ValidateCiLintSchema, ValidateProjectCiLintSchema, ListCiCatalogResourcesSchema, GetCiCatalogResourceSchema, ListPipelinesSchema, ListPipelineSchedulesSchema, GetPipelineScheduleSchema, ListPipelineSchedulePipelinesSchema, CreatePipelineScheduleSchema, UpdatePipelineScheduleSchema, DeletePipelineScheduleSchema, PlayPipelineScheduleSchema, TakeOwnershipPipelineScheduleSchema, GetPipelineScheduleVariableSchema, CreatePipelineScheduleVariableSchema, UpdatePipelineScheduleVariableSchema, DeletePipelineScheduleVariableSchema, ListGroupMembersSchema, ListProjectMembersSchema, ListProjectMilestonesSchema, ListProjectsSchema, ListProjectRunnersSchema, ListReleasesSchema, ListTagsSchema, ListProtectedTagsSchema, ListWebhookEventsSchema, ListWebhooksSchema, ListWikiPagesSchema, ListWorkItemNotesSchema, ListWorkItemStatusesSchema, ListWorkItemsSchema, MarkdownUploadSchema, MarkdownUploadRemoteSchema, MergeMergeRequestSchema, MoveWorkItemSchema, MyIssuesSchema, OrbitQuerySchema, OrbitSchemaSchema, OrbitStatusSchema, OrbitToolsSchema, PlayPipelineJobSchema, PromoteProjectMilestoneSchema, PublishDraftNoteSchema, PushFilesSchema, ResolveMergeRequestThreadSchema, RetryPipelineJobSchema, RetryPipelineSchema, SearchCodeSchema, SearchGroupCodeSchema, SearchProjectCodeSchema, SearchRepositoriesSchema, ListSnippetsSchema, GetSnippetSchema, CreateSnippetSchema, UpdateSnippetSchema, DeleteSnippetSchema, UnapproveMergeRequestSchema, UpdateDraftNoteSchema, UpdateGroupWikiPageSchema, UpdateIssueNoteSchema, UpdateIssueSchema, UpdateIssueDescriptionPatchSchema, UpdateLabelSchema, UpdateProjectSchema, UpdateMergeRequestDiscussionNoteSchema, UpdateMergeRequestNoteSchema, UpdateMergeRequestSchema, UpdateReleaseSchema, UpdateWebhookSchema, UpdateWikiPageSchema, UpdateWorkItemSchema, VerifyNamespaceSchema, ListProjectVariablesSchema, GetProjectVariableSchema, CreateProjectVariableSchema, UpdateProjectVariableSchema, DeleteProjectVariableSchema, ListGroupVariablesSchema, GetGroupVariableSchema, CreateGroupVariableSchema, UpdateGroupVariableSchema, DeleteGroupVariableSchema, GetDependencyProxySettingsSchema, UpdateDependencyProxySettingsSchema, ListDependencyProxyBlobsSchema, PurgeDependencyProxyCacheSchema, ListProjectVulnerabilitiesSchema, GetVulnerabilitySchema, DismissVulnerabilitySchema, ConfirmVulnerabilitySchema, } from "../schemas.js";
5
5
  const IS_REMOTE = SSE || STREAMABLE_HTTP;
6
6
  // Define all available tools
7
7
  export const allTools = [
@@ -132,27 +132,27 @@ export const allTools = [
132
132
  },
133
133
  {
134
134
  name: "get_merge_request",
135
- description: "Get details of a merge request (mergeRequestIid or branchName required). Set include_summaries=true for deployment/commit/approval summaries",
135
+ description: "Get details of a merge request (merge_request_iid or source_branch required). Set include_summaries=true for deployment/commit/approval summaries",
136
136
  inputSchema: toJSONSchema(GetMergeRequestSchema),
137
137
  },
138
138
  {
139
139
  name: "get_merge_request_diffs",
140
- description: "Get the changes/diffs of a merge request (mergeRequestIid or branchName required)",
140
+ description: "Get the changes/diffs of a merge request (merge_request_iid or source_branch required)",
141
141
  inputSchema: toJSONSchema(GetMergeRequestDiffsSchema),
142
142
  },
143
143
  {
144
144
  name: "list_merge_request_changed_files",
145
- description: "List changed file paths in a merge request without diff content (mergeRequestIid or branchName required)",
145
+ description: "List changed file paths in a merge request without diff content (merge_request_iid or source_branch required)",
146
146
  inputSchema: toJSONSchema(ListMergeRequestChangedFilesSchema),
147
147
  },
148
148
  {
149
149
  name: "list_merge_request_diffs",
150
- description: "List merge request diffs with pagination (mergeRequestIid or branchName required)",
150
+ description: "List merge request diffs with pagination (merge_request_iid or source_branch required)",
151
151
  inputSchema: toJSONSchema(ListMergeRequestDiffsSchema),
152
152
  },
153
153
  {
154
154
  name: "get_merge_request_file_diff",
155
- description: "Get diffs for specific files from a merge request (mergeRequestIid or branchName required)",
155
+ description: "Get diffs for specific files from a merge request (merge_request_iid or source_branch required)",
156
156
  inputSchema: toJSONSchema(GetMergeRequestFileDiffSchema),
157
157
  },
158
158
  {
@@ -172,7 +172,7 @@ export const allTools = [
172
172
  },
173
173
  {
174
174
  name: "update_merge_request",
175
- description: "Update a merge request (mergeRequestIid or branchName required)",
175
+ description: "Update a merge request (merge_request_iid or source_branch required)",
176
176
  inputSchema: toJSONSchema(UpdateMergeRequestSchema),
177
177
  },
178
178
  {
@@ -1088,6 +1088,26 @@ export const allTools = [
1088
1088
  description: "Get the X.509 signature of a signed tag (404 if unsigned)",
1089
1089
  inputSchema: toJSONSchema(GetTagSignatureSchema),
1090
1090
  },
1091
+ {
1092
+ name: "list_protected_tags",
1093
+ description: "List protected tags in a project",
1094
+ inputSchema: toJSONSchema(ListProtectedTagsSchema),
1095
+ },
1096
+ {
1097
+ name: "get_protected_tag",
1098
+ description: "Get details of a single protected tag or wildcard (create access levels)",
1099
+ inputSchema: toJSONSchema(GetProtectedTagSchema),
1100
+ },
1101
+ {
1102
+ name: "protect_tag",
1103
+ description: "Protect a repository tag or wildcard (set who can create it)",
1104
+ inputSchema: toJSONSchema(ProtectTagSchema),
1105
+ },
1106
+ {
1107
+ name: "unprotect_tag",
1108
+ description: "Remove protection from a previously protected tag or wildcard",
1109
+ inputSchema: toJSONSchema(UnprotectTagSchema),
1110
+ },
1091
1111
  // --- Snippet tools ---
1092
1112
  {
1093
1113
  name: "list_snippets",
@@ -1366,6 +1386,16 @@ export const allTools = [
1366
1386
  description: "List the MCP tool definitions exposed by GitLab Orbit",
1367
1387
  inputSchema: toJSONSchema(OrbitToolsSchema),
1368
1388
  },
1389
+ {
1390
+ name: "list_project_runners",
1391
+ description: "List runners available to a project",
1392
+ inputSchema: toJSONSchema(ListProjectRunnersSchema),
1393
+ },
1394
+ {
1395
+ name: "get_runner",
1396
+ description: "Get one runner by ID",
1397
+ inputSchema: toJSONSchema(GetRunnerSchema),
1398
+ },
1369
1399
  // --- Meta tool: Dynamic tool discovery ---
1370
1400
  {
1371
1401
  name: "discover_tools",
@@ -1446,6 +1476,8 @@ export const readOnlyTools = new Set([
1446
1476
  "list_pipeline_trigger_jobs",
1447
1477
  "get_pipeline_job",
1448
1478
  "get_pipeline_job_output",
1479
+ "list_project_runners",
1480
+ "get_runner",
1449
1481
  "wait_for_pipeline",
1450
1482
  "wait_for_job",
1451
1483
  "validate_ci_lint",
@@ -1492,6 +1524,8 @@ export const readOnlyTools = new Set([
1492
1524
  "list_tags",
1493
1525
  "get_tag",
1494
1526
  "get_tag_signature",
1527
+ "list_protected_tags",
1528
+ "get_protected_tag",
1495
1529
  "list_snippets",
1496
1530
  "get_snippet",
1497
1531
  "get_merge_request_approval_state",
@@ -1567,6 +1601,8 @@ export const destructiveTools = new Set([
1567
1601
  "delete_branch",
1568
1602
  "unprotect_branch",
1569
1603
  "protect_branch",
1604
+ "unprotect_tag",
1605
+ "protect_tag",
1570
1606
  "update_default_branch",
1571
1607
  "merge_merge_request",
1572
1608
  "push_files",
@@ -1614,12 +1650,13 @@ export const deleteTools = new Set([
1614
1650
  "delete_work_item_note_emoji_reaction",
1615
1651
  "purge_dependency_proxy_cache",
1616
1652
  // Destructive teardown operations whose names do not start with `delete_`:
1617
- // stopping/cancelling live pipelines and environments, and removing branch protection.
1653
+ // stopping/cancelling live pipelines and environments, and removing branch or tag protection.
1618
1654
  "cancel_pipeline",
1619
1655
  "cancel_pipeline_job",
1620
1656
  "stop_environment",
1621
1657
  "stop_stale_environments",
1622
1658
  "unprotect_branch",
1659
+ "unprotect_tag",
1623
1660
  ]);
1624
1661
  // Define which tools are related to wiki and can be toggled by USE_GITLAB_WIKI
1625
1662
  export const wikiToolNames = new Set([
@@ -1715,6 +1752,8 @@ export const pipelineToolNames = new Set([
1715
1752
  "list_job_artifacts",
1716
1753
  "download_job_artifacts",
1717
1754
  "get_job_artifact_file",
1755
+ "list_project_runners",
1756
+ "get_runner",
1718
1757
  ]);
1719
1758
  export const TOOLSET_DEFINITIONS = [
1720
1759
  {
@@ -1973,6 +2012,8 @@ export const TOOLSET_DEFINITIONS = [
1973
2012
  "list_job_artifacts",
1974
2013
  "download_job_artifacts",
1975
2014
  "get_job_artifact_file",
2015
+ "list_project_runners",
2016
+ "get_runner",
1976
2017
  ]),
1977
2018
  },
1978
2019
  {
@@ -2030,7 +2071,17 @@ export const TOOLSET_DEFINITIONS = [
2030
2071
  {
2031
2072
  id: "tags",
2032
2073
  isDefault: false,
2033
- tools: new Set(["list_tags", "get_tag", "create_tag", "delete_tag", "get_tag_signature"]),
2074
+ tools: new Set([
2075
+ "list_tags",
2076
+ "get_tag",
2077
+ "create_tag",
2078
+ "delete_tag",
2079
+ "get_tag_signature",
2080
+ "list_protected_tags",
2081
+ "get_protected_tag",
2082
+ "protect_tag",
2083
+ "unprotect_tag",
2084
+ ]),
2034
2085
  },
2035
2086
  {
2036
2087
  id: "snippets",
@@ -9,7 +9,7 @@ const TOOL_GUIDANCE = {
9
9
  push_files: "Use this to commit several file changes atomically; use `create_or_update_file` when only one path is involved. Each file defaults to action `create`; optional per-file `action` (create/update/delete/move) and `encoding` (text/base64) are additive. `GITLAB_PERMISSION_MODE=modify` rejects `delete` and `move`. The operation writes repository history on the selected branch, requires repository write permission, and returns the commit result or a validation, conflict, or protected-branch error.",
10
10
  create_issue: "Use this to open a new issue; use `update_issue` for an existing issue and `create_issue_note` to add discussion without changing issue fields. The operation creates remote project data, requires issue creation permission, and returns the new issue or a validation, permission, or duplicate-related error.",
11
11
  list_issues: "Use this for issue management: list GitLab issues, optionally scoped with `project_id`. Use `get_issue` when the issue iid is already known and `my_issues` for issues assigned to the current user. It is read-only and paginated, requires issue read permission, and returns issue records or GitLab errors for invalid identifiers, missing resources, or rate limits.",
12
- my_issues: "Use this for issue management: list issues assigned to the authenticated user. Use `list_issues` for project-wide or author-scoped listing and `get_issue` for one issue. It is read-only and paginated, requires authentication, and returns assigned issue records or permission/rate-limit errors.",
12
+ my_issues: "Use this for issue management: list issues assigned to the authenticated user. Use `list_issues` for project-wide or author-scoped listing and `get_issue` for one issue. Resolving the current user requires classic read_user or fine-grained User:Read. It is read-only and paginated, requires authentication, and returns assigned issue records or permission/rate-limit errors.",
13
13
  get_issue: "Use this for issue management: inspect one issue's fields; use `list_issues` or `my_issues` to discover issues first. It is read-only, requires issue read permission, and returns the issue or an error when the identifier is invalid, the issue is missing, or access is denied.",
14
14
  create_merge_request: "Use this to open a new merge request from an existing source branch to a target branch; use `update_merge_request` after it exists. The operation creates remote review state, requires project access, and returns the new merge request or a validation, permission, branch, or duplicate-related error.",
15
15
  fork_repository: "Use this to create a copy of an existing project in the current user's namespace or a permitted namespace; use `search_repositories` or `get_project` to inspect projects without copying them. The operation creates a new project, requires fork permission, and returns the forked project or a namespace/permission error.",
@@ -17,6 +17,8 @@ const TOOL_GUIDANCE = {
17
17
  delete_branch: "Use this only after confirming the branch name and intended data loss; use `get_branch` or `list_branches` before deletion and never use it to remove branch protection. The operation permanently removes a remote branch, requires branch-delete permission, and returns the deletion result or a protected-branch, missing-resource, or permission error.",
18
18
  protect_branch: "Use this to create or update protection rules for a branch or wildcard; use `get_protected_branch` to inspect existing rules first. The operation changes who may push, merge, or unprotect, may enable force-push or code-owner settings, requires maintainer-level permission, and returns the protection rule or a validation/permission error.",
19
19
  unprotect_branch: "Use this to remove protection from an existing branch; use `protect_branch` to change access levels without removing the rule. The operation changes repository security controls, requires permission to manage protected branches, and returns the result or an error when the branch is missing or policy forbids the change.",
20
+ protect_tag: "Use this to protect a tag or wildcard such as `v*` so protected CI/CD variables reach tag pipelines; use `get_protected_tag` or `list_protected_tags` to inspect existing rules first. The operation changes who may create matching tags, requires maintainer-level permission, and returns the protection rule or a validation/permission error.",
21
+ unprotect_tag: "Use this to remove protection from an existing tag or wildcard. To change the create access level, use `unprotect_tag` first, then `protect_tag`; `protect_tag` creates a rule and does not update an existing rule. This change is unavailable in `modify` mode because `unprotect_tag` is blocked there. The operation changes repository security controls, requires permission to manage protected tags, and returns the result or an error when the tag is missing or policy forbids the change.",
20
22
  update_default_branch: "Use this to change which branch GitLab treats as the project's default; use `create_branch` to create a branch rather than changing project defaults. The operation changes project settings and may affect clone, merge request, and CI defaults, requires project-maintainer permission, and returns the updated project or a validation/permission error.",
21
23
  create_note: "Use this for a top-level comment on an issue or merge request when no typed discussion operation is needed; use `create_merge_request_thread` or `create_issue_note` for threaded replies. The operation creates remote discussion content, requires note permission, and returns the created note or a target/permission/validation error.",
22
24
  create_merge_request_thread: "Use this to start a review thread on a merge request; use `create_merge_request_note` for an unthreaded note and `create_merge_request_discussion_note` to reply to an existing thread. The operation creates remote review content, requires note permission, and returns the discussion or a position/permission/validation error.",
@@ -36,8 +38,10 @@ const TOOL_GUIDANCE = {
36
38
  download_attachment: "Use this to retrieve a previously uploaded project attachment; remote mode returns inline base64 for images or a download URL, while local mode can save to a path. It is read-only with respect to GitLab, requires project access, and returns the file content or an attachment/permission error.",
37
39
  execute_graphql: "Use this only when a supported GitLab REST tool does not cover the requested operation; prefer a typed tool when one exists. The query is sent directly to GitLab and can include mutations when permission allows, so callers must treat it as potentially state-changing and handle GraphQL errors in the returned response.",
38
40
  discover_tools: "Use this when a needed opt-in category is not currently exposed; omit `category` to inspect available categories, then call it with a category to activate that group for the current session. It changes only the session's tool registry, returns the active-tool summary, and does not change GitLab data.",
39
- health_check: "Use this to verify server connectivity and authentication before making GitLab requests; use `whoami` when the authenticated user's identity is the goal. It does not mutate GitLab state and returns server/authentication status plus GitLab version details when available.",
40
- whoami: "Use this to identify the authenticated GitLab user; use `get_user` or `get_users` when looking up another user. It is read-only and returns the current user profile, while missing credentials or GitLab permission failures are reported as errors.",
41
+ health_check: "Use this to verify server connectivity and authentication before making GitLab requests; use `whoami` when the authenticated user's identity is the goal. A 403 from GET /user with an insufficient_scope error means the token is accepted but User API scope is missing (read_user or fine-grained User:Read); the check stays ok, sets user_api_warning, and omits version metadata. Rate-limit and authentication-ban 403 responses stay unauthenticated. It does not mutate GitLab state and returns server/authentication status plus GitLab version details when available. Connection failures with no HTTP response return status error, authenticated false, and an error object (kind tls, dns, timeout, or network, plus code and message) instead of a protocol error.",
42
+ whoami: "Use this to identify the authenticated GitLab user; use `get_user` or `get_users` when looking up another user. It requires classic read_user or fine-grained User:Read. It is read-only and returns the current user profile, while missing credentials or GitLab permission failures are reported as errors.",
43
+ list_project_runners: "Use this when a job is stuck pending with no matching runner and you need the runners a project can use, including ancestor group runners and allowed instance runners. Filter with type, status, paused, and tag_list; the list payload includes id, description, runner_type, status, and paused (among other non-secret GitLab runner fields) but not tags — call `get_runner` for tag_list, run_untagged, locked, and access_level. It is read-only and paginated, requires Maintainer or Auditor access on the project, and never returns a runner registration token (runners_token) or runner authentication token.",
44
+ get_runner: "Use this after list_project_runners when you already have a runner ID and need tag_list, run_untagged, locked, and access_level to explain a stuck pending job. Instance runner details are visible to authenticated users; group and project runners follow GitLab's role checks for that runner. When GITLAB_ALLOWED_PROJECT_IDS or a session project scope is active, the runner must appear on an allowed project's runner list before details are returned — pass project_id when more than one project is allowed — projects outside that allowlist are omitted, and groups are omitted because a project allowlist cannot verify them. GITLAB_PROJECT_ID alone is only the default project for that membership check when project_id is omitted. It does not omit other projects or groups. It is read-only and never returns a runner registration token (runners_token) or runner authentication token. token_expires_at is expiry metadata, not the token.",
41
45
  list_pipeline_schedules: "Use this to survey a project's scheduled pipelines and their `cron`, `next_run_at`, and `active` state, optionally narrowed with `scope` to `active` or `inactive`; use `list_pipelines` for pipelines that already ran and `get_pipeline_schedule` when the schedule ID is known. The list response carries no `variables` — call `get_pipeline_schedule` for those — and includes `inputs` only for Maintainers, Owners, or the schedule owner. It is read-only and paginated, requires project access, and returns schedule records or an error when the project is missing or access is denied.",
42
46
  get_pipeline_schedule: "Use this to inspect one schedule's cron, owner, last pipeline, variables, and inputs; use `list_pipeline_schedules` to discover schedule IDs and `list_pipeline_schedule_pipelines` for the pipelines it produced. It is read-only, returns `variables` and `inputs` only to Maintainers, Owners, or the schedule owner, and returns the schedule or an error when the schedule is missing or access is denied.",
43
47
  list_pipeline_schedule_pipelines: "Use this to review the pipelines a schedule has actually triggered, for example to see whether the nightly run succeeded; use `get_pipeline` for one pipeline's full detail and `list_pipelines` for project-wide filtering. Narrow the result with `scope`, `status`, `sort`, and the `created_*` / `updated_*` date bounds. It is read-only and paginated, requires project access, and returns pipeline records or an error when the schedule is missing or access is denied.",
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Classify fetch failures that never produced an HTTP response.
3
+ * health_check must report these as a normal tool result: the caller is
4
+ * already asking whether the connection works, so a JSON-RPC -32603 hides the
5
+ * only signal they came for.
6
+ *
7
+ * node-fetch puts the OS code on `code` / `errno`. undici leaves `code` empty
8
+ * on the TypeError and stores it on `cause.code` (errno there is numeric).
9
+ */
10
+ import { redactGitLabUrlCredentials, redactUrlSecretsInText } from "./redact-url.js";
11
+ export { redactGitLabUrlCredentials };
12
+ const TLS_CODES = new Set([
13
+ "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
14
+ "UNABLE_TO_GET_ISSUER_CERT",
15
+ "UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
16
+ "SELF_SIGNED_CERT_IN_CHAIN",
17
+ "DEPTH_ZERO_SELF_SIGNED_CERT",
18
+ "CERT_HAS_EXPIRED",
19
+ "ERR_TLS_CERT_ALTNAME_INVALID",
20
+ ]);
21
+ const TLS_CODE_PREFIXES = ["CERT_", "ERR_TLS_", "ERR_SSL_"];
22
+ const DNS_CODES = new Set(["ENOTFOUND", "EAI_AGAIN"]);
23
+ const TIMEOUT_CODES = new Set(["ETIMEDOUT", "UND_ERR_CONNECT_TIMEOUT"]);
24
+ const TIMEOUT_ERROR_NAMES = new Set(["AbortError", "TimeoutError"]);
25
+ const REQUEST_TIMEOUT_TYPE = "request-timeout";
26
+ const TLS_CA_HINT = "Make sure NODE_EXTRA_CA_CERTS or GITLAB_CA_CERT_PATH contains the full certificate chain, including intermediate CAs.";
27
+ const FALLBACK_MESSAGE = "GitLab request failed before an HTTP response";
28
+ export function classifyHealthCheckTransportError(error) {
29
+ const code = readTransportCode(error);
30
+ const kind = classifyKind(error, code);
31
+ const failure = {
32
+ kind,
33
+ code,
34
+ message: readTransportMessage(error),
35
+ };
36
+ if (kind === "tls") {
37
+ failure.hint = TLS_CA_HINT;
38
+ }
39
+ return failure;
40
+ }
41
+ function classifyKind(error, code) {
42
+ if (code !== null) {
43
+ return kindFromCode(code);
44
+ }
45
+ if (isTimeoutSignal(error) || isTimeoutSignal(readCause(error))) {
46
+ return "timeout";
47
+ }
48
+ return "network";
49
+ }
50
+ function kindFromCode(code) {
51
+ if (isTlsCode(code))
52
+ return "tls";
53
+ if (DNS_CODES.has(code))
54
+ return "dns";
55
+ if (TIMEOUT_CODES.has(code))
56
+ return "timeout";
57
+ return "network";
58
+ }
59
+ function isTlsCode(code) {
60
+ if (TLS_CODES.has(code))
61
+ return true;
62
+ return TLS_CODE_PREFIXES.some(prefix => code.startsWith(prefix));
63
+ }
64
+ function readTransportCode(error) {
65
+ const cause = readCause(error);
66
+ return (readStringField(error, "code") ??
67
+ readStringField(error, "errno") ??
68
+ readStringField(cause, "code") ??
69
+ readStringField(cause, "errno"));
70
+ }
71
+ function isTimeoutSignal(value) {
72
+ const name = readStringField(value, "name");
73
+ if (name !== null && TIMEOUT_ERROR_NAMES.has(name))
74
+ return true;
75
+ return readStringField(value, "type") === REQUEST_TIMEOUT_TYPE;
76
+ }
77
+ function readTransportMessage(error) {
78
+ if (typeof error === "string") {
79
+ const trimmed = error.trim();
80
+ return trimmed.length > 0 ? redactCredentials(trimmed) : FALLBACK_MESSAGE;
81
+ }
82
+ const combined = combineMessages(readStringField(error, "message"), readStringField(readCause(error), "message"));
83
+ if (combined.length === 0)
84
+ return FALLBACK_MESSAGE;
85
+ return redactCredentials(combined);
86
+ }
87
+ function combineMessages(outer, inner) {
88
+ if (outer && inner && !outer.includes(inner)) {
89
+ return `${outer}: ${inner}`;
90
+ }
91
+ return outer ?? inner ?? "";
92
+ }
93
+ function redactCredentials(message) {
94
+ return redactUrlSecretsInText(message)
95
+ .replace(/\bBearer\s+\S+/gi, "Bearer [REDACTED]")
96
+ .replace(/\b((?:private|job)-token|authorization)\b\s*[:=]\s*\S+/gi, "$1: [REDACTED]")
97
+ .replace(/\bgl(?:pat|dt|rt|pt|ft|oas|soat)-[A-Za-z0-9._-]+/gi, "[REDACTED]");
98
+ }
99
+ function readCause(value) {
100
+ if (!isRecord(value))
101
+ return undefined;
102
+ return value.cause;
103
+ }
104
+ function readStringField(value, key) {
105
+ if (!isRecord(value))
106
+ return null;
107
+ const field = value[key];
108
+ if (typeof field !== "string")
109
+ return null;
110
+ const trimmed = field.trim();
111
+ return trimmed.length > 0 ? trimmed : null;
112
+ }
113
+ function isRecord(value) {
114
+ return typeof value === "object" && value !== null;
115
+ }
@@ -25,30 +25,96 @@ const REDACT_PATHS = [
25
25
  "arguments.approval_password",
26
26
  "params.arguments.approval_password",
27
27
  ];
28
- function isPrettyFactory(value) {
29
- return typeof value === "function";
30
- }
28
+ const STDERR_FD = 2;
29
+ // Stay under sonic-boom's minLength so a startup warning is still buffered when
30
+ // flushStartupLogs runs. A larger line starts an async write immediately, and
31
+ // flushSync cannot pull that in-flight chunk back into order.
32
+ const ASYNC_LOG_MIN_LENGTH = 1024 * 1024;
33
+ const ASYNC_LOG_MAX_WRITE = ASYNC_LOG_MIN_LENGTH + 1;
34
+ const ASYNC_LOG_FLUSH_INTERVAL_MS = 100;
35
+ const startupFlushes = new WeakMap();
31
36
  function isPrettyModule(value) {
32
- if (typeof value !== "object" || value === null || !("default" in value)) {
37
+ if (typeof value !== "function" || !("prettyFactory" in value)) {
33
38
  return false;
34
39
  }
35
- return isPrettyFactory(value.default);
40
+ return typeof value.prettyFactory === "function";
41
+ }
42
+ function readPrettyModule(value) {
43
+ if (isPrettyModule(value)) {
44
+ return value;
45
+ }
46
+ if (typeof value === "object" && value !== null && "default" in value) {
47
+ if (isPrettyModule(value.default)) {
48
+ return value.default;
49
+ }
50
+ }
51
+ return undefined;
52
+ }
53
+ function loadPrettifier() {
54
+ const loaded = require("pino-pretty");
55
+ const prettyModule = readPrettyModule(loaded);
56
+ if (prettyModule === undefined) {
57
+ throw new Error("pino-pretty did not export a prettifier");
58
+ }
59
+ return prettyModule.prettyFactory({ colorize: true, levelFirst: true });
60
+ }
61
+ function createAsyncStderr() {
62
+ return pino.destination({
63
+ dest: STDERR_FD,
64
+ sync: false,
65
+ minLength: ASYNC_LOG_MIN_LENGTH,
66
+ maxWrite: ASYNC_LOG_MAX_WRITE,
67
+ periodicFlush: ASYNC_LOG_FLUSH_INTERVAL_MS,
68
+ });
36
69
  }
37
70
  /**
38
71
  * Pretty-print in process. The worker transport has the same text format, but
39
72
  * it spawns a thread on every logger, including stdio MCP startup.
40
73
  * LOG_FORMAT=json still skips pino-pretty entirely.
74
+ *
75
+ * Steady-state writes stay asynchronous. flushSync is only for startup warnings,
76
+ * so a full stderr pipe cannot block later request logs.
41
77
  */
42
- function createPrettyStream() {
43
- const loaded = require("pino-pretty");
44
- const options = { colorize: true, levelFirst: true, destination: 2 };
45
- if (isPrettyFactory(loaded)) {
46
- return loaded(options);
47
- }
48
- if (isPrettyModule(loaded)) {
49
- return loaded.default(options);
78
+ function createPrettyDestination() {
79
+ const prettify = loadPrettifier();
80
+ const sonic = createAsyncStderr();
81
+ return {
82
+ write(message) {
83
+ const source = message.endsWith("\n") ? message.slice(0, -1) : message;
84
+ const line = prettify(source);
85
+ if (typeof line !== "string" || line.length === 0) {
86
+ return;
87
+ }
88
+ sonic.write(line);
89
+ },
90
+ flush(callback) {
91
+ sonic.flush(callback);
92
+ },
93
+ flushSync() {
94
+ sonic.flushSync();
95
+ },
96
+ };
97
+ }
98
+ function rememberStartupFlush(logger, flushSync) {
99
+ startupFlushes.set(logger, flushSync);
100
+ }
101
+ /**
102
+ * Block until startup warnings already passed to this logger reach stderr.
103
+ * Steady-state logs stay buffered and are flushed asynchronously.
104
+ */
105
+ function flushDestination(logger) {
106
+ const flush = startupFlushes.get(logger);
107
+ if (flush === undefined) {
108
+ return;
50
109
  }
51
- throw new Error("pino-pretty did not export a stream factory");
110
+ flush();
111
+ }
112
+ export function flushStartupLogs(logger) {
113
+ flushDestination(logger);
114
+ }
115
+ /** Write buffered lines now. Info logs stay buffered until this or process exit. */
116
+ export function flushBufferedLogs(logger) {
117
+ flushDestination(logger);
52
118
  }
53
119
  /**
54
120
  * Create a pino logger with consistent redaction, level, format, and
@@ -71,7 +137,17 @@ export function createLogger(name) {
71
137
  },
72
138
  };
73
139
  if (isJson) {
74
- return pino(options, pino.destination(2));
140
+ const destination = createAsyncStderr();
141
+ const logger = pino(options, destination);
142
+ rememberStartupFlush(logger, () => {
143
+ destination.flushSync();
144
+ });
145
+ return logger;
75
146
  }
76
- return pino(options, createPrettyStream());
147
+ const destination = createPrettyDestination();
148
+ const logger = pino(options, destination);
149
+ rememberStartupFlush(logger, () => {
150
+ destination.flushSync();
151
+ });
152
+ return logger;
77
153
  }