@zereight/mcp-gitlab 2.2.6 → 2.3.1

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.
@@ -15,4 +15,6 @@ export const STATELESS_PURPOSES = {
15
15
  STORED_TOKENS: "pc",
16
16
  /** Sealed Mcp-Session-Id, carries session auth across pod hops. */
17
17
  SESSION_ID: "sid",
18
+ /** HMAC key for remembered callback-proxy consent cookies. */
19
+ OAUTH_CONSENT: "oc",
18
20
  };
@@ -38,3 +38,17 @@ export function selectExposedTools(input) {
38
38
  }
39
39
  return withoutHidden.filter(tool => !isExcludedBySlimProfile(tool.name, true, input.individuallyEnabledTools));
40
40
  }
41
+ /**
42
+ * Hidden tools that would be listed if the hidden policy were ignored.
43
+ * Hiding a tool keeps it callable only while toolsets, `GITLAB_TOOLS`, legacy
44
+ * flags, permission mode, and the regex denylist still expose it.
45
+ */
46
+ export function selectCallableHiddenToolNames(input) {
47
+ if (input.hiddenToolNames.size === 0) {
48
+ return new Set();
49
+ }
50
+ const exposedIgnoringHidden = selectExposedTools({ ...input, hiddenToolNames: new Set() });
51
+ return new Set(exposedIgnoringHidden
52
+ .map(tool => tool.name)
53
+ .filter(toolName => input.hiddenToolNames.has(toolName)));
54
+ }
@@ -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, 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";
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, RebaseMergeRequestSchema, 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
  import { GitLabApiRequestSchema } from "./gitlab-api-request.js";
6
6
  const IS_REMOTE = SSE || STREAMABLE_HTTP;
7
7
  // Define all available tools
@@ -1007,7 +1007,7 @@ export const allTools = [
1007
1007
  name: "download_attachment",
1008
1008
  description: IS_REMOTE
1009
1009
  ? "Download an uploaded file from a project (images returned inline as base64, other files returned as download URL)"
1010
- : "Download an uploaded file from a project (images returned as base64; use local_path to save to disk)",
1010
+ : "Download an uploaded file from a project (images returned as base64; other files, or images with local_path, are saved as new files under the download directory)",
1011
1011
  inputSchema: IS_REMOTE
1012
1012
  ? toJSONSchema(DownloadAttachmentRemoteSchema)
1013
1013
  : toJSONSchema(DownloadAttachmentSchema),
@@ -1397,6 +1397,11 @@ export const allTools = [
1397
1397
  description: "Get one runner by ID",
1398
1398
  inputSchema: toJSONSchema(GetRunnerSchema),
1399
1399
  },
1400
+ {
1401
+ name: "rebase_merge_request",
1402
+ description: "Rebase the source branch of a merge request onto its target branch",
1403
+ inputSchema: toJSONSchema(RebaseMergeRequestSchema),
1404
+ },
1400
1405
  // --- Explicit opt-in raw API. Not in `all` and not activatable via discover_tools. ---
1401
1406
  {
1402
1407
  name: "gitlab_api_request",
@@ -1570,8 +1575,14 @@ export const readOnlyTools = new Set([
1570
1575
  // Stays in readOnlyTools so readonly mode can still run queries, but mutations
1571
1576
  // are allowed in other permission modes and rejected only at call time.
1572
1577
  const MIXED_ACCESS_TOOLS = new Set(["execute_graphql", "gitlab_api_request"]);
1578
+ // Read-only on GitLab, but in stdio mode they save files to the local disk. They stay
1579
+ // listed in readonly mode, where the handlers refuse anything that would write a file
1580
+ // (only inline images are returned), and never advertise readOnlyHint.
1581
+ const LOCAL_FILE_WRITE_TOOLS = new Set(IS_REMOTE ? [] : ["download_attachment", "download_job_artifacts"]);
1573
1582
  export function advertisesReadOnly(toolName) {
1574
- return readOnlyTools.has(toolName) && !MIXED_ACCESS_TOOLS.has(toolName);
1583
+ return (readOnlyTools.has(toolName) &&
1584
+ !MIXED_ACCESS_TOOLS.has(toolName) &&
1585
+ !LOCAL_FILE_WRITE_TOOLS.has(toolName));
1575
1586
  }
1576
1587
  // Define which tools are destructive (data loss potential)
1577
1588
  export const destructiveTools = new Set([
@@ -1614,6 +1625,7 @@ export const destructiveTools = new Set([
1614
1625
  "protect_tag",
1615
1626
  "update_default_branch",
1616
1627
  "merge_merge_request",
1628
+ "rebase_merge_request",
1617
1629
  "push_files",
1618
1630
  "delete_project_variable",
1619
1631
  "delete_group_variable",
@@ -1817,6 +1829,7 @@ export const TOOLSET_DEFINITIONS = [
1817
1829
  isDefault: false,
1818
1830
  tools: new Set([
1819
1831
  "merge_merge_request",
1832
+ "rebase_merge_request",
1820
1833
  "approve_merge_request",
1821
1834
  "unapprove_merge_request",
1822
1835
  "get_merge_request_approval_state",
@@ -2254,8 +2267,15 @@ const discoverTool = allTools.find(t => t.name === "discover_tools");
2254
2267
  if (discoverTool) {
2255
2268
  discoverTool.description = `Discover and activate additional tool categories for this session. Available categories: ${[...DISCOVERABLE_TOOLSET_IDS].join(", ")}. Already-active categories are listed in the response. Explicit-only toolsets are omitted; list them in GITLAB_TOOLSETS.`;
2256
2269
  }
2270
+ function acceptsProjectOrGroupId(inputSchema) {
2271
+ const properties = inputSchema?.properties;
2272
+ return (typeof properties === "object" &&
2273
+ properties !== null &&
2274
+ ("project_id" in properties || "group_id" in properties));
2275
+ }
2276
+ const allToolNames = new Set(allTools.map(tool => tool.name));
2257
2277
  for (const tool of allTools) {
2258
- tool.description = getToolDescription(tool.name, tool.description, readOnlyTools.has(tool.name), destructiveTools.has(tool.name));
2278
+ tool.description = getToolDescription(tool.name, tool.description, readOnlyTools.has(tool.name), destructiveTools.has(tool.name), acceptsProjectOrGroupId(tool.inputSchema), allToolNames);
2259
2279
  }
2260
2280
  export function parseEnabledToolsets(raw) {
2261
2281
  if (!raw || raw.trim() === "") {
@@ -2,6 +2,7 @@ const TOOL_GUIDANCE = {
2
2
  merge_merge_request: "Use this only after checking the merge request approval, conflict, and pipeline state; use `approve_merge_request` to approve rather than merge. The operation changes repository state and may squash commits, schedule auto-merge, or delete the source branch, so it requires merge permission and returns GitLab's merge result or a mergeability error. Pass `sha` from `get_merge_request` (`sha` or `diff_refs.head_sha`); GitLab 19.2+ groups may reject merges without it.",
3
3
  approve_merge_request: "Use this to record an approval on an existing merge request; it does not merge the request or change its source branch. The operation changes review state, may require re-authentication or approval permission, and returns the updated approval result or a permission/state error.",
4
4
  unapprove_merge_request: "Use this to remove the current user's approval from an existing merge request; use `merge_merge_request` only when you intend to merge. The operation changes review state and requires approval permission, and GitLab returns the updated result or an error when the request or approval is unavailable.",
5
+ rebase_merge_request: "Use this when the source branch is behind its target branch; use `get_merge_request_conflicts` to inspect conflicts and `merge_merge_request` to merge. The operation rewrites the source branch history with a force push and requires permission to push to that branch. With `wait` true (default), it polls until the rebase ends and returns the merge request: GitLab reports a failed rebase in `merge_error`, but that field can also keep a message from an earlier attempt, so compare `sha` with its value before the rebase. If the rebase still runs after `timeout_seconds`, it returns a timeout error while GitLab continues the rebase. With `wait` false, it returns GitLab's enqueue response (`rebase_in_progress` true) at once. Set `skip_ci` to skip the CI pipeline for the rebased branch.",
5
6
  get_merge_request_approval_state: "Use this to inspect approval rules and approvers before deciding whether a merge request can be merged; use `approve_merge_request` to change approval state. It is read-only and returns the approval-state response, while missing requests, unsupported GitLab versions, and permission failures are reported as errors.",
6
7
  get_merge_request_conflicts: "Use this to inspect merge conflicts before attempting `merge_merge_request`; it reports conflicts and does not resolve them. It is read-only, requires access to the project and merge request, and returns GitLab's conflict data or an error when the request cannot be evaluated.",
7
8
  list_merge_request_pipelines: "Use this to inspect pipelines associated with one merge request; use `list_pipelines` for project-wide pipeline filtering. It is read-only and paginated, requires project access, and returns pipeline records or GitLab errors for invalid identifiers, missing resources, or rate limits.",
@@ -34,8 +35,8 @@ const TOOL_GUIDANCE = {
34
35
  update_issue_description_patch: "Use this for a targeted search/replace or unified-diff change to an issue description; use `dry_run` before applying an uncertain patch and `create_note` when an audit summary is wanted. It changes the issue description when not dry-running, requires issue-edit permission, and returns the patch result or a mismatch/validation/permission error.",
35
36
  delete_issue: "Use this only after confirming the issue and intended permanent removal; use `update_issue` to close or edit an issue without deleting it. The operation permanently removes issue data, requires delete permission, and returns the deletion result or a missing-resource, permission, or policy error.",
36
37
  delete_issue_link: "Use this to remove an existing relationship between two issues; use `list_issue_links` or `get_issue_link` to verify the link first. The operation changes issue relationships, requires issue-edit permission, and returns the result or an error when the link is missing or access is denied.",
37
- download_job_artifacts: "Use this to retrieve a pipeline job's artifact archive; remote HTTP mode returns a download URL while local mode saves the archive to a local path. It is read-only but may create a local file in stdio mode, requires job/project access, and returns the download result or an artifact/permission error.",
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.",
38
+ download_job_artifacts: "Use this to retrieve a pipeline job's artifact archive; remote HTTP mode returns a download URL while local mode saves the archive to a local path. It is read-only on GitLab but creates a local file in stdio mode (rejected in readonly mode), requires job/project access, and returns the download result or an artifact/permission error.",
39
+ 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 saves the file as a new file under the download directory (GITLAB_DOWNLOAD_DIR, default .gitlab-mcp-downloads). It is read-only with respect to GitLab but creates a local file in stdio mode (readonly mode returns images inline and rejects everything else), never replaces an existing file, requires project access, and returns the file content or an attachment/permission error.",
39
40
  gitlab_api_request: "Send one GitLab REST call under /api/v4. Readonly mode allows GET only, and modify mode rejects DELETE. Enable it only by listing api in GITLAB_TOOLSETS; all, discover_tools, and GITLAB_TOOLS do not. Prompt injection can drive this tool, so operators who rely on group, project, or tool restrictions must not enable it. GITLAB_ALLOWED_PROJECT_IDS is enforced only for /projects/:id paths; other endpoints are not constrained.",
40
41
  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.",
41
42
  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.",
@@ -62,46 +63,80 @@ const TOOL_GUIDANCE = {
62
63
  orbit_get_status: "Use this to check whether Orbit indexing has completed before trusting `orbit_query` results; results reflect the last index cycle, not real-time state. It is read-only and free of credit charges, and returns the indexing status or a permission error.",
63
64
  orbit_list_tools: "Use this to see which MCP tool definitions Orbit itself exposes; use `orbit_query` to run graph queries directly. It is read-only and free of credit charges, and returns the tool definition list or a permission error.",
64
65
  };
65
- const PARAMETER_GUIDANCE = "When `project_id` or `group_id` is accepted, provide the numeric ID or complete URL-encoded path described by the schema; use required identifiers and pagination fields exactly as documented.";
66
+ const PARAMETER_GUIDANCE = "`project_id`/`group_id` take a numeric ID or URL-encoded path.";
66
67
  function ensureSentence(text) {
67
68
  const trimmed = text.trim();
68
69
  return /[.!?]$/.test(trimmed) ? trimmed : `${trimmed}.`;
69
70
  }
70
- function getLifecycleGuidance(name) {
71
+ // Candidate get_* names for a list_* resource: list_pipelines -> get_pipeline,
72
+ // list_branches -> get_branch, list_policies -> get_policy, list_diffs -> get_diffs.
73
+ function matchingGetToolNames(resource) {
74
+ const candidates = [`get_${resource}`];
75
+ if (resource.endsWith("ies"))
76
+ candidates.push(`get_${resource.slice(0, -3)}y`);
77
+ if (resource.endsWith("es"))
78
+ candidates.push(`get_${resource.slice(0, -2)}`);
79
+ if (resource.endsWith("s"))
80
+ candidates.push(`get_${resource.slice(0, -1)}`);
81
+ return candidates;
82
+ }
83
+ // Candidate list_*/search_* names for a get_* resource: get_pipeline -> list_pipelines,
84
+ // get_branch -> list_branches, get_policy -> list_policies, get_diffs -> list_diffs.
85
+ function matchingDiscoveryToolNames(resource) {
86
+ const plurals = [resource, `${resource}s`, `${resource}es`];
87
+ if (resource.endsWith("y"))
88
+ plurals.push(`${resource.slice(0, -1)}ies`);
89
+ return plurals.flatMap(plural => [`list_${plural}`, `search_${plural}`]);
90
+ }
91
+ // Sibling hints are only added when the sibling tool exists, so a description
92
+ // never points at a tool the server does not have.
93
+ function getLifecycleGuidance(name, toolNames) {
71
94
  const verb = name.split("_")[0] ?? "use";
95
+ const resource = name.slice(verb.length + 1);
72
96
  if (verb === "list") {
73
- return "Use this for a collection of resources; choose the corresponding get tool when you already know the single resource to inspect.";
97
+ return matchingGetToolNames(resource).some(candidate => toolNames.has(candidate))
98
+ ? "For one known resource, use the matching get tool."
99
+ : "";
74
100
  }
75
101
  if (verb === "get") {
76
- return "Use this for a known resource or result; choose the corresponding list or search tool when you need to discover multiple resources.";
102
+ return matchingDiscoveryToolNames(resource).some(candidate => toolNames.has(candidate))
103
+ ? "To find resources, use the matching list or search tool."
104
+ : "";
77
105
  }
78
106
  if (verb === "create") {
79
- return "Use this for a new resource or action; choose the corresponding update or edit tool when the resource already exists.";
107
+ return toolNames.has(`update_${resource}`) || toolNames.has(`edit_${resource}`)
108
+ ? "If the resource already exists, use the update tool."
109
+ : "";
80
110
  }
81
111
  if (verb === "update" || verb === "edit") {
82
- return "Use this for an existing resource; choose the corresponding create tool for a new resource and a note tool for discussion-only text.";
112
+ if (!toolNames.has(`create_${resource}`)) {
113
+ return "";
114
+ }
115
+ return toolNames.has(`create_${resource}_note`)
116
+ ? "Use the create tool for new resources and a note tool for comments."
117
+ : "Use the create tool for new resources.";
83
118
  }
84
119
  if (verb === "delete" || verb === "remove") {
85
- return "Use this only after verifying the target; choose a get or list tool first when you need to inspect state without changing it.";
120
+ return "Verify the target first with a get or list tool.";
86
121
  }
87
122
  if (verb === "search") {
88
- return "Use this to discover matching content; choose a typed get or list tool when the target identifier is already known.";
123
+ return "If the identifier is known, use a typed get or list tool.";
89
124
  }
90
125
  if (verb === "validate") {
91
- return "Use this to check configuration without applying it; choose a create or update tool only after validation succeeds.";
126
+ return "Checks without applying; create or update only after it passes.";
92
127
  }
93
- return "Use this for the specific operation described; choose a sibling tool when you need a different resource or lifecycle action.";
128
+ return "";
94
129
  }
95
130
  function getBehaviorGuidance(readOnly, destructive) {
96
131
  if (readOnly) {
97
- return "It is read-only and does not mutate GitLab data; missing resources, invalid identifiers, insufficient permission, and rate limits are returned as errors.";
132
+ return "Read-only; missing resources, denied access, and rate limits return errors.";
98
133
  }
99
134
  if (destructive) {
100
- return "It changes or removes remote GitLab data and may be irreversible; it requires the necessary project or group permission and returns validation, conflict, permission, or rate-limit errors.";
135
+ return "Changes or removes GitLab data and may be irreversible; needs permission.";
101
136
  }
102
- return "It changes remote GitLab state and requires the necessary project or group permission; GitLab returns validation, conflict, permission, or rate-limit errors instead of silently applying an invalid request.";
137
+ return "Changes GitLab data; needs permission and returns validation or conflict errors.";
103
138
  }
104
- export function getToolDescription(name, baseDescription, readOnly, destructive) {
139
+ export function getToolDescription(name, baseDescription, readOnly, destructive, acceptsProjectOrGroupId, toolNames) {
105
140
  const explicitGuidance = TOOL_GUIDANCE[name];
106
141
  if (explicitGuidance) {
107
142
  return `${ensureSentence(baseDescription)} ${explicitGuidance}`;
@@ -110,5 +145,12 @@ export function getToolDescription(name, baseDescription, readOnly, destructive)
110
145
  if (normalizedDescription.length >= 180) {
111
146
  return normalizedDescription;
112
147
  }
113
- return `${normalizedDescription} ${getLifecycleGuidance(name)} ${getBehaviorGuidance(readOnly, destructive)} ${PARAMETER_GUIDANCE}`;
148
+ return [
149
+ normalizedDescription,
150
+ getLifecycleGuidance(name, toolNames),
151
+ getBehaviorGuidance(readOnly, destructive),
152
+ acceptsProjectOrGroupId ? PARAMETER_GUIDANCE : "",
153
+ ]
154
+ .filter(Boolean)
155
+ .join(" ");
114
156
  }
@@ -112,8 +112,7 @@ function skipInsignificantGraphQL(source, index) {
112
112
  function extractTopLevelMutationFields(normalized) {
113
113
  const fields = [];
114
114
  const mutationRegex = /(?:^|[;},]\s*)mutation\b[^({]*(?:\([^)]*\))?\s*\{/g;
115
- let match;
116
- while ((match = mutationRegex.exec(normalized)) !== null) {
115
+ while (mutationRegex.exec(normalized) !== null) {
117
116
  let i = mutationRegex.lastIndex;
118
117
  let braceDepth = 1;
119
118
  let parenDepth = 0;
@@ -82,8 +82,11 @@ export function resolveSafeExistingPath(inputPath, label = "path", baseDir) {
82
82
  * it if needed. Rejects symlink components and verifies the final real path
83
83
  * remains inside the base.
84
84
  */
85
- export function resolveSafeOutputDir(inputDir, label = "local_path", baseDir) {
85
+ export function resolveSafeOutputDir(inputDir, label = "local_path", baseDir, options = {}) {
86
86
  const relative = assertSafeRelativePath(inputDir, label);
87
+ if (options.rejectDotNames && relative.split(path.sep).some(isDotName)) {
88
+ throw new Error(`Invalid ${label}: names starting with a dot are not allowed.`);
89
+ }
87
90
  const baseReal = resolveTrustedBaseDir(baseDir);
88
91
  assertNoSymlinkComponents(baseReal, relative, label);
89
92
  const absolute = path.resolve(baseReal, relative);
@@ -136,6 +139,11 @@ const WRITE_OPEN_FLAGS = fs.constants.O_WRONLY |
136
139
  fs.constants.O_CREAT |
137
140
  fs.constants.O_TRUNC |
138
141
  (fs.constants.O_NOFOLLOW ?? 0);
142
+ // O_EXCL also refuses a symlink in the final component.
143
+ const EXCLUSIVE_WRITE_OPEN_FLAGS = fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL;
144
+ function isDotName(part) {
145
+ return part.startsWith(".") && part !== ".";
146
+ }
139
147
  /**
140
148
  * Validate and read a file in one step so callers never reuse a path string
141
149
  * after a symlink swap (final-component TOCTOU mitigation via O_NOFOLLOW).
@@ -171,7 +179,7 @@ export function readSafeExistingFile(inputPath, label = "path", baseDir) {
171
179
  * Validate an output destination and open a write stream immediately so the
172
180
  * path is not handed back for a separate createWriteStream call.
173
181
  */
174
- export function openSafeOutputWriteStream(filename, inputDir, label = "local_path", baseDir) {
182
+ export function openSafeOutputWriteStream(filename, inputDir, label = "local_path", baseDir, options = {}) {
175
183
  if (!filename ||
176
184
  filename === "." ||
177
185
  filename === ".." ||
@@ -180,10 +188,27 @@ export function openSafeOutputWriteStream(filename, inputDir, label = "local_pat
180
188
  filename.includes(path.sep)) {
181
189
  throw new Error(`Invalid ${label}: directory separators are not allowed in filename.`);
182
190
  }
191
+ if (options.rejectDotNames && isDotName(filename)) {
192
+ throw new Error("Invalid filename: names starting with a dot are not allowed.");
193
+ }
183
194
  const dirReal = inputDir
184
- ? resolveSafeOutputDir(inputDir, label, baseDir)
195
+ ? resolveSafeOutputDir(inputDir, label, baseDir, options)
185
196
  : resolveTrustedBaseDir(baseDir);
186
197
  const dest = path.join(dirReal, filename);
198
+ if (options.exclusive) {
199
+ let fd;
200
+ try {
201
+ fd = fs.openSync(dest, EXCLUSIVE_WRITE_OPEN_FLAGS, 0o666);
202
+ }
203
+ catch (err) {
204
+ if (err.code === "EEXIST") {
205
+ throw new Error(`${dest} already exists. Choose another ${label} or remove the file first.`);
206
+ }
207
+ throw err;
208
+ }
209
+ const stream = fs.createWriteStream(null, { fd, autoClose: true });
210
+ return { stream, path: dest };
211
+ }
187
212
  try {
188
213
  const st = fs.lstatSync(dest);
189
214
  if (st.isSymbolicLink()) {
@@ -203,6 +228,40 @@ export function openSafeOutputWriteStream(filename, inputDir, label = "local_pat
203
228
  });
204
229
  return { stream, path: dest };
205
230
  }
231
+ export const DEFAULT_DOWNLOAD_DIR = ".gitlab-mcp-downloads";
232
+ /**
233
+ * Resolve (and create on first use) the directory that downloads are saved under.
234
+ * The default sits in the working directory, which may be a cloned repository, so it
235
+ * must be a real directory rather than a symlink planted there. A directory the
236
+ * server creates gets a `.gitignore` so its contents are never committed by accident.
237
+ */
238
+ export function resolveDownloadRoot(configured) {
239
+ const root = path.resolve(configured || DEFAULT_DOWNLOAD_DIR);
240
+ let st;
241
+ try {
242
+ st = fs.lstatSync(root);
243
+ }
244
+ catch (err) {
245
+ if (err.code !== "ENOENT") {
246
+ throw err;
247
+ }
248
+ }
249
+ if (st?.isSymbolicLink() && !configured) {
250
+ throw new Error(`Invalid download directory: ${root} is a symbolic link. Remove it or set GITLAB_DOWNLOAD_DIR.`);
251
+ }
252
+ if (!st) {
253
+ fs.mkdirSync(root, { recursive: true });
254
+ try {
255
+ fs.writeFileSync(path.join(root, ".gitignore"), "*\n", { flag: "wx" });
256
+ }
257
+ catch (err) {
258
+ if (err.code !== "EEXIST") {
259
+ throw err;
260
+ }
261
+ }
262
+ }
263
+ return fs.realpathSync(root);
264
+ }
206
265
  /**
207
266
  * Estimate the number of merge commits that will be added based on the merge method.
208
267
  */
@@ -245,7 +304,7 @@ export function filterDiffsByPatterns(diffs, excludedFilePatterns) {
245
304
  try {
246
305
  return new RegExp(pattern);
247
306
  }
248
- catch (e) {
307
+ catch {
249
308
  console.warn(`Invalid regex pattern ignored: ${pattern}`);
250
309
  return null;
251
310
  }
@@ -193,7 +193,7 @@ function withoutCredentialHeaders(options) {
193
193
  * Runs on every redirect hop, so an upstream cannot steer the request to loopback,
194
194
  * link-local instance metadata, or a private-range host.
195
195
  */
196
- async function assertRedirectTargetAllowed(target, trusted, options) {
196
+ async function assertRedirectTargetAllowed(target, trusted, _options) {
197
197
  if (trusted) {
198
198
  return;
199
199
  }
@@ -40,6 +40,7 @@ const DOT_SEGMENT_PATTERN = /(^|[\\/])\.\.?([\\/]|$)/;
40
40
  /** A decoded value that starts with a path separator (absolute-path payload). */
41
41
  const LEADING_SEPARATOR_PATTERN = /^[\\/]/;
42
42
  /** NUL, newline and the other control characters: never valid in a path segment. */
43
+ // eslint-disable-next-line no-control-regex
43
44
  const CONTROL_CHARACTER_PATTERN = /[\u0000-\u001f\u007f]/;
44
45
  /**
45
46
  * How many times a value is percent-decoded before the checks run. A payload can be
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zereight/mcp-gitlab",
3
- "version": "2.2.6",
3
+ "version": "2.3.1",
4
4
  "mcpName": "io.github.zereight/gitlab-mcp",
5
5
  "description": "GitLab MCP server for projects, merge requests, issues, pipelines, wiki, releases, and more",
6
6
  "keywords": [
@@ -62,11 +62,11 @@
62
62
  "test:oauth": "tsx test/oauth-tests.ts && node --import tsx/esm --test --experimental-test-isolation=none test/oauth-device-flow-tests.ts",
63
63
  "test:list-merge-requests": "npm run build && tsx test/test-list-merge-requests.ts",
64
64
  "test:approvals": "npm run build && tsx test/test-merge-request-approvals.ts",
65
- "lint": "eslint . --ext .ts",
65
+ "lint": "eslint .",
66
66
  "check:runtime-deps": "node scripts/check-runtime-deps.mjs",
67
67
  "check:skill-sync": "tsx scripts/check-skill-sync.ts",
68
68
  "docs:tools": "tsx scripts/generate-tool-docs.ts && tsx scripts/generate-tool-coverage.ts",
69
- "lint:fix": "eslint . --ext .ts --fix",
69
+ "lint:fix": "eslint . --fix",
70
70
  "release": "bash scripts/release.sh",
71
71
  "release:mcp-registry": "bash scripts/publish_mcp_registry.sh",
72
72
  "format": "prettier --write \"**/*.{js,ts,json,md}\"",
@@ -90,6 +90,7 @@
90
90
  "zod": "^4.6.5"
91
91
  },
92
92
  "devDependencies": {
93
+ "@eslint/js": "^9.27.0",
93
94
  "@types/express": "^5.0.2",
94
95
  "@types/jmespath": "^0.15.2",
95
96
  "@types/node": "^22.13.10",