@zereight/mcp-gitlab 2.1.40 → 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 +1 -1
- package/README.md +212 -178
- package/README.zh-CN.md +1 -1
- package/build/index.js +50 -25
- package/build/test/path-segment-encoding.test.js +4 -0
- package/build/test/streamable-http-unauthenticated-discovery.test.js +15 -0
- package/build/test/test-download-attachment.js +8 -0
- package/build/test/test-geteffectiveprojectid.js +27 -1
- package/package.json +1 -1
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.
|
|
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.
|
|
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. `
|
|
545
|
-
3. `
|
|
546
|
-
4. `
|
|
547
|
-
5. `
|
|
548
|
-
6. `
|
|
549
|
-
7. `
|
|
550
|
-
8. `
|
|
551
|
-
9. `
|
|
552
|
-
10. `
|
|
553
|
-
11. `
|
|
554
|
-
12. `
|
|
555
|
-
13. `
|
|
556
|
-
14. `
|
|
557
|
-
15. `
|
|
558
|
-
16. `
|
|
559
|
-
17. `
|
|
560
|
-
18. `
|
|
561
|
-
19. `
|
|
562
|
-
20. `
|
|
563
|
-
21. `
|
|
564
|
-
22. `
|
|
565
|
-
23. `
|
|
566
|
-
24. `
|
|
567
|
-
25. `
|
|
568
|
-
26. `
|
|
569
|
-
27. `
|
|
570
|
-
28. `
|
|
571
|
-
29. `
|
|
572
|
-
30. `
|
|
573
|
-
31. `
|
|
574
|
-
32. `
|
|
575
|
-
33. `
|
|
576
|
-
34. `
|
|
577
|
-
35. `
|
|
578
|
-
36. `
|
|
579
|
-
37. `
|
|
580
|
-
38. `
|
|
581
|
-
39. `
|
|
582
|
-
40. `
|
|
583
|
-
41. `
|
|
584
|
-
42. `
|
|
585
|
-
43. `
|
|
586
|
-
44. `
|
|
587
|
-
45. `
|
|
588
|
-
46. `
|
|
589
|
-
47. `
|
|
590
|
-
48. `
|
|
591
|
-
49. `
|
|
592
|
-
50. `
|
|
593
|
-
51. `
|
|
594
|
-
52. `
|
|
595
|
-
53. `
|
|
596
|
-
54. `
|
|
597
|
-
55. `
|
|
598
|
-
56. `
|
|
599
|
-
57. `
|
|
600
|
-
58. `
|
|
601
|
-
59. `
|
|
602
|
-
60. `
|
|
603
|
-
61. `
|
|
604
|
-
62. `
|
|
605
|
-
63. `
|
|
606
|
-
64. `
|
|
607
|
-
65. `
|
|
608
|
-
66. `
|
|
609
|
-
67. `
|
|
610
|
-
68. `
|
|
611
|
-
69. `
|
|
612
|
-
70. `
|
|
613
|
-
71. `
|
|
614
|
-
72. `
|
|
615
|
-
73. `
|
|
616
|
-
74. `
|
|
617
|
-
75. `
|
|
618
|
-
76. `
|
|
619
|
-
77. `
|
|
620
|
-
78. `
|
|
621
|
-
79. `
|
|
622
|
-
80. `
|
|
623
|
-
81. `
|
|
624
|
-
82. `
|
|
625
|
-
83. `
|
|
626
|
-
84. `
|
|
627
|
-
85. `
|
|
628
|
-
86. `
|
|
629
|
-
87. `
|
|
630
|
-
88. `
|
|
631
|
-
89. `
|
|
632
|
-
90. `
|
|
633
|
-
91. `
|
|
634
|
-
92. `
|
|
635
|
-
93. `
|
|
636
|
-
94. `
|
|
637
|
-
95. `
|
|
638
|
-
96. `
|
|
639
|
-
97. `
|
|
640
|
-
98. `
|
|
641
|
-
99. `
|
|
642
|
-
100. `
|
|
643
|
-
101. `
|
|
644
|
-
102. `
|
|
645
|
-
103. `
|
|
646
|
-
104. `
|
|
647
|
-
105. `
|
|
648
|
-
106. `
|
|
649
|
-
107. `
|
|
650
|
-
108. `
|
|
651
|
-
109. `
|
|
652
|
-
110. `
|
|
653
|
-
111. `
|
|
654
|
-
112. `
|
|
655
|
-
113. `
|
|
656
|
-
114. `
|
|
657
|
-
115. `
|
|
658
|
-
116. `
|
|
659
|
-
117. `
|
|
660
|
-
118. `
|
|
661
|
-
119. `
|
|
662
|
-
120. `
|
|
663
|
-
121. `
|
|
664
|
-
122. `
|
|
665
|
-
123. `
|
|
666
|
-
124. `
|
|
667
|
-
125. `
|
|
668
|
-
126. `
|
|
669
|
-
127. `
|
|
670
|
-
128. `
|
|
671
|
-
129. `
|
|
672
|
-
130. `
|
|
673
|
-
131. `
|
|
674
|
-
132. `
|
|
675
|
-
133. `
|
|
676
|
-
134. `
|
|
677
|
-
135. `
|
|
678
|
-
136. `
|
|
679
|
-
137. `
|
|
680
|
-
138. `
|
|
681
|
-
139. `
|
|
682
|
-
140. `
|
|
683
|
-
141. `
|
|
684
|
-
142. `
|
|
685
|
-
143. `
|
|
686
|
-
144. `
|
|
687
|
-
145. `
|
|
688
|
-
146. `
|
|
689
|
-
147. `
|
|
690
|
-
148. `
|
|
691
|
-
149. `
|
|
692
|
-
150. `
|
|
693
|
-
151. `
|
|
694
|
-
152. `
|
|
695
|
-
153. `
|
|
696
|
-
154. `
|
|
697
|
-
155. `
|
|
698
|
-
156. `
|
|
699
|
-
157. `
|
|
700
|
-
158. `
|
|
701
|
-
159. `
|
|
702
|
-
160. `
|
|
703
|
-
161. `
|
|
704
|
-
162. `
|
|
705
|
-
163. `
|
|
706
|
-
164. `
|
|
707
|
-
165. `
|
|
708
|
-
166. `
|
|
709
|
-
167. `
|
|
710
|
-
168. `
|
|
711
|
-
169. `
|
|
712
|
-
170. `
|
|
713
|
-
171. `
|
|
714
|
-
172. `
|
|
715
|
-
173. `
|
|
716
|
-
174. `
|
|
717
|
-
175. `
|
|
718
|
-
176. `
|
|
719
|
-
177. `
|
|
720
|
-
178. `
|
|
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.
|
|
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
|
-
|
|
1127
|
+
decodedSegment = decodeURIComponent(segment);
|
|
1127
1128
|
}
|
|
1128
1129
|
catch {
|
|
1129
|
-
|
|
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
|
|
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 =>
|
|
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
|
|
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(
|
|
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,
|
|
6530
|
+
savePath = path.join(normalizedLocalPath, safeFilename);
|
|
6514
6531
|
}
|
|
6515
6532
|
else {
|
|
6516
|
-
savePath =
|
|
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/${
|
|
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/${
|
|
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/${
|
|
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/${
|
|
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/${
|
|
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
|
-
|
|
9540
|
-
|
|
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