@zereight/mcp-gitlab 2.1.39 → 2.1.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.ko.md CHANGED
@@ -80,7 +80,7 @@ npm install -g @zereight/mcp-gitlab
80
80
 
81
81
  예시는 기존 `mcp-gitlab`보다 충돌 가능성이 낮은 `zereight-mcp-gitlab` 별칭을 사용합니다. MCP 클라이언트가 찾지 못하면 `which zereight-mcp-gitlab`의 절대 경로를 사용하세요.
82
82
 
83
- 전역 설치를 쓰지 않으려면 `npx -y @zereight/mcp-gitlab@2.1.38`처럼 직전 안정 버전(문서가 권장하는 버전)으로 고정하세요. 항상 최신 버전을 원하면 `npx -y @zereight/mcp-gitlab@latest`를 사용하세요. 새 버전이 나오면 서버가 시작 시 stderr로 알려줍니다(`GITLAB_DISABLE_VERSION_CHECK=true`로 비활성화 가능).
83
+ 전역 설치를 쓰지 않으려면 `npx -y @zereight/mcp-gitlab@2.1.40`처럼 직전 안정 버전(문서가 권장하는 버전)으로 고정하세요. 항상 최신 버전을 원하면 `npx -y @zereight/mcp-gitlab@latest`를 사용하세요. 새 버전이 나오면 서버가 시작 시 stderr로 알려줍니다(`GITLAB_DISABLE_VERSION_CHECK=true`로 비활성화 가능).
84
84
 
85
85
  #### CLI 인자 사용하기(환경 변수 문제가 있는 클라이언트용)
86
86
 
package/README.md CHANGED
@@ -80,7 +80,7 @@ npm install -g @zereight/mcp-gitlab
80
80
 
81
81
  The examples use `zereight-mcp-gitlab`, a less collision-prone alias for the legacy `mcp-gitlab` binary. If your MCP client cannot find it, use the absolute path from `which zereight-mcp-gitlab`.
82
82
 
83
- No global install? Pin `npx` to the previous stable release (the version these docs recommend), for example `npx -y @zereight/mcp-gitlab@2.1.38`. If you always want the newest release, use `npx -y @zereight/mcp-gitlab@latest` instead. The server prints a notice to stderr on startup when a newer version is available (disable with `GITLAB_DISABLE_VERSION_CHECK=true`).
83
+ No global install? Pin `npx` to the previous stable release (the version these docs recommend), for example `npx -y @zereight/mcp-gitlab@2.1.40`. If you always want the newest release, use `npx -y @zereight/mcp-gitlab@latest` instead. The server prints a notice to stderr on startup when a newer version is available (disable with `GITLAB_DISABLE_VERSION_CHECK=true`).
84
84
 
85
85
  #### Using CLI Arguments (for clients with env var issues)
86
86
 
@@ -541,183 +541,217 @@ Register the skill directory in your AI client to get optimal tool usage guidanc
541
541
  <!-- TOOLS-START -->
542
542
 
543
543
  1. `merge_merge_request` - Merge a merge request in a GitLab project
544
- 2. `create_or_update_file` - Create or update a single file in a GitLab project
545
- 3. `search_repositories` - Search for GitLab projects
546
- 4. `create_repository` - Create a new GitLab project
547
- 5. `create_group` - Create a new GitLab group or subgroup (name, path, description, visibility, and optional parent_id)
548
- 6. `get_file_contents` - Get the contents of a file or directory from a GitLab project
549
- 7. `push_files` - Push multiple files to a GitLab project in a single commit
550
- 8. `create_issue` - Create a new issue in a GitLab project
551
- 9. `create_merge_request` - Create a new merge request in a GitLab project
552
- 10. `fork_repository` - Fork a GitLab project to your account or specified namespace
553
- 11. `create_branch` - Create a new branch in a GitLab project
554
- 12. `get_merge_request` - Get details of a merge request with compact deployment summary, behind-count, commit addition summary, and approval summary (Either mergeRequestIid or branchName must be provided)
555
- 13. `get_merge_request_diffs` - Get the changes/diffs of a merge request (Either mergeRequestIid or branchName must be provided)
556
- 14. `list_merge_request_diffs` - List merge request diffs with pagination support (Either mergeRequestIid or branchName must be provided)
557
- 15. `get_merge_request_conflicts` - Get the conflicts of a merge request in a GitLab project
558
- 16. `list_merge_request_changed_files` - STEP 1 of code review workflow. Returns ONLY the list of changed file paths in a merge request — WITHOUT diff content. Call this first to get file paths, then call get_merge_request_file_diff with multiple files in a single batched call (recommended 3-5 files per call). Supports excluded_file_patterns filtering using regex. (Either mergeRequestIid or branchName must be provided)
559
- 17. `get_merge_request_file_diff` - STEP 2 of code review workflow. Get diffs for one or more files from a merge request. Call list_merge_request_changed_files first, then pass them as an array to fetch diffs efficiently. Batching multiple files (recommended 3-5) is supported. (Either mergeRequestIid or branchName must be provided)
560
- 18. `list_merge_request_versions` - List all versions of a merge request
561
- 19. `get_merge_request_version` - Get a specific version of a merge request
562
- 20. `get_branch_diffs` - Get the changes/diffs between two branches or commits in a GitLab project
563
- 21. `update_merge_request` - Update a merge request (Either mergeRequestIid or branchName must be provided)
564
- 22. `create_note` - Create a new note (comment) to an issue or merge request
565
- 23. `create_merge_request_thread` - Create a new thread on a merge request
566
- 24. `mr_discussions` - List discussion items for a merge request
567
- 25. `resolve_merge_request_thread` - Resolve a thread on a merge request
568
- 26. `update_merge_request_note` - Modify an existing merge request thread note
569
- 27. `create_merge_request_note` - Add a new note to an existing merge request thread
570
- 28. `delete_merge_request_discussion_note` - Delete a discussion note on a merge request
571
- 29. `update_merge_request_discussion_note` - Update a discussion note on a merge request
572
- 30. `create_merge_request_discussion_note` - Add a new discussion note to an existing merge request thread
573
- 31. `delete_merge_request_note` - Delete an existing merge request note
574
- 32. `get_merge_request_note` - Get a specific note for a merge request
575
- 33. `get_merge_request_notes` - List notes for a merge request
576
- 34. `get_draft_note` - Get a single draft note from a merge request
577
- 35. `list_draft_notes` - List draft notes for a merge request
578
- 36. `create_draft_note` - Create a draft note for a merge request
579
- 37. `update_draft_note` - Update an existing draft note
580
- 38. `delete_draft_note` - Delete a draft note
581
- 39. `publish_draft_note` - Publish a single draft note
582
- 40. `bulk_publish_draft_notes` - Publish all draft notes for a merge request
583
- 41. `list_merge_requests` - List merge requests globally or in a specific GitLab project with filtering options (project_id is now optional)
584
- 42. `approve_merge_request` - Approve a merge request (requires appropriate permissions)
585
- 43. `unapprove_merge_request` - Unapprove a previously approved merge request
586
- 44. `get_merge_request_approval_state` - Get merge request approval details including approvers (uses `approval_state` when available, otherwise falls back to `approvals`)
587
- 45. `update_issue_note` - Modify an existing issue thread note
588
- 46. `create_issue_note` - Add a new note to an existing issue thread
589
- 47. `list_issues` - List issues (default: created by current user only; use scope='all' for all accessible issues)
590
- 48. `my_issues` - List issues assigned to the authenticated user (defaults to open issues)
591
- 49. `get_issue` - Get details of a specific issue in a GitLab project
592
- 50. `update_issue` - Update an issue in a GitLab project
593
- 51. `update_issue_description_patch` - Apply a patch (search/replace or unified diff) to an issue description. Reduces token usage by sending only the change instead of the full description. Supports `dry_run` to preview and `create_note` to summarize.
594
- 52. `delete_issue` - Delete an issue from a GitLab project
595
- 53. `list_todos` - List GitLab to-do items for the current user
596
- 54. `mark_todo_done` - Mark a GitLab to-do item as done
597
- 55. `mark_all_todos_done` - Mark all pending GitLab to-do items as done for the current user
598
- 56. `list_issue_links` - List all issue links for a specific issue
599
- 57. `list_issue_discussions` - List discussions for an issue in a GitLab project
600
- 58. `get_issue_link` - Get a specific issue link
601
- 59. `create_issue_link` - Create an issue link between two issues
602
- 60. `delete_issue_link` - Delete an issue link
603
- 61. `list_namespaces` - List all namespaces available to the current user
604
- 62. `get_namespace` - Get details of a namespace by ID or path
605
- 63. `verify_namespace` - Verify if a namespace path exists
606
- 64. `get_project` - Get details of a specific project
607
- 65. `list_projects` - List projects accessible by the current user
608
- 66. `list_project_members` - List members of a GitLab project
609
- 67. `list_group_projects` - List projects in a GitLab group with filtering options
610
- 68. `list_group_iterations` - List group iterations with filtering options
611
- 69. `list_labels` - List labels for a project
612
- 70. `get_label` - Get a single label from a project
613
- 71. `create_label` - Create a new label in a project
614
- 72. `update_label` - Update an existing label in a project
615
- 73. `delete_label` - Delete a label from a project
616
- 74. `list_pipelines` - List pipelines in a GitLab project with filtering options
617
- 75. `get_pipeline` - Get details of a specific pipeline in a GitLab project
618
- 76. `list_pipeline_jobs` - List all jobs in a specific pipeline
619
- 77. `list_pipeline_trigger_jobs` - List all trigger jobs (bridges) in a specific pipeline that trigger downstream pipelines
620
- 78. `get_pipeline_job` - Get details of a GitLab pipeline job number
621
- 79. `get_pipeline_job_output` - Get the output/trace of a GitLab pipeline job with optional pagination to limit context window usage
622
- 80. `validate_ci_lint` - Validate provided GitLab CI/CD YAML content for a project
623
- 81. `validate_project_ci_lint` - Validate an existing `.gitlab-ci.yml` configuration for a project
624
- 82. `create_pipeline` - Create a new pipeline for a branch or tag
625
- 83. `retry_pipeline` - Retry a failed or canceled pipeline
626
- 84. `cancel_pipeline` - Cancel a running pipeline
627
- 85. `play_pipeline_job` - Run a manual pipeline job
628
- 86. `retry_pipeline_job` - Retry a failed or canceled pipeline job
629
- 87. `cancel_pipeline_job` - Cancel a running pipeline job
630
- 88. `list_deployments` - List deployments in a GitLab project with filtering options
631
- 89. `get_deployment` - Get details of a specific deployment in a GitLab project
632
- 90. `list_environments` - List environments in a GitLab project
633
- 91. `get_environment` - Get details of a specific environment in a GitLab project
634
- 92. `list_job_artifacts` - List artifact files in a job's artifacts archive. Returns file names, paths, types, and sizes
635
- 93. `download_job_artifacts` - Download the entire artifact archive (zip) for a job to a local path. Returns the saved file path
636
- 94. `get_job_artifact_file` - Get the content of a single file from a job's artifacts by its path within the archive
637
- 95. `list_milestones` - List milestones in a GitLab project with filtering options
638
- 96. `get_milestone` - Get details of a specific milestone
639
- 97. `create_milestone` - Create a new milestone in a GitLab project
640
- 98. `edit_milestone` - Edit an existing milestone in a GitLab project
641
- 99. `delete_milestone` - Delete a milestone from a GitLab project
642
- 100. `get_milestone_issue` - Get issues associated with a specific milestone
643
- 101. `get_milestone_merge_requests` - Get merge requests associated with a specific milestone
644
- 102. `promote_milestone` - Promote a milestone to the next stage
645
- 103. `get_milestone_burndown_events` - Get burndown events for a specific milestone
646
- 104. `list_group_milestones` - List milestones in a GitLab group with filtering options
647
- 105. `get_group_milestone` - Get details of a specific group milestone
648
- 106. `create_group_milestone` - Create a new milestone in a GitLab group
649
- 107. `edit_group_milestone` - Edit an existing group milestone
650
- 108. `delete_group_milestone` - Delete a milestone from a GitLab group
651
- 109. `get_group_milestone_issue` - Get issues associated with a specific group milestone
652
- 110. `get_group_milestone_merge_requests` - Get merge requests associated with a specific group milestone
653
- 111. `get_group_milestone_burndown_events` - Get burndown events for a specific group milestone
654
- 112. `list_wiki_pages` - List wiki pages in a GitLab project
655
- 113. `get_wiki_page` - Get details of a specific wiki page
656
- 114. `create_wiki_page` - Create a new wiki page in a GitLab project
657
- 115. `update_wiki_page` - Update an existing wiki page in a GitLab project
658
- 116. `delete_wiki_page` - Delete a wiki page from a GitLab project
659
- 117. `list_group_wiki_pages` - List wiki pages in a GitLab group
660
- 118. `get_group_wiki_page` - Get details of a specific group wiki page
661
- 119. `create_group_wiki_page` - Create a new wiki page in a GitLab group
662
- 120. `update_group_wiki_page` - Update an existing wiki page in a GitLab group
663
- 121. `delete_group_wiki_page` - Delete a wiki page from a GitLab group
664
- 122. `get_repository_tree` - Get the repository tree for a GitLab project (list files and directories)
665
- 123. `list_commits` - List repository commits with filtering options
666
- 124. `get_commit` - Get details of a specific commit
667
- 125. `get_commit_diff` - Get changes/diffs of a specific commit
668
- 126. `list_commit_statuses` - List statuses for a specific commit
669
- 127. `create_commit_status` - Create or update the status of a specific commit
670
- 128. `list_releases` - List all releases for a project
671
- 129. `get_release` - Get a release by tag name
672
- 130. `create_release` - Create a new release in a GitLab project
673
- 131. `update_release` - Update an existing release in a GitLab project
674
- 132. `delete_release` - Delete a release from a GitLab project (does not delete the associated tag)
675
- 133. `create_release_evidence` - Create release evidence for an existing release (GitLab Premium/Ultimate only)
676
- 134. `download_release_asset` - Download a release asset file by direct asset path
677
- 135. `list_tags` - List repository tags with filtering and pagination support
678
- 136. `get_tag` - Get details of a specific repository tag
679
- 137. `create_tag` - Create a new tag in the repository
680
- 138. `delete_tag` - Delete a tag from the repository
681
- 139. `get_tag_signature` - Get the signature of a signed tag
682
- 140. `get_users` - Get GitLab user details by usernames
683
- 141. `list_events` - List all events for the currently authenticated user
684
- 142. `get_project_events` - List all visible events for a specified project
685
- 143. `upload_markdown` - Upload a file to a GitLab project for use in markdown content
686
- 144. `download_attachment` - Download an uploaded file from a GitLab project by secret and filename
687
- 145. `get_work_item` - Get a single work item with full details including status, hierarchy (parent/children), type, labels, assignees, and all widgets
688
- 146. `list_work_items` - List work items in a project with filters (type, state, search, assignees, labels). Returns items with status and hierarchy info
689
- 147. `create_work_item` - Create a new work item (issue, task, incident, test_case, epic, key_result, objective, requirement, ticket). Supports setting title, description, labels, assignees, weight, parent, health status, start/due dates, milestone, and confidentiality
690
- 148. `update_work_item` - Update a work item. Can modify title, description, labels, assignees, weight, state, status, parent hierarchy, children, health status, start/due dates, milestone, confidentiality, linked items, and custom fields
691
- 149. `convert_work_item_type` - Convert a work item to a different type (e.g. issue to task, task to incident)
692
- 150. `list_work_item_statuses` - List available statuses for a work item type in a project. Requires GitLab Premium/Ultimate with configurable statuses
693
- 151. `list_custom_field_definitions` - List available custom field definitions for a work item type in a project. Returns field names, types, and IDs needed for setting custom fields via update_work_item
694
- 152. `move_work_item` - Move a work item (issue, task, etc.) to a different project. Uses GitLab GraphQL issueMove mutation
695
- 153. `list_work_item_notes` - List notes and discussions on a work item. Returns threaded discussions with author, body, timestamps, and system/internal flags
696
- 154. `create_work_item_note` - Add a note/comment to a work item. Supports Markdown, internal notes, and threaded replies
697
- 155. `get_timeline_events` - List timeline events for an incident. Returns chronological events with notes, timestamps, and tags
698
- 156. `create_timeline_event` - Create a timeline event on an incident. Supports tags: 'Start time', 'End time', 'Impact detected', 'Response initiated', 'Impact mitigated', 'Cause identified'
699
- 157. `list_webhooks` - List all configured webhooks for a GitLab project or group. Provide either project_id or group_id
700
- 158. `list_webhook_events` - List recent webhook events (past 7 days) for a project or group webhook. Use summary mode for overview, then get_webhook_event for full details
701
- 159. `get_webhook_event` - Get full details of a specific webhook event by ID, including request/response payloads
702
- 160. `search_code` - Search for code across all projects on the GitLab instance (requires advanced search or exact code search to be enabled)
703
- 161. `search_project_code` - Search for code within a specific GitLab project (requires advanced search or exact code search to be enabled)
704
- 162. `search_group_code` - Search for code within a specific GitLab group (requires advanced search or exact code search to be enabled)
705
- 163. `execute_graphql` - Execute a GitLab GraphQL query
706
- 164. `list_merge_request_pipelines` - List pipelines for a merge request with pagination support
707
- 165. `list_project_variables` - List CI/CD variables for a project with optional environment scope filter
708
- 166. `get_project_variable` - Get a single CI/CD variable from a project by key, with optional environment scope filter
709
- 167. `create_project_variable` - Create a new CI/CD variable in a project
710
- 168. `update_project_variable` - Update an existing CI/CD variable in a project, with optional filter to disambiguate by environment scope
711
- 169. `delete_project_variable` - Delete a CI/CD variable from a project, with optional filter to disambiguate by environment scope
712
- 170. `list_group_variables` - List CI/CD variables for a group with optional environment scope filter
713
- 171. `get_group_variable` - Get a single CI/CD variable from a group by key, with optional environment scope filter
714
- 172. `create_group_variable` - Create a new CI/CD variable in a group
715
- 173. `update_group_variable` - Update an existing CI/CD variable in a group, with optional filter to disambiguate by environment scope
716
- 174. `delete_group_variable` - Delete a CI/CD variable from a group, with optional filter to disambiguate by environment scope
717
- 175. `get_dependency_proxy_settings` - Get dependency proxy settings for a group (enabled status, blob count, total size, image prefix, TTL policy)
718
- 176. `update_dependency_proxy_settings` - Update dependency proxy settings for a group (enable/disable, credentials for authenticated Docker Hub pulls)
719
- 177. `list_dependency_proxy_blobs` - List cached dependency proxy blobs for a group with cursor-based pagination
720
- 178. `purge_dependency_proxy_cache` - Schedule purge of all cached dependency proxy blobs for a group
544
+ 2. `approve_merge_request` - Approve a merge request (requires appropriate permissions)
545
+ 3. `unapprove_merge_request` - Unapprove a previously approved merge request
546
+ 4. `get_merge_request_approval_state` - Get merge request approval details including approvers (uses `approval_state` when available, otherwise falls back to `approvals`)
547
+ 5. `get_merge_request_conflicts` - Get the conflicts of a merge request in a GitLab project
548
+ 6. `list_merge_request_pipelines` - List pipelines for a merge request with pagination support
549
+ 7. `execute_graphql` - Execute a GitLab GraphQL query
550
+ 8. `create_or_update_file` - Create or update a single file in a GitLab project
551
+ 9. `search_repositories` - Search for GitLab projects
552
+ 10. `create_repository` - Create a new GitLab project
553
+ 11. `create_group` - Create a new GitLab group or subgroup (name, path, description, visibility, and optional parent_id)
554
+ 12. `get_file_contents` - Get the contents of a file or directory from a GitLab project
555
+ 13. `push_files` - Push multiple files to a GitLab project in a single commit
556
+ 14. `create_issue` - Create a new issue in a GitLab project
557
+ 15. `create_merge_request` - Create a new merge request in a GitLab project
558
+ 16. `fork_repository` - Fork a GitLab project to your account or specified namespace
559
+ 17. `create_branch` - Create a new branch in a GitLab project
560
+ 18. `get_branch` - Get branch details (commit, protection status)
561
+ 19. `list_branches` - List branches in project with search filter
562
+ 20. `delete_branch` - Delete branch from project
563
+ 21. `list_protected_branches` - List protected branches in a project, supports search filter
564
+ 22. `get_protected_branch` - Get details of a single protected branch (access levels, force push settings)
565
+ 23. `protect_branch` - Protect a repository branch (set push/merge/unprotect access levels)
566
+ 24. `unprotect_branch` - Remove protection from a previously protected branch
567
+ 25. `update_default_branch` - Change the default branch of a project
568
+ 26. `get_merge_request` - Get details of a merge request with compact deployment summary, behind-count, commit addition summary, and approval summary (Either mergeRequestIid or branchName must be provided)
569
+ 27. `get_merge_request_diffs` - Get the changes/diffs of a merge request (Either mergeRequestIid or branchName must be provided)
570
+ 28. `list_merge_request_changed_files` - STEP 1 of code review workflow. Returns ONLY the list of changed file paths in a merge request — WITHOUT diff content. Call this first to get file paths, then call get_merge_request_file_diff with multiple files in a single batched call (recommended 3-5 files per call). Supports excluded_file_patterns filtering using regex. (Either mergeRequestIid or branchName must be provided)
571
+ 29. `list_merge_request_diffs` - List merge request diffs with pagination support (Either mergeRequestIid or branchName must be provided)
572
+ 30. `get_merge_request_file_diff` - STEP 2 of code review workflow. Get diffs for one or more files from a merge request. Call list_merge_request_changed_files first, then pass them as an array to fetch diffs efficiently. Batching multiple files (recommended 3-5) is supported. (Either mergeRequestIid or branchName must be provided)
573
+ 31. `list_merge_request_versions` - List all versions of a merge request
574
+ 32. `get_merge_request_version` - Get a specific version of a merge request
575
+ 33. `get_branch_diffs` - Get the changes/diffs between two branches or commits in a GitLab project
576
+ 34. `update_merge_request` - Update a merge request (Either mergeRequestIid or branchName must be provided)
577
+ 35. `create_note` - Create a new note (comment) to an issue or merge request
578
+ 36. `create_merge_request_thread` - Create a new thread on a merge request
579
+ 37. `resolve_merge_request_thread` - Resolve a thread on a merge request
580
+ 38. `mr_discussions` - List discussion items for a merge request
581
+ 39. `delete_merge_request_discussion_note` - Delete a discussion note on a merge request
582
+ 40. `update_merge_request_discussion_note` - Update a discussion note on a merge request
583
+ 41. `create_merge_request_discussion_note` - Add a new discussion note to an existing merge request thread
584
+ 42. `create_merge_request_note` - Add a new note to an existing merge request thread
585
+ 43. `delete_merge_request_note` - Delete an existing merge request note
586
+ 44. `get_merge_request_note` - Get a specific note for a merge request
587
+ 45. `get_merge_request_notes` - List notes for a merge request
588
+ 46. `update_merge_request_note` - Modify an existing merge request thread note
589
+ 47. `get_draft_note` - Get a single draft note from a merge request
590
+ 48. `list_draft_notes` - List draft notes for a merge request
591
+ 49. `create_draft_note` - Create a draft note for a merge request
592
+ 50. `update_draft_note` - Update an existing draft note
593
+ 51. `delete_draft_note` - Delete a draft note
594
+ 52. `publish_draft_note` - Publish a single draft note
595
+ 53. `bulk_publish_draft_notes` - Publish all draft notes for a merge request
596
+ 54. `list_merge_request_emoji_reactions` - List all emoji reactions on a merge request
597
+ 55. `list_merge_request_note_emoji_reactions` - List all emoji reactions on a merge request note. Pass discussion_id for discussion thread replies.
598
+ 56. `create_merge_request_emoji_reaction` - Add an emoji reaction to a merge request (e.g. thumbsup, rocket, eyes)
599
+ 57. `delete_merge_request_emoji_reaction` - Remove an emoji reaction from a merge request
600
+ 58. `create_merge_request_note_emoji_reaction` - Add an emoji reaction to a merge request note. Pass discussion_id for discussion thread replies.
601
+ 59. `delete_merge_request_note_emoji_reaction` - Remove an emoji reaction from a merge request note. Pass discussion_id for discussion thread replies.
602
+ 60. `update_issue_note` - Modify an existing issue thread note
603
+ 61. `create_issue_note` - Add a new note to an existing issue thread
604
+ 62. `list_issue_emoji_reactions` - List all emoji reactions on an issue
605
+ 63. `list_issue_note_emoji_reactions` - List all emoji reactions on an issue note. Pass discussion_id for discussion thread replies.
606
+ 64. `create_issue_emoji_reaction` - Add an emoji reaction to an issue (e.g. thumbsup, rocket, eyes)
607
+ 65. `delete_issue_emoji_reaction` - Remove an emoji reaction from an issue
608
+ 66. `create_issue_note_emoji_reaction` - Add an emoji reaction to an issue note. Pass discussion_id for discussion thread replies.
609
+ 67. `delete_issue_note_emoji_reaction` - Remove an emoji reaction from an issue note. Pass discussion_id for discussion thread replies.
610
+ 68. `list_issues` - List issues (default: created by current user only; use scope='all' for all accessible issues)
611
+ 69. `my_issues` - List issues assigned to the authenticated user (defaults to open issues)
612
+ 70. `get_issue` - Get details of a specific issue in a GitLab project
613
+ 71. `update_issue` - Update an issue in a GitLab project
614
+ 72. `update_issue_description_patch` - Apply a patch (search/replace or unified diff) to an issue description. Reduces token usage by sending only the change instead of the full description. Supports `dry_run` to preview and `create_note` to summarize.
615
+ 73. `delete_issue` - Delete an issue from a GitLab project
616
+ 74. `list_todos` - List GitLab to-do items for the current user
617
+ 75. `mark_todo_done` - Mark a GitLab to-do item as done
618
+ 76. `mark_all_todos_done` - Mark all pending GitLab to-do items as done for the current user
619
+ 77. `list_issue_links` - List all issue links for a specific issue
620
+ 78. `list_issue_discussions` - List discussions for an issue in a GitLab project
621
+ 79. `get_issue_link` - Get a specific issue link
622
+ 80. `create_issue_link` - Create an issue link between two issues
623
+ 81. `delete_issue_link` - Delete an issue link
624
+ 82. `list_namespaces` - List all namespaces available to the current user
625
+ 83. `get_namespace` - Get details of a namespace by ID or path
626
+ 84. `verify_namespace` - Verify if a namespace path exists
627
+ 85. `get_project` - Get details of a specific project
628
+ 86. `list_projects` - List projects accessible by the current user
629
+ 87. `update_project` - Update project settings such as description, visibility, default branch, and feature access levels
630
+ 88. `list_project_members` - List members of a GitLab project
631
+ 89. `list_labels` - List labels for a project
632
+ 90. `get_label` - Get a single label from a project
633
+ 91. `create_label` - Create a new label in a project
634
+ 92. `update_label` - Update an existing label in a project
635
+ 93. `delete_label` - Delete a label from a project
636
+ 94. `list_group_projects` - List projects in a GitLab group with filtering options
637
+ 95. `list_wiki_pages` - List wiki pages in a GitLab project
638
+ 96. `get_wiki_page` - Get details of a specific wiki page
639
+ 97. `create_wiki_page` - Create a new wiki page in a GitLab project
640
+ 98. `update_wiki_page` - Update an existing wiki page in a GitLab project
641
+ 99. `delete_wiki_page` - Delete a wiki page from a GitLab project
642
+ 100. `list_group_wiki_pages` - List wiki pages in a GitLab group
643
+ 101. `get_group_wiki_page` - Get details of a specific group wiki page
644
+ 102. `create_group_wiki_page` - Create a new wiki page in a GitLab group
645
+ 103. `update_group_wiki_page` - Update an existing wiki page in a GitLab group
646
+ 104. `delete_group_wiki_page` - Delete a wiki page from a GitLab group
647
+ 105. `get_repository_tree` - Get the repository tree for a GitLab project (list files and directories)
648
+ 106. `list_pipelines` - List pipelines in a GitLab project with filtering options
649
+ 107. `get_pipeline` - Get details of a specific pipeline in a GitLab project
650
+ 108. `list_deployments` - List deployments in a GitLab project with filtering options
651
+ 109. `get_deployment` - Get details of a specific deployment in a GitLab project
652
+ 110. `list_environments` - List environments in a GitLab project
653
+ 111. `get_environment` - Get details of a specific environment in a GitLab project
654
+ 112. `list_pipeline_jobs` - List all jobs in a specific pipeline
655
+ 113. `list_pipeline_trigger_jobs` - List all trigger jobs (bridges) in a specific pipeline that trigger downstream pipelines
656
+ 114. `get_pipeline_job` - Get details of a GitLab pipeline job number
657
+ 115. `get_pipeline_job_output` - Get the output/trace of a GitLab pipeline job with optional pagination to limit context window usage
658
+ 116. `validate_ci_lint` - Validate provided GitLab CI/CD YAML content for a project
659
+ 117. `validate_project_ci_lint` - Validate an existing `.gitlab-ci.yml` configuration for a project
660
+ 118. `list_ci_catalog_resources` - List GitLab CI/CD Catalog resources/components visible to the user
661
+ 119. `get_ci_catalog_resource` - Get details for a GitLab CI/CD Catalog resource, including versions and components
662
+ 120. `create_pipeline` - Create a new pipeline for a branch or tag
663
+ 121. `retry_pipeline` - Retry a failed or canceled pipeline
664
+ 122. `cancel_pipeline` - Cancel a running pipeline
665
+ 123. `play_pipeline_job` - Run a manual pipeline job
666
+ 124. `retry_pipeline_job` - Retry a failed or canceled pipeline job
667
+ 125. `cancel_pipeline_job` - Cancel a running pipeline job
668
+ 126. `list_job_artifacts` - List artifact files in a job's artifacts archive. Returns file names, paths, types, and sizes
669
+ 127. `download_job_artifacts` - Download the entire artifact archive (zip) for a job to a local path. Returns the saved file path
670
+ 128. `get_job_artifact_file` - Get the content of a single file from a job's artifacts by its path within the archive
671
+ 129. `list_merge_requests` - List merge requests globally or in a specific GitLab project with filtering options (project_id is now optional)
672
+ 130. `list_milestones` - List milestones in a GitLab project with filtering options
673
+ 131. `get_milestone` - Get details of a specific milestone
674
+ 132. `create_milestone` - Create a new milestone in a GitLab project
675
+ 133. `edit_milestone` - Edit an existing milestone in a GitLab project
676
+ 134. `delete_milestone` - Delete a milestone from a GitLab project
677
+ 135. `get_milestone_issue` - Get issues associated with a specific milestone
678
+ 136. `get_milestone_merge_requests` - Get merge requests associated with a specific milestone
679
+ 137. `promote_milestone` - Promote a milestone to the next stage
680
+ 138. `get_milestone_burndown_events` - Get burndown events for a specific milestone
681
+ 139. `list_group_milestones` - List milestones in a GitLab group with filtering options
682
+ 140. `get_group_milestone` - Get details of a specific group milestone
683
+ 141. `create_group_milestone` - Create a new milestone in a GitLab group
684
+ 142. `edit_group_milestone` - Edit an existing group milestone
685
+ 143. `delete_group_milestone` - Delete a milestone from a GitLab group
686
+ 144. `get_group_milestone_issue` - Get issues associated with a specific group milestone
687
+ 145. `get_group_milestone_merge_requests` - Get merge requests associated with a specific group milestone
688
+ 146. `get_group_milestone_burndown_events` - Get burndown events for a specific group milestone
689
+ 147. `get_users` - Get GitLab user details by usernames
690
+ 148. `get_user` - Get user details by ID
691
+ 149. `whoami` - Get current authenticated user details
692
+ 150. `list_commits` - List repository commits with filtering options
693
+ 151. `get_commit` - Get details of a specific commit
694
+ 152. `get_commit_diff` - Get changes/diffs of a specific commit
695
+ 153. `get_file_blame` - Get git blame for a file at a given ref. Each entry maps a contiguous range of source lines to the commit that last changed them (id, author, authored_date, message). Use range_start/range_end to limit blame to specific lines.
696
+ 154. `list_commit_statuses` - List statuses for a specific commit
697
+ 155. `create_commit_status` - Create or update the status of a specific commit
698
+ 156. `list_group_iterations` - List group iterations with filtering options
699
+ 157. `upload_markdown` - Upload a file to a GitLab project for use in markdown content
700
+ 158. `download_attachment` - Download an uploaded file from a GitLab project by secret and filename
701
+ 159. `health_check` - Verify server status and authentication
702
+ 160. `list_events` - List all events for the currently authenticated user
703
+ 161. `get_project_events` - List all visible events for a specified project
704
+ 162. `list_releases` - List all releases for a project
705
+ 163. `get_release` - Get a release by tag name
706
+ 164. `create_release` - Create a new release in a GitLab project
707
+ 165. `update_release` - Update an existing release in a GitLab project
708
+ 166. `delete_release` - Delete a release from a GitLab project (does not delete the associated tag)
709
+ 167. `create_release_evidence` - Create release evidence for an existing release (GitLab Premium/Ultimate only)
710
+ 168. `download_release_asset` - Download a release asset file by direct asset path
711
+ 169. `list_tags` - List repository tags with filtering and pagination support
712
+ 170. `get_tag` - Get details of a specific repository tag
713
+ 171. `create_tag` - Create a new tag in the repository
714
+ 172. `delete_tag` - Delete a tag from the repository
715
+ 173. `get_tag_signature` - Get the signature of a signed tag
716
+ 174. `get_work_item` - Get a single work item with full details including status, hierarchy (parent/children), type, labels, assignees, and all widgets
717
+ 175. `list_work_items` - List work items in a project with filters (type, state, search, assignees, labels). Returns items with status and hierarchy info
718
+ 176. `create_work_item` - Create a new work item (issue, task, incident, test_case, epic, key_result, objective, requirement, ticket). Supports setting title, description, labels, assignees, weight, parent, health status, start/due dates, milestone, and confidentiality
719
+ 177. `update_work_item` - Update a work item. Can modify title, description, labels, assignees, weight, state, status, parent hierarchy, children, health status, start/due dates, milestone, confidentiality, linked items, and custom fields
720
+ 178. `convert_work_item_type` - Convert a work item to a different type (e.g. issue to task, task to incident)
721
+ 179. `list_work_item_statuses` - List available statuses for a work item type in a project. Requires GitLab Premium/Ultimate with configurable statuses
722
+ 180. `list_custom_field_definitions` - List available custom field definitions for a work item type in a project. Returns field names, types, and IDs needed for setting custom fields via update_work_item
723
+ 181. `move_work_item` - Move a work item (issue, task, etc.) to a different project. Uses GitLab GraphQL issueMove mutation
724
+ 182. `list_work_item_notes` - List notes and discussions on a work item. Returns threaded discussions with author, body, timestamps, and system/internal flags
725
+ 183. `create_work_item_note` - Add a note/comment to a work item. Supports Markdown, internal notes, and threaded replies
726
+ 184. `list_work_item_emoji_reactions` - List all emoji reactions on a work item
727
+ 185. `list_work_item_note_emoji_reactions` - List all emoji reactions on a work item note (comment, thread, or thread reply)
728
+ 186. `create_work_item_emoji_reaction` - Add an emoji reaction to a work item (e.g. thumbsup, rocket, eyes)
729
+ 187. `delete_work_item_emoji_reaction` - Remove an emoji reaction from a work item
730
+ 188. `create_work_item_note_emoji_reaction` - Add an emoji reaction to a work item note (comment, thread, or thread reply)
731
+ 189. `delete_work_item_note_emoji_reaction` - Remove an emoji reaction from a work item note (comment, thread, or thread reply)
732
+ 190. `get_timeline_events` - List timeline events for an incident. Returns chronological events with notes, timestamps, and tags
733
+ 191. `create_timeline_event` - Create a timeline event on an incident. Supports tags: 'Start time', 'End time', 'Impact detected', 'Response initiated', 'Impact mitigated', 'Cause identified'
734
+ 192. `list_webhooks` - List all configured webhooks for a GitLab project or group. Provide either project_id or group_id
735
+ 193. `list_webhook_events` - List recent webhook events (past 7 days) for a project or group webhook. Use summary mode for overview, then get_webhook_event for full details
736
+ 194. `get_webhook_event` - Get full details of a specific webhook event by ID, including request/response payloads
737
+ 195. `search_code` - Search for code across all projects on the GitLab instance (requires advanced search or exact code search to be enabled)
738
+ 196. `search_project_code` - Search for code within a specific GitLab project (requires advanced search or exact code search to be enabled)
739
+ 197. `search_group_code` - Search for code within a specific GitLab group (requires advanced search or exact code search to be enabled)
740
+ 198. `list_project_variables` - List CI/CD variables for a project with optional environment scope filter
741
+ 199. `get_project_variable` - Get a single CI/CD variable from a project by key, with optional environment scope filter
742
+ 200. `create_project_variable` - Create a new CI/CD variable in a project
743
+ 201. `update_project_variable` - Update an existing CI/CD variable in a project, with optional filter to disambiguate by environment scope
744
+ 202. `delete_project_variable` - Delete a CI/CD variable from a project, with optional filter to disambiguate by environment scope
745
+ 203. `list_group_variables` - List CI/CD variables for a group with optional environment scope filter
746
+ 204. `get_group_variable` - Get a single CI/CD variable from a group by key, with optional environment scope filter
747
+ 205. `create_group_variable` - Create a new CI/CD variable in a group
748
+ 206. `update_group_variable` - Update an existing CI/CD variable in a group, with optional filter to disambiguate by environment scope
749
+ 207. `delete_group_variable` - Delete a CI/CD variable from a group, with optional filter to disambiguate by environment scope
750
+ 208. `get_dependency_proxy_settings` - Get dependency proxy settings for a group (enabled status, blob count, total size, image prefix, TTL policy)
751
+ 209. `update_dependency_proxy_settings` - Update dependency proxy settings for a group (enable/disable, credentials for authenticated Docker Hub pulls)
752
+ 210. `list_dependency_proxy_blobs` - List cached dependency proxy blobs for a group with cursor-based pagination
753
+ 211. `purge_dependency_proxy_cache` - Schedule purge of all cached dependency proxy blobs for a group
754
+ 212. `discover_tools` - Discover and activate additional tool categories for this session. Available categories: merge_requests, issues, repositories, branches, projects, labels, ci, groups, pipelines, milestones, wiki, releases, tags, users, workitems, webhooks, search, variables, dependency_proxy. Already-active categories are listed in the response.
721
755
 
722
756
  <!-- TOOLS-END -->
723
757
 
package/README.zh-CN.md CHANGED
@@ -80,7 +80,7 @@ npm install -g @zereight/mcp-gitlab
80
80
 
81
81
  示例使用 `zereight-mcp-gitlab`,这是比旧的 `mcp-gitlab` 更不容易冲突的别名。如果 MCP 客户端找不到它,请使用 `which zereight-mcp-gitlab` 输出的绝对路径。
82
82
 
83
- 如果不想全局安装,请将 `npx` 固定到上一个稳定版本(即文档推荐的版本),例如 `npx -y @zereight/mcp-gitlab@2.1.38`。如果始终想使用最新版本,请改用 `npx -y @zereight/mcp-gitlab@latest`。有新版本发布时,服务器会在启动时通过 stderr 提示(可用 `GITLAB_DISABLE_VERSION_CHECK=true` 关闭)。
83
+ 如果不想全局安装,请将 `npx` 固定到上一个稳定版本(即文档推荐的版本),例如 `npx -y @zereight/mcp-gitlab@2.1.40`。如果始终想使用最新版本,请改用 `npx -y @zereight/mcp-gitlab@latest`。有新版本发布时,服务器会在启动时通过 stderr 提示(可用 `GITLAB_DISABLE_VERSION_CHECK=true` 关闭)。
84
84
 
85
85
  #### 使用 CLI 参数(适用于环境变量有问题的客户端)
86
86
 
package/build/index.js CHANGED
@@ -1122,12 +1122,17 @@ function parseAllowedGitLabApiUrls(value) {
1122
1122
  }
1123
1123
  function encodeGitLabPathSegment(value) {
1124
1124
  const segment = String(value);
1125
+ let decodedSegment;
1125
1126
  try {
1126
- return encodeURIComponent(decodeURIComponent(segment));
1127
+ decodedSegment = decodeURIComponent(segment);
1127
1128
  }
1128
1129
  catch {
1129
- return encodeURIComponent(segment);
1130
+ decodedSegment = segment;
1130
1131
  }
1132
+ if (decodedSegment === "." || decodedSegment === "..") {
1133
+ throw new Error("GitLab URL path segments cannot be '.' or '..'");
1134
+ }
1135
+ return encodeURIComponent(decodedSegment);
1131
1136
  }
1132
1137
  function encodeGitLabPath(value) {
1133
1138
  return value.split("/").map(encodeGitLabPathSegment).join("/");
@@ -3060,7 +3065,7 @@ async function createMergeRequest(projectId, options) {
3060
3065
  */
3061
3066
  async function listDiscussions(projectId, resourceType, resourceIid, options = {}) {
3062
3067
  projectId = decodeURIComponent(projectId); // Decode project ID
3063
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/${resourceType}/${resourceIid}/discussions`);
3068
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/${resourceType}/${encodeGitLabPathSegment(resourceIid)}/discussions`);
3064
3069
  // Add query parameters for pagination and sorting
3065
3070
  if (options.page) {
3066
3071
  url.searchParams.append("page", options.page.toString());
@@ -3298,15 +3303,15 @@ async function deleteMergeRequestNote(projectId, mergeRequestIid, noteId) {
3298
3303
  function buildAwardEmojiPath(entity, projectId, entityIid, opts) {
3299
3304
  projectId = decodeURIComponent(projectId);
3300
3305
  const pp = encodeURIComponent(getEffectiveProjectId(projectId));
3301
- let path = `${getEffectiveApiUrl()}/projects/${pp}/${entity}/${entityIid}`;
3306
+ let path = `${getEffectiveApiUrl()}/projects/${pp}/${entity}/${encodeGitLabPathSegment(entityIid)}`;
3302
3307
  if (opts?.noteId) {
3303
3308
  path = opts.discussionId
3304
- ? `${path}/discussions/${opts.discussionId}/notes/${opts.noteId}`
3305
- : `${path}/notes/${opts.noteId}`;
3309
+ ? `${path}/discussions/${encodeGitLabPathSegment(opts.discussionId)}/notes/${encodeGitLabPathSegment(opts.noteId)}`
3310
+ : `${path}/notes/${encodeGitLabPathSegment(opts.noteId)}`;
3306
3311
  }
3307
3312
  path += "/award_emoji";
3308
3313
  if (opts?.awardId)
3309
- path += `/${opts.awardId}`;
3314
+ path += `/${encodeGitLabPathSegment(opts.awardId)}`;
3310
3315
  return path;
3311
3316
  }
3312
3317
  async function createRestAwardEmoji(path, name) {
@@ -4234,7 +4239,7 @@ async function createNote(projectId, noteableType, // specifies 'issue' or 'merg
4234
4239
  noteableIid, body) {
4235
4240
  projectId = decodeURIComponent(projectId); // Decode project ID
4236
4241
  // ⚙️ Response type can be adjusted according to the GitLab API documentation
4237
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/${noteableType}s/${noteableIid}/notes` // Using plural form (issues/merge_requests) as per GitLab API documentation
4242
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/${noteableType}s/${encodeGitLabPathSegment(noteableIid)}/notes` // Using plural form (issues/merge_requests) as per GitLab API documentation
4238
4243
  );
4239
4244
  const response = await fetch(url.toString(), {
4240
4245
  ...getFetchConfig(),
@@ -4254,7 +4259,8 @@ noteableIid, body) {
4254
4259
  * @returns {Promise<GitLabDraftNote[]>} Array of draft notes
4255
4260
  */
4256
4261
  async function getDraftNote(project_id, merge_request_iid, draft_note_id) {
4257
- const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(project_id)}/merge_requests/${merge_request_iid}/draft_notes/${draft_note_id}`, { ...getFetchConfig() });
4262
+ const effectiveProjectId = getEffectiveProjectId(decodeURIComponent(project_id));
4263
+ const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/merge_requests/${encodeGitLabPathSegment(merge_request_iid)}/draft_notes/${encodeGitLabPathSegment(draft_note_id)}`, { ...getFetchConfig() });
4258
4264
  if (!response.ok) {
4259
4265
  const errorText = await response.text();
4260
4266
  throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorText}`);
@@ -5323,7 +5329,7 @@ async function downloadJobArtifacts(projectId, jobId, localPath) {
5323
5329
  throw new Error(`Job artifacts not found. The job may not have produced artifacts or the job ID is invalid.`);
5324
5330
  }
5325
5331
  await handleGitLabError(response);
5326
- const filename = `artifacts_job_${jobId}.zip`;
5332
+ const filename = `artifacts_job_${encodeGitLabPathSegment(jobId)}.zip`;
5327
5333
  const savePath = localPath ? path.join(localPath, filename) : filename;
5328
5334
  fs.mkdirSync(path.dirname(savePath), { recursive: true });
5329
5335
  if (!response.body) {
@@ -5345,7 +5351,7 @@ async function getJobArtifactFile(projectId, jobId, artifactPath) {
5345
5351
  const effectiveProjectId = getEffectiveProjectId(projectId);
5346
5352
  const encodedArtifactPath = artifactPath
5347
5353
  .split("/")
5348
- .map(segment => encodeURIComponent(segment))
5354
+ .map(segment => encodeGitLabPathSegment(segment))
5349
5355
  .join("/");
5350
5356
  const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/jobs/${encodeGitLabPathSegment(jobId)}/artifacts/${encodedArtifactPath}`);
5351
5357
  const response = await fetch(url.toString(), {
@@ -6487,9 +6493,20 @@ function getImageMimeType(filename) {
6487
6493
  const ext = path.extname(filename).toLowerCase();
6488
6494
  return IMAGE_MIME_TYPES[ext] ?? null;
6489
6495
  }
6496
+ function getSafeAttachmentFilename(filename) {
6497
+ if (!filename ||
6498
+ filename === "." ||
6499
+ filename === ".." ||
6500
+ filename.includes("/") ||
6501
+ filename.includes("\\")) {
6502
+ throw new Error("Invalid filename: directory separators are not allowed.");
6503
+ }
6504
+ return filename;
6505
+ }
6490
6506
  async function downloadAttachment(projectId, secret, filename, localPath) {
6491
6507
  const effectiveProjectId = getEffectiveProjectId(projectId);
6492
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/uploads/${secret}/${filename}`);
6508
+ const safeFilename = getSafeAttachmentFilename(filename);
6509
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/uploads/${encodeGitLabPathSegment(secret)}/${encodeGitLabPathSegment(safeFilename)}`);
6493
6510
  const response = await fetch(url.toString(), {
6494
6511
  ...getFetchConfig(),
6495
6512
  method: "GET",
@@ -6497,7 +6514,7 @@ async function downloadAttachment(projectId, secret, filename, localPath) {
6497
6514
  if (!response.ok) {
6498
6515
  await handleGitLabError(response);
6499
6516
  }
6500
- const mimeType = getImageMimeType(filename);
6517
+ const mimeType = getImageMimeType(safeFilename);
6501
6518
  // For non-image files, always save to disk.
6502
6519
  // For image files, only save to disk if local_path is explicitly provided.
6503
6520
  if (!mimeType || localPath) {
@@ -6510,10 +6527,10 @@ async function downloadAttachment(projectId, secret, filename, localPath) {
6510
6527
  normalizedLocalPath.includes(path.sep + ".." + path.sep)) {
6511
6528
  throw new Error("Invalid local_path: directory traversal is not allowed.");
6512
6529
  }
6513
- savePath = path.join(normalizedLocalPath, filename);
6530
+ savePath = path.join(normalizedLocalPath, safeFilename);
6514
6531
  }
6515
6532
  else {
6516
- savePath = filename;
6533
+ savePath = safeFilename;
6517
6534
  }
6518
6535
  const dir = path.dirname(savePath);
6519
6536
  if (!fs.existsSync(dir)) {
@@ -6524,11 +6541,11 @@ async function downloadAttachment(projectId, secret, filename, localPath) {
6524
6541
  throw new Error("No response body from GitLab");
6525
6542
  }
6526
6543
  await streamPipeline(response.body, fs.createWriteStream(savePath));
6527
- return { buffer: Buffer.alloc(0), filename, mimeType, savedPath: savePath };
6544
+ return { buffer: Buffer.alloc(0), filename: safeFilename, mimeType, savedPath: savePath };
6528
6545
  }
6529
6546
  // Images returned inline — buffer into memory for base64 encoding
6530
6547
  const buffer = Buffer.from(await response.arrayBuffer());
6531
- return { buffer, filename, mimeType };
6548
+ return { buffer, filename: safeFilename, mimeType };
6532
6549
  }
6533
6550
  /**
6534
6551
  * List all events for the currently authenticated user
@@ -6605,7 +6622,7 @@ async function listReleases(projectId, options = {}) {
6605
6622
  */
6606
6623
  async function getRelease(projectId, tagName, includeHtmlDescription) {
6607
6624
  const effectiveProjectId = getEffectiveProjectId(projectId);
6608
- const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/releases/${encodeURIComponent(tagName)}`);
6625
+ const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/releases/${encodeGitLabPathSegment(tagName)}`);
6609
6626
  if (includeHtmlDescription !== undefined) {
6610
6627
  url.searchParams.append("include_html_description", includeHtmlDescription.toString());
6611
6628
  }
@@ -6644,7 +6661,7 @@ async function createRelease(projectId, options) {
6644
6661
  */
6645
6662
  async function updateRelease(projectId, tagName, options) {
6646
6663
  const effectiveProjectId = getEffectiveProjectId(projectId);
6647
- const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/releases/${encodeURIComponent(tagName)}`, {
6664
+ const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/releases/${encodeGitLabPathSegment(tagName)}`, {
6648
6665
  ...getFetchConfig(),
6649
6666
  method: "PUT",
6650
6667
  body: JSON.stringify(options),
@@ -6662,7 +6679,7 @@ async function updateRelease(projectId, tagName, options) {
6662
6679
  */
6663
6680
  async function deleteRelease(projectId, tagName) {
6664
6681
  const effectiveProjectId = getEffectiveProjectId(projectId);
6665
- const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/releases/${encodeURIComponent(tagName)}`, {
6682
+ const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/releases/${encodeGitLabPathSegment(tagName)}`, {
6666
6683
  ...getFetchConfig(),
6667
6684
  method: "DELETE",
6668
6685
  });
@@ -6678,7 +6695,7 @@ async function deleteRelease(projectId, tagName) {
6678
6695
  */
6679
6696
  async function createReleaseEvidence(projectId, tagName) {
6680
6697
  const effectiveProjectId = getEffectiveProjectId(projectId);
6681
- const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/releases/${encodeURIComponent(tagName)}/evidence`, {
6698
+ const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/releases/${encodeGitLabPathSegment(tagName)}/evidence`, {
6682
6699
  ...getFetchConfig(),
6683
6700
  method: "POST",
6684
6701
  });
@@ -6694,7 +6711,7 @@ async function createReleaseEvidence(projectId, tagName) {
6694
6711
  */
6695
6712
  async function downloadReleaseAsset(projectId, tagName, directAssetPath) {
6696
6713
  const effectiveProjectId = getEffectiveProjectId(projectId);
6697
- const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/releases/${encodeURIComponent(tagName)}/downloads/${directAssetPath}`, {
6714
+ const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/releases/${encodeGitLabPathSegment(tagName)}/downloads/${encodeGitLabPath(directAssetPath)}`, {
6698
6715
  ...getFetchConfig(),
6699
6716
  });
6700
6717
  await handleGitLabError(response);
@@ -9536,13 +9553,16 @@ async function startStreamableHTTPServer() {
9536
9553
  clearAuthTimeout(sessionId);
9537
9554
  // Set new timeout
9538
9555
  authTimeouts[sessionId] = setTimeout(() => {
9539
- if (authBySession[sessionId]) {
9540
- logger.info(`Session ${sessionId}: auth token expired after ${SESSION_TIMEOUT_SECONDS}s of inactivity`);
9556
+ const authData = authBySession[sessionId];
9557
+ const transport = streamableTransports[sessionId];
9558
+ if (authData || transport) {
9559
+ logger.info(authData
9560
+ ? `Session ${sessionId}: auth token expired after ${SESSION_TIMEOUT_SECONDS}s of inactivity`
9561
+ : `Session ${sessionId}: unauthenticated discovery expired after ${SESSION_TIMEOUT_SECONDS}s`);
9541
9562
  delete authBySession[sessionId];
9542
9563
  delete authTimeouts[sessionId];
9543
9564
  metrics.expiredSessions++;
9544
9565
  // Close the transport to free the slot; without this, stale sessions accumulate and exhaust MAX_SESSIONS.
9545
- const transport = streamableTransports[sessionId];
9546
9566
  if (transport) {
9547
9567
  transport.close().catch(err => {
9548
9568
  logger.error(`Error closing transport for expired session ${sessionId}:`, err);
@@ -9975,10 +9995,12 @@ async function startStreamableHTTPServer() {
9975
9995
  return;
9976
9996
  }
9977
9997
  const newRemoteAuthData = !sessionId && REMOTE_AUTHORIZATION ? parseAuthHeaders(req) : null;
9998
+ let unauthenticatedDiscoveryRequested = false;
9978
9999
  let remoteAuthValidatedForInit = false;
9979
10000
  if (!sessionId && REMOTE_AUTHORIZATION) {
9980
10001
  const allowUnauthenticatedDiscovery = GITLAB_ALLOW_UNAUTHENTICATED_TOOL_DISCOVERY &&
9981
10002
  isUnauthenticatedDiscoveryRequestBody(req.body);
10003
+ unauthenticatedDiscoveryRequested = allowUnauthenticatedDiscovery && !newRemoteAuthData;
9982
10004
  if (!newRemoteAuthData && !allowUnauthenticatedDiscovery) {
9983
10005
  metrics.authFailures++;
9984
10006
  res.status(401).json({
@@ -10147,6 +10169,9 @@ async function startStreamableHTTPServer() {
10147
10169
  logger.info(`Session ${newSessionId}: stored ${authData.header} header`);
10148
10170
  setAuthTimeout(newSessionId);
10149
10171
  }
10172
+ else if (unauthenticatedDiscoveryRequested) {
10173
+ setAuthTimeout(newSessionId);
10174
+ }
10150
10175
  }
10151
10176
  // Store OAuth token for newly created session in MCP OAuth mode.
10152
10177
  // If Private-Token or JOB-TOKEN headers are present, prefer them.
@@ -14,6 +14,10 @@ const rawPathSegmentPatterns = [
14
14
  "/jobs/${jobId}",
15
15
  "/pipelines/${pipelineId}",
16
16
  "/milestones/${milestoneId}",
17
+ "/merge_requests/${merge_request_iid}/draft_notes/${draft_note_id}",
18
+ "/uploads/${secret}/${filename}",
19
+ "/downloads/${directAssetPath}",
20
+ "/${entity}/${entityIid}",
17
21
  ];
18
22
  test("GitLab URL path IDs are encoded before interpolation", () => {
19
23
  for (const pattern of rawPathSegmentPatterns) {
@@ -106,6 +106,21 @@ describe("Streamable HTTP unauthenticated tool discovery", { timeout: 20_000 },
106
106
  assert.ok(Array.isArray(listResponse.data.result?.tools), "tools/list should return tools");
107
107
  assert.ok(listResponse.data.result.tools.length > 0, "tools/list should not be empty");
108
108
  });
109
+ test("expires unauthenticated discovery sessions and frees capacity", async () => {
110
+ const { server, mcpUrl } = await launchRemoteAuthServer({
111
+ GITLAB_ALLOW_UNAUTHENTICATED_TOOL_DISCOVERY: "true",
112
+ MAX_SESSIONS: "1",
113
+ SESSION_TIMEOUT_SECONDS: "1",
114
+ });
115
+ servers.push(server);
116
+ await initialize(mcpUrl);
117
+ await new Promise(resolve => setTimeout(resolve, 2_000));
118
+ const healthResponse = await fetch(mcpUrl.replace("/mcp", "/health"));
119
+ assert.strictEqual(healthResponse.status, 200);
120
+ const nextSession = await rawMcpRequest(mcpUrl, initializeBody);
121
+ assert.strictEqual(nextSession.status, 200, nextSession.text);
122
+ assert.ok(nextSession.sessionId, "expired discovery session should free capacity");
123
+ });
109
124
  test("still blocks unauthenticated tools/call when discovery is enabled", async () => {
110
125
  const { server, mcpUrl } = await launchRemoteAuthServer({
111
126
  GITLAB_ALLOW_UNAUTHENTICATED_TOOL_DISCOVERY: "true",
@@ -125,6 +125,14 @@ describe('download_attachment', () => {
125
125
  raw.result.content.some(c => typeof c.text === 'string' && c.text.toLowerCase().includes('traversal'));
126
126
  assert.ok(isRpcError || isContentError, 'Should return an error mentioning directory traversal');
127
127
  });
128
+ test('filename with directory traversal is rejected before writing to disk', async () => {
129
+ const raw = await callDownloadAttachment({ project_id: TEST_PROJECT_ID, secret: TEST_SECRET, filename: 'image.png#../../owned.txt' }, env);
130
+ const isRpcError = typeof raw.error?.message === 'string' &&
131
+ raw.error.message.toLowerCase().includes('filename');
132
+ const isContentError = Array.isArray(raw.result?.content) &&
133
+ raw.result.content.some(c => typeof c.text === 'string' && c.text.toLowerCase().includes('filename'));
134
+ assert.ok(isRpcError || isContentError, 'Should return an error mentioning filename validation');
135
+ });
128
136
  test('non-existent local_path directory is auto-created before saving', async () => {
129
137
  const baseDir = `omc-test-newdir-${RUN_ID}`;
130
138
  const localPath = `${baseDir}/subdir`;
@@ -582,7 +582,7 @@ describe('getEffectiveProjectId', { concurrency: 1 }, () => {
582
582
  REMOTE_AUTHORIZATION: 'true',
583
583
  GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
584
584
  GITLAB_ALLOWED_PROJECT_IDS: DEFAULT_PROJECT_ID,
585
- GITLAB_TOOLSETS: 'variables',
585
+ GITLAB_TOOLSETS: 'variables,merge_requests,issues,pipelines',
586
586
  }
587
587
  });
588
588
  servers.push(server);
@@ -657,6 +657,32 @@ describe('getEffectiveProjectId', { concurrency: 1 }, () => {
657
657
  const project = JSON.parse(content.text);
658
658
  assert.strictEqual(project.id.toString(), DEFAULT_PROJECT_ID, 'Should use default project');
659
659
  });
660
+ test('should enforce the project allowlist for get_draft_note', async () => {
661
+ await assert.rejects(client.callTool('get_draft_note', {
662
+ project_id: OTHER_PROJECT_ID,
663
+ merge_request_iid: '1',
664
+ draft_note_id: '1',
665
+ }), /Access denied/);
666
+ });
667
+ test('should reject dot path segments before making GitLab requests', async () => {
668
+ const calls = [
669
+ {
670
+ name: 'get_draft_note',
671
+ args: { project_id: DEFAULT_PROJECT_ID, merge_request_iid: '..', draft_note_id: '1' },
672
+ },
673
+ {
674
+ name: 'get_job_artifact_file',
675
+ args: { project_id: DEFAULT_PROJECT_ID, job_id: '1', artifact_path: '../file.txt' },
676
+ },
677
+ {
678
+ name: 'list_issue_emoji_reactions',
679
+ args: { project_id: DEFAULT_PROJECT_ID, issue_iid: '..' },
680
+ },
681
+ ];
682
+ for (const { name, args } of calls) {
683
+ await assert.rejects(client.callTool(name, args), /path segments/);
684
+ }
685
+ });
660
686
  });
661
687
  describe('GITLAB_READ_ONLY_MODE enforces read-only for all write tools', () => {
662
688
  let mcpUrl;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zereight/mcp-gitlab",
3
- "version": "2.1.39",
3
+ "version": "2.1.41",
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": [