@zereight/mcp-gitlab 2.1.12 → 2.1.13
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.md +152 -151
- package/build/index.js +78 -8
- package/build/schemas.js +77 -0
- package/build/test/test-get-file-blame.js +145 -0
- package/build/test/test-geteffectiveprojectid.js +230 -6
- package/build/test/test-toolset-filtering.js +4 -1
- package/build/tools/registry.js +20 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -495,157 +495,158 @@ Register the skill directory in your AI client to get optimal tool usage guidanc
|
|
|
495
495
|
2. `create_or_update_file` - Create or update a single file in a GitLab project
|
|
496
496
|
3. `search_repositories` - Search for GitLab projects
|
|
497
497
|
4. `create_repository` - Create a new GitLab project
|
|
498
|
-
5. `
|
|
499
|
-
6. `
|
|
500
|
-
7. `
|
|
501
|
-
8. `
|
|
502
|
-
9. `
|
|
503
|
-
10. `
|
|
504
|
-
11. `
|
|
505
|
-
12. `
|
|
506
|
-
13. `
|
|
507
|
-
14. `
|
|
508
|
-
15. `
|
|
509
|
-
16. `
|
|
510
|
-
17. `
|
|
511
|
-
18. `
|
|
512
|
-
19. `
|
|
513
|
-
20. `
|
|
514
|
-
21. `
|
|
515
|
-
22. `
|
|
516
|
-
23. `
|
|
517
|
-
24. `
|
|
518
|
-
25. `
|
|
519
|
-
26. `
|
|
520
|
-
27. `
|
|
521
|
-
28. `
|
|
522
|
-
29. `
|
|
523
|
-
30. `
|
|
524
|
-
31. `
|
|
525
|
-
32. `
|
|
526
|
-
33. `
|
|
527
|
-
34. `
|
|
528
|
-
35. `
|
|
529
|
-
36. `
|
|
530
|
-
37. `
|
|
531
|
-
38. `
|
|
532
|
-
39. `
|
|
533
|
-
40. `
|
|
534
|
-
41. `
|
|
535
|
-
42. `
|
|
536
|
-
43. `
|
|
537
|
-
44. `
|
|
538
|
-
45. `
|
|
539
|
-
46. `
|
|
540
|
-
47. `
|
|
541
|
-
48. `
|
|
542
|
-
49. `
|
|
543
|
-
50. `
|
|
544
|
-
51. `
|
|
545
|
-
52. `
|
|
546
|
-
53. `
|
|
547
|
-
54. `
|
|
548
|
-
55. `
|
|
549
|
-
56. `
|
|
550
|
-
57. `
|
|
551
|
-
58. `
|
|
552
|
-
59. `
|
|
553
|
-
60. `
|
|
554
|
-
61. `
|
|
555
|
-
62. `
|
|
556
|
-
63. `
|
|
557
|
-
64. `
|
|
558
|
-
65. `
|
|
559
|
-
66. `
|
|
560
|
-
67. `
|
|
561
|
-
68. `
|
|
562
|
-
69. `
|
|
563
|
-
70. `
|
|
564
|
-
71. `
|
|
565
|
-
72. `
|
|
566
|
-
73. `
|
|
567
|
-
74. `
|
|
568
|
-
75. `
|
|
569
|
-
76. `
|
|
570
|
-
77. `
|
|
571
|
-
78. `
|
|
572
|
-
79. `
|
|
573
|
-
80. `
|
|
574
|
-
81. `
|
|
575
|
-
82. `
|
|
576
|
-
83. `
|
|
577
|
-
84. `
|
|
578
|
-
85. `
|
|
579
|
-
86. `
|
|
580
|
-
87. `
|
|
581
|
-
88. `
|
|
582
|
-
89. `
|
|
583
|
-
90. `
|
|
584
|
-
91. `
|
|
585
|
-
92. `
|
|
586
|
-
93. `
|
|
587
|
-
94. `
|
|
588
|
-
95. `
|
|
589
|
-
96. `
|
|
590
|
-
97. `
|
|
591
|
-
98. `
|
|
592
|
-
99. `
|
|
593
|
-
100. `
|
|
594
|
-
101. `
|
|
595
|
-
102. `
|
|
596
|
-
103. `
|
|
597
|
-
104. `
|
|
598
|
-
105. `
|
|
599
|
-
106. `
|
|
600
|
-
107. `
|
|
601
|
-
108. `
|
|
602
|
-
109. `
|
|
603
|
-
110. `
|
|
604
|
-
111. `
|
|
605
|
-
112. `
|
|
606
|
-
113. `
|
|
607
|
-
114. `
|
|
608
|
-
115. `
|
|
609
|
-
116. `
|
|
610
|
-
117. `
|
|
611
|
-
118. `
|
|
612
|
-
119. `
|
|
613
|
-
120. `
|
|
614
|
-
121. `
|
|
615
|
-
122. `
|
|
616
|
-
123. `
|
|
617
|
-
124. `
|
|
618
|
-
125. `
|
|
619
|
-
126. `
|
|
620
|
-
127. `
|
|
621
|
-
128. `
|
|
622
|
-
129. `
|
|
623
|
-
130. `
|
|
624
|
-
131. `
|
|
625
|
-
132. `
|
|
626
|
-
133. `
|
|
627
|
-
134. `
|
|
628
|
-
135. `
|
|
629
|
-
136. `
|
|
630
|
-
137. `
|
|
631
|
-
138. `
|
|
632
|
-
139. `
|
|
633
|
-
140. `
|
|
634
|
-
141. `
|
|
635
|
-
142. `
|
|
636
|
-
143. `
|
|
637
|
-
144. `
|
|
638
|
-
145. `
|
|
639
|
-
146. `
|
|
640
|
-
147. `
|
|
641
|
-
148. `
|
|
642
|
-
149. `
|
|
643
|
-
150. `
|
|
644
|
-
151. `
|
|
645
|
-
152. `
|
|
646
|
-
153. `
|
|
647
|
-
154. `
|
|
648
|
-
155. `
|
|
498
|
+
5. `create_group` - Create a new GitLab group or subgroup (name, path, description, visibility, and optional parent_id)
|
|
499
|
+
6. `get_file_contents` - Get the contents of a file or directory from a GitLab project
|
|
500
|
+
7. `push_files` - Push multiple files to a GitLab project in a single commit
|
|
501
|
+
8. `create_issue` - Create a new issue in a GitLab project
|
|
502
|
+
9. `create_merge_request` - Create a new merge request in a GitLab project
|
|
503
|
+
10. `fork_repository` - Fork a GitLab project to your account or specified namespace
|
|
504
|
+
11. `create_branch` - Create a new branch in a GitLab project
|
|
505
|
+
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)
|
|
506
|
+
13. `get_merge_request_diffs` - Get the changes/diffs of a merge request (Either mergeRequestIid or branchName must be provided)
|
|
507
|
+
14. `list_merge_request_diffs` - List merge request diffs with pagination support (Either mergeRequestIid or branchName must be provided)
|
|
508
|
+
15. `get_merge_request_conflicts` - Get the conflicts of a merge request in a GitLab project
|
|
509
|
+
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)
|
|
510
|
+
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)
|
|
511
|
+
18. `list_merge_request_versions` - List all versions of a merge request
|
|
512
|
+
19. `get_merge_request_version` - Get a specific version of a merge request
|
|
513
|
+
20. `get_branch_diffs` - Get the changes/diffs between two branches or commits in a GitLab project
|
|
514
|
+
21. `update_merge_request` - Update a merge request (Either mergeRequestIid or branchName must be provided)
|
|
515
|
+
22. `create_note` - Create a new note (comment) to an issue or merge request
|
|
516
|
+
23. `create_merge_request_thread` - Create a new thread on a merge request
|
|
517
|
+
24. `mr_discussions` - List discussion items for a merge request
|
|
518
|
+
25. `resolve_merge_request_thread` - Resolve a thread on a merge request
|
|
519
|
+
26. `update_merge_request_note` - Modify an existing merge request thread note
|
|
520
|
+
27. `create_merge_request_note` - Add a new note to an existing merge request thread
|
|
521
|
+
28. `delete_merge_request_discussion_note` - Delete a discussion note on a merge request
|
|
522
|
+
29. `update_merge_request_discussion_note` - Update a discussion note on a merge request
|
|
523
|
+
30. `create_merge_request_discussion_note` - Add a new discussion note to an existing merge request thread
|
|
524
|
+
31. `delete_merge_request_note` - Delete an existing merge request note
|
|
525
|
+
32. `get_merge_request_note` - Get a specific note for a merge request
|
|
526
|
+
33. `get_merge_request_notes` - List notes for a merge request
|
|
527
|
+
34. `get_draft_note` - Get a single draft note from a merge request
|
|
528
|
+
35. `list_draft_notes` - List draft notes for a merge request
|
|
529
|
+
36. `create_draft_note` - Create a draft note for a merge request
|
|
530
|
+
37. `update_draft_note` - Update an existing draft note
|
|
531
|
+
38. `delete_draft_note` - Delete a draft note
|
|
532
|
+
39. `publish_draft_note` - Publish a single draft note
|
|
533
|
+
40. `bulk_publish_draft_notes` - Publish all draft notes for a merge request
|
|
534
|
+
41. `list_merge_requests` - List merge requests globally or in a specific GitLab project with filtering options (project_id is now optional)
|
|
535
|
+
42. `approve_merge_request` - Approve a merge request (requires appropriate permissions)
|
|
536
|
+
43. `unapprove_merge_request` - Unapprove a previously approved merge request
|
|
537
|
+
44. `get_merge_request_approval_state` - Get merge request approval details including approvers (uses `approval_state` when available, otherwise falls back to `approvals`)
|
|
538
|
+
45. `update_issue_note` - Modify an existing issue thread note
|
|
539
|
+
46. `create_issue_note` - Add a new note to an existing issue thread
|
|
540
|
+
47. `list_issues` - List issues (default: created by current user only; use scope='all' for all accessible issues)
|
|
541
|
+
48. `my_issues` - List issues assigned to the authenticated user (defaults to open issues)
|
|
542
|
+
49. `get_issue` - Get details of a specific issue in a GitLab project
|
|
543
|
+
50. `update_issue` - Update an issue in a GitLab project
|
|
544
|
+
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.
|
|
545
|
+
52. `delete_issue` - Delete an issue from a GitLab project
|
|
546
|
+
53. `list_todos` - List GitLab to-do items for the current user
|
|
547
|
+
54. `mark_todo_done` - Mark a GitLab to-do item as done
|
|
548
|
+
55. `mark_all_todos_done` - Mark all pending GitLab to-do items as done for the current user
|
|
549
|
+
56. `list_issue_links` - List all issue links for a specific issue
|
|
550
|
+
57. `list_issue_discussions` - List discussions for an issue in a GitLab project
|
|
551
|
+
58. `get_issue_link` - Get a specific issue link
|
|
552
|
+
59. `create_issue_link` - Create an issue link between two issues
|
|
553
|
+
60. `delete_issue_link` - Delete an issue link
|
|
554
|
+
61. `list_namespaces` - List all namespaces available to the current user
|
|
555
|
+
62. `get_namespace` - Get details of a namespace by ID or path
|
|
556
|
+
63. `verify_namespace` - Verify if a namespace path exists
|
|
557
|
+
64. `get_project` - Get details of a specific project
|
|
558
|
+
65. `list_projects` - List projects accessible by the current user
|
|
559
|
+
66. `list_project_members` - List members of a GitLab project
|
|
560
|
+
67. `list_group_projects` - List projects in a GitLab group with filtering options
|
|
561
|
+
68. `list_group_iterations` - List group iterations with filtering options
|
|
562
|
+
69. `list_labels` - List labels for a project
|
|
563
|
+
70. `get_label` - Get a single label from a project
|
|
564
|
+
71. `create_label` - Create a new label in a project
|
|
565
|
+
72. `update_label` - Update an existing label in a project
|
|
566
|
+
73. `delete_label` - Delete a label from a project
|
|
567
|
+
74. `list_pipelines` - List pipelines in a GitLab project with filtering options
|
|
568
|
+
75. `get_pipeline` - Get details of a specific pipeline in a GitLab project
|
|
569
|
+
76. `list_pipeline_jobs` - List all jobs in a specific pipeline
|
|
570
|
+
77. `list_pipeline_trigger_jobs` - List all trigger jobs (bridges) in a specific pipeline that trigger downstream pipelines
|
|
571
|
+
78. `get_pipeline_job` - Get details of a GitLab pipeline job number
|
|
572
|
+
79. `get_pipeline_job_output` - Get the output/trace of a GitLab pipeline job with optional pagination to limit context window usage
|
|
573
|
+
80. `validate_ci_lint` - Validate provided GitLab CI/CD YAML content for a project
|
|
574
|
+
81. `validate_project_ci_lint` - Validate an existing `.gitlab-ci.yml` configuration for a project
|
|
575
|
+
82. `create_pipeline` - Create a new pipeline for a branch or tag
|
|
576
|
+
83. `retry_pipeline` - Retry a failed or canceled pipeline
|
|
577
|
+
84. `cancel_pipeline` - Cancel a running pipeline
|
|
578
|
+
85. `play_pipeline_job` - Run a manual pipeline job
|
|
579
|
+
86. `retry_pipeline_job` - Retry a failed or canceled pipeline job
|
|
580
|
+
87. `cancel_pipeline_job` - Cancel a running pipeline job
|
|
581
|
+
88. `list_deployments` - List deployments in a GitLab project with filtering options
|
|
582
|
+
89. `get_deployment` - Get details of a specific deployment in a GitLab project
|
|
583
|
+
90. `list_environments` - List environments in a GitLab project
|
|
584
|
+
91. `get_environment` - Get details of a specific environment in a GitLab project
|
|
585
|
+
92. `list_job_artifacts` - List artifact files in a job's artifacts archive. Returns file names, paths, types, and sizes
|
|
586
|
+
93. `download_job_artifacts` - Download the entire artifact archive (zip) for a job to a local path. Returns the saved file path
|
|
587
|
+
94. `get_job_artifact_file` - Get the content of a single file from a job's artifacts by its path within the archive
|
|
588
|
+
95. `list_milestones` - List milestones in a GitLab project with filtering options
|
|
589
|
+
96. `get_milestone` - Get details of a specific milestone
|
|
590
|
+
97. `create_milestone` - Create a new milestone in a GitLab project
|
|
591
|
+
98. `edit_milestone` - Edit an existing milestone in a GitLab project
|
|
592
|
+
99. `delete_milestone` - Delete a milestone from a GitLab project
|
|
593
|
+
100. `get_milestone_issue` - Get issues associated with a specific milestone
|
|
594
|
+
101. `get_milestone_merge_requests` - Get merge requests associated with a specific milestone
|
|
595
|
+
102. `promote_milestone` - Promote a milestone to the next stage
|
|
596
|
+
103. `get_milestone_burndown_events` - Get burndown events for a specific milestone
|
|
597
|
+
104. `list_wiki_pages` - List wiki pages in a GitLab project
|
|
598
|
+
105. `get_wiki_page` - Get details of a specific wiki page
|
|
599
|
+
106. `create_wiki_page` - Create a new wiki page in a GitLab project
|
|
600
|
+
107. `update_wiki_page` - Update an existing wiki page in a GitLab project
|
|
601
|
+
108. `delete_wiki_page` - Delete a wiki page from a GitLab project
|
|
602
|
+
109. `list_group_wiki_pages` - List wiki pages in a GitLab group
|
|
603
|
+
110. `get_group_wiki_page` - Get details of a specific group wiki page
|
|
604
|
+
111. `create_group_wiki_page` - Create a new wiki page in a GitLab group
|
|
605
|
+
112. `update_group_wiki_page` - Update an existing wiki page in a GitLab group
|
|
606
|
+
113. `delete_group_wiki_page` - Delete a wiki page from a GitLab group
|
|
607
|
+
114. `get_repository_tree` - Get the repository tree for a GitLab project (list files and directories)
|
|
608
|
+
115. `list_commits` - List repository commits with filtering options
|
|
609
|
+
116. `get_commit` - Get details of a specific commit
|
|
610
|
+
117. `get_commit_diff` - Get changes/diffs of a specific commit
|
|
611
|
+
118. `list_commit_statuses` - List statuses for a specific commit
|
|
612
|
+
119. `create_commit_status` - Create or update the status of a specific commit
|
|
613
|
+
120. `list_releases` - List all releases for a project
|
|
614
|
+
121. `get_release` - Get a release by tag name
|
|
615
|
+
122. `create_release` - Create a new release in a GitLab project
|
|
616
|
+
123. `update_release` - Update an existing release in a GitLab project
|
|
617
|
+
124. `delete_release` - Delete a release from a GitLab project (does not delete the associated tag)
|
|
618
|
+
125. `create_release_evidence` - Create release evidence for an existing release (GitLab Premium/Ultimate only)
|
|
619
|
+
126. `download_release_asset` - Download a release asset file by direct asset path
|
|
620
|
+
127. `list_tags` - List repository tags with filtering and pagination support
|
|
621
|
+
128. `get_tag` - Get details of a specific repository tag
|
|
622
|
+
129. `create_tag` - Create a new tag in the repository
|
|
623
|
+
130. `delete_tag` - Delete a tag from the repository
|
|
624
|
+
131. `get_tag_signature` - Get the signature of a signed tag
|
|
625
|
+
132. `get_users` - Get GitLab user details by usernames
|
|
626
|
+
133. `list_events` - List all events for the currently authenticated user
|
|
627
|
+
134. `get_project_events` - List all visible events for a specified project
|
|
628
|
+
135. `upload_markdown` - Upload a file to a GitLab project for use in markdown content
|
|
629
|
+
136. `download_attachment` - Download an uploaded file from a GitLab project by secret and filename
|
|
630
|
+
137. `get_work_item` - Get a single work item with full details including status, hierarchy (parent/children), type, labels, assignees, and all widgets
|
|
631
|
+
138. `list_work_items` - List work items in a project with filters (type, state, search, assignees, labels). Returns items with status and hierarchy info
|
|
632
|
+
139. `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
|
|
633
|
+
140. `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
|
|
634
|
+
141. `convert_work_item_type` - Convert a work item to a different type (e.g. issue to task, task to incident)
|
|
635
|
+
142. `list_work_item_statuses` - List available statuses for a work item type in a project. Requires GitLab Premium/Ultimate with configurable statuses
|
|
636
|
+
143. `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
|
|
637
|
+
144. `move_work_item` - Move a work item (issue, task, etc.) to a different project. Uses GitLab GraphQL issueMove mutation
|
|
638
|
+
145. `list_work_item_notes` - List notes and discussions on a work item. Returns threaded discussions with author, body, timestamps, and system/internal flags
|
|
639
|
+
146. `create_work_item_note` - Add a note/comment to a work item. Supports Markdown, internal notes, and threaded replies
|
|
640
|
+
147. `get_timeline_events` - List timeline events for an incident. Returns chronological events with notes, timestamps, and tags
|
|
641
|
+
148. `create_timeline_event` - Create a timeline event on an incident. Supports tags: 'Start time', 'End time', 'Impact detected', 'Response initiated', 'Impact mitigated', 'Cause identified'
|
|
642
|
+
149. `list_webhooks` - List all configured webhooks for a GitLab project or group. Provide either project_id or group_id
|
|
643
|
+
150. `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
|
|
644
|
+
151. `get_webhook_event` - Get full details of a specific webhook event by ID, including request/response payloads
|
|
645
|
+
152. `search_code` - Search for code across all projects on the GitLab instance (requires advanced search or exact code search to be enabled)
|
|
646
|
+
153. `search_project_code` - Search for code within a specific GitLab project (requires advanced search or exact code search to be enabled)
|
|
647
|
+
154. `search_group_code` - Search for code within a specific GitLab group (requires advanced search or exact code search to be enabled)
|
|
648
|
+
155. `execute_graphql` - Execute a GitLab GraphQL query
|
|
649
|
+
156. `list_merge_request_pipelines` - List pipelines for a merge request with pagination support
|
|
649
650
|
|
|
650
651
|
<!-- TOOLS-END -->
|
|
651
652
|
|
package/build/index.js
CHANGED
|
@@ -26,12 +26,12 @@ import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middlew
|
|
|
26
26
|
import { GitLabClientPool } from "./gitlab-client-pool.js";
|
|
27
27
|
import { allTools, readOnlyTools, destructiveTools, parseEnabledToolsets, parseIndividualTools, buildFeatureFlagOverrides, isToolInEnabledToolset, TOOLSET_DEFINITIONS, ALL_TOOLSET_IDS, } from "./tools/registry.js";
|
|
28
28
|
import { BulkPublishDraftNotesSchema, CancelPipelineJobSchema, CancelPipelineSchema, CreateBranchSchema, CreateDraftNoteSchema, CreateIssueLinkSchema, CreateIssueNoteSchema, CreateIssueSchema, CreateIssueEmojiReactionSchema, CreateIssueNoteEmojiReactionSchema, ListIssueEmojiReactionsSchema, ListIssueNoteEmojiReactionsSchema, CreateLabelSchema, // Added
|
|
29
|
-
CreateMergeRequestNoteSchema, CreateMergeRequestDiscussionNoteSchema, CreateMergeRequestEmojiReactionSchema, CreateMergeRequestNoteEmojiReactionSchema, ListMergeRequestEmojiReactionsSchema, ListMergeRequestNoteEmojiReactionsSchema, CreateMergeRequestSchema, CreateMergeRequestThreadSchema, CreateNoteSchema, CreateCommitStatusSchema, CreateOrUpdateFileSchema, CreatePipelineSchema, CreateProjectMilestoneSchema, CreateRepositorySchema, CreateWikiPageSchema, CreateGroupWikiPageSchema, DeleteBranchSchema, DeleteDraftNoteSchema, DeleteGroupWikiPageSchema, DeleteIssueLinkSchema, DeleteIssueSchema, DeleteIssueEmojiReactionSchema, DeleteIssueNoteEmojiReactionSchema, DeleteLabelSchema, DeleteProjectMilestoneSchema, DeleteWikiPageSchema, DeleteMergeRequestNoteSchema, DeleteMergeRequestEmojiReactionSchema, DeleteMergeRequestNoteEmojiReactionSchema, EditProjectMilestoneSchema, ForkRepositorySchema, GetBranchDiffsSchema, GetBranchSchema, GetCommitDiffSchema, GetCommitSchema, GetDraftNoteSchema, GetFileContentsSchema, GetIssueLinkSchema, GetIssueSchema, GetLabelSchema, GetMergeRequestDiffsSchema, GetMergeRequestSchema, GetMilestoneBurndownEventsSchema, GetMilestoneIssuesSchema, GetMilestoneMergeRequestsSchema, GetDeploymentSchema, GetEnvironmentSchema, GetNamespaceSchema, GitLabCiLintResultSchema, GetPipelineJobOutputSchema, GetPipelineSchema, GetProjectMilestoneSchema, GetProjectSchema, GetRepositoryTreeSchema, GetUsersSchema, GetUserSchema, GitLabUserFullSchema, WhoAmISchema, GitLabCurrentUserSchema, GetWikiPageSchema, GitLabCommitSchema, GitLabCommitStatusSchema, GitLabCompareResultSchema, GitLabContentSchema, GitLabCreateUpdateFileResponseSchema, GitLabDiffSchema,
|
|
29
|
+
CreateMergeRequestNoteSchema, CreateMergeRequestDiscussionNoteSchema, CreateMergeRequestEmojiReactionSchema, CreateMergeRequestNoteEmojiReactionSchema, ListMergeRequestEmojiReactionsSchema, ListMergeRequestNoteEmojiReactionsSchema, CreateMergeRequestSchema, CreateMergeRequestThreadSchema, CreateNoteSchema, CreateCommitStatusSchema, CreateOrUpdateFileSchema, CreatePipelineSchema, CreateProjectMilestoneSchema, CreateRepositorySchema, CreateGroupSchema, CreateWikiPageSchema, CreateGroupWikiPageSchema, DeleteBranchSchema, DeleteDraftNoteSchema, DeleteGroupWikiPageSchema, DeleteIssueLinkSchema, DeleteIssueSchema, DeleteIssueEmojiReactionSchema, DeleteIssueNoteEmojiReactionSchema, DeleteLabelSchema, DeleteProjectMilestoneSchema, DeleteWikiPageSchema, DeleteMergeRequestNoteSchema, DeleteMergeRequestEmojiReactionSchema, DeleteMergeRequestNoteEmojiReactionSchema, EditProjectMilestoneSchema, ForkRepositorySchema, GetBranchDiffsSchema, GetBranchSchema, GetCommitDiffSchema, GetCommitSchema, GetFileBlameSchema, GitLabBlameEntrySchema, GetDraftNoteSchema, GetFileContentsSchema, GetIssueLinkSchema, GetIssueSchema, GetLabelSchema, GetMergeRequestDiffsSchema, GetMergeRequestSchema, GetMilestoneBurndownEventsSchema, GetMilestoneIssuesSchema, GetMilestoneMergeRequestsSchema, GetDeploymentSchema, GetEnvironmentSchema, GetNamespaceSchema, GitLabCiLintResultSchema, GetPipelineJobOutputSchema, GetPipelineSchema, GetProjectMilestoneSchema, GetProjectSchema, GetRepositoryTreeSchema, GetUsersSchema, GetUserSchema, GitLabUserFullSchema, WhoAmISchema, GitLabCurrentUserSchema, GetWikiPageSchema, GitLabCommitSchema, GitLabCommitStatusSchema, GitLabCompareResultSchema, GitLabContentSchema, GitLabCreateUpdateFileResponseSchema, GitLabDiffSchema,
|
|
30
30
|
// Discussion Schemas
|
|
31
31
|
GitLabDiscussionNoteSchema, // Added
|
|
32
32
|
GitLabDiscussionSchema,
|
|
33
33
|
// Draft Notes Schemas
|
|
34
|
-
GitLabDraftNoteSchema, GitLabForkSchema, GitLabBranchSchema, GitLabIssueLinkSchema, GitLabIssueSchema, GitLabIssueWithLinkDetailsSchema, GitLabMarkdownUploadSchema, GitLabMergeRequestPipelineSchema, GitLabMergeRequestSchema, GitLabMilestonesSchema, GitLabNamespaceExistsResponseSchema, GitLabNamespaceSchema, GitLabPipelineJobSchema, GitLabDeploymentSchema, GitLabEnvironmentSchema, GitLabPipelineSchema, GitLabPipelineTriggerJobSchema, GitLabProjectMemberSchema, GitLabProjectSchema, GitLabTodoSchema, GitLabReferenceSchema, GitLabRepositorySchema, GitLabSearchBlobResultSchema, GitLabSearchResponseSchema, GitLabTreeItemSchema, GitLabUserSchema, GitLabUsersResponseSchema, GitLabWikiPageSchema, GroupIteration, ListCommitStatusesSchema, ListBranchesSchema, ListCommitsSchema, ListDraftNotesSchema, ListGroupIterationsSchema, ListGroupProjectsSchema, ListIssueDiscussionsSchema, ListIssueLinksSchema, ListIssuesSchema, ListTodosSchema, ListLabelsSchema, ListMergeRequestDiffsSchema, // Added
|
|
34
|
+
GitLabDraftNoteSchema, GitLabForkSchema, GitLabBranchSchema, GitLabGroupSchema, GitLabIssueLinkSchema, GitLabIssueSchema, GitLabIssueWithLinkDetailsSchema, GitLabMarkdownUploadSchema, GitLabMergeRequestPipelineSchema, GitLabMergeRequestSchema, GitLabMilestonesSchema, GitLabNamespaceExistsResponseSchema, GitLabNamespaceSchema, GitLabPipelineJobSchema, GitLabDeploymentSchema, GitLabEnvironmentSchema, GitLabPipelineSchema, GitLabPipelineTriggerJobSchema, GitLabProjectMemberSchema, GitLabProjectSchema, GitLabTodoSchema, GitLabReferenceSchema, GitLabRepositorySchema, GitLabSearchBlobResultSchema, GitLabSearchResponseSchema, GitLabTreeItemSchema, GitLabUserSchema, GitLabUsersResponseSchema, GitLabWikiPageSchema, GroupIteration, ListCommitStatusesSchema, ListBranchesSchema, ListCommitsSchema, ListDraftNotesSchema, ListGroupIterationsSchema, ListGroupProjectsSchema, ListIssueDiscussionsSchema, ListIssueLinksSchema, ListIssuesSchema, ListTodosSchema, ListLabelsSchema, ListMergeRequestDiffsSchema, // Added
|
|
35
35
|
GetMergeRequestFileDiffSchema, ListMergeRequestChangedFilesSchema, ListMergeRequestDiscussionsSchema, ListMergeRequestPipelinesSchema, ListMergeRequestsSchema, ListMergeRequestVersionsSchema, GetMergeRequestVersionSchema, GitLabMergeRequestVersionSchema, GitLabMergeRequestVersionDetailSchema, ListNamespacesSchema, ListPipelineJobsSchema, ListPipelinesSchema, ListDeploymentsSchema, ListEnvironmentsSchema, ListPipelineTriggerJobsSchema, ValidateCiLintSchema, ValidateProjectCiLintSchema, ListProjectMembersSchema, ListProjectMilestonesSchema, ListProjectsSchema, ListWikiPagesSchema, GetGroupWikiPageSchema, ListGroupWikiPagesSchema, UpdateGroupWikiPageSchema, MarkdownUploadSchema, DownloadAttachmentSchema, DownloadJobArtifactsSchema, GetJobArtifactFileSchema, GitLabArtifactEntrySchema, ListJobArtifactsSchema, MergeMergeRequestSchema, ApproveMergeRequestSchema, UnapproveMergeRequestSchema, GetMergeRequestApprovalStateSchema, GetMergeRequestConflictsSchema, GitLabMergeRequestApprovalsResponseSchema, GitLabMergeRequestApprovalStateSchema, MyIssuesSchema, MarkAllTodosDoneSchema, MarkTodoDoneSchema, PaginatedDiscussionsResponseSchema, PromoteProjectMilestoneSchema, PublishDraftNoteSchema, PlayPipelineJobSchema, PushFilesSchema, RetryPipelineJobSchema, RetryPipelineSchema, SearchCodeSchema, SearchGroupCodeSchema, SearchProjectCodeSchema, SearchRepositoriesSchema, UpdateDraftNoteSchema, UpdateIssueNoteSchema, UpdateIssueSchema, UpdateIssueDescriptionPatchSchema, UpdateLabelSchema, UpdateMergeRequestNoteSchema, UpdateMergeRequestDiscussionNoteSchema, UpdateMergeRequestSchema, UpdateWikiPageSchema, VerifyNamespaceSchema, GitLabEventSchema, ListEventsSchema, GetProjectEventsSchema, ExecuteGraphQLSchema, GitLabReleaseSchema, ListReleasesSchema, GetReleaseSchema, CreateReleaseSchema, UpdateReleaseSchema, DeleteReleaseSchema, CreateReleaseEvidenceSchema, DownloadReleaseAssetSchema, ListTagsSchema, GetTagSchema, CreateTagSchema, DeleteTagSchema, GetTagSignatureSchema, GitLabTagSchema, GitLabTagSignatureSchema, GetMergeRequestNotesSchema, GetMergeRequestNoteSchema, DeleteMergeRequestDiscussionNoteSchema, ResolveMergeRequestThreadSchema, GetWorkItemSchema, ListWorkItemsSchema, CreateWorkItemSchema, UpdateWorkItemSchema, ConvertWorkItemTypeSchema, ListWorkItemStatusesSchema, ListWorkItemNotesSchema, CreateWorkItemNoteSchema, CreateWorkItemEmojiReactionSchema, CreateWorkItemNoteEmojiReactionSchema, ListWorkItemEmojiReactionsSchema, ListWorkItemNoteEmojiReactionsSchema, DeleteWorkItemEmojiReactionSchema, DeleteWorkItemNoteEmojiReactionSchema, MoveWorkItemSchema, ListCustomFieldDefinitionsSchema, GetTimelineEventsSchema, CreateTimelineEventSchema, ListWebhooksSchema, ListWebhookEventsSchema, GetWebhookEventSchema, HealthCheckSchema, } from "./schemas.js";
|
|
36
36
|
import { randomUUID } from "node:crypto";
|
|
37
37
|
import { pino } from "pino";
|
|
@@ -937,6 +937,14 @@ function getEffectiveProjectId(projectId) {
|
|
|
937
937
|
}
|
|
938
938
|
throw new Error("No project ID provided and GITLAB_PROJECT_ID is not set");
|
|
939
939
|
}
|
|
940
|
+
function rejectIfProjectScopedDeployment(toolName) {
|
|
941
|
+
if (GITLAB_PROJECT_ID) {
|
|
942
|
+
throw new Error(`${toolName} is not allowed when GITLAB_PROJECT_ID is set (server is locked to a single project)`);
|
|
943
|
+
}
|
|
944
|
+
if (GITLAB_ALLOWED_PROJECT_IDS.length > 0) {
|
|
945
|
+
throw new Error(`${toolName} is not allowed when GITLAB_ALLOWED_PROJECT_IDS is set (server access is restricted to configured projects)`);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
940
948
|
/**
|
|
941
949
|
* Create a fork of a GitLab project
|
|
942
950
|
* 프로젝트 포크 생성 (Create a project fork)
|
|
@@ -5416,6 +5424,33 @@ async function getCommitDiff(projectId, sha, full_diff) {
|
|
|
5416
5424
|
}
|
|
5417
5425
|
return allDiffs;
|
|
5418
5426
|
}
|
|
5427
|
+
/**
|
|
5428
|
+
* Get blame for a file at a specific ref.
|
|
5429
|
+
*
|
|
5430
|
+
* Wraps GitLab REST endpoint
|
|
5431
|
+
* GET /projects/:id/repository/files/:file_path/blame?ref=
|
|
5432
|
+
* Returns an array of entries; each entry has `lines` (the source lines covered)
|
|
5433
|
+
* and `commit` (the commit that last changed those lines: id, author, message, ...).
|
|
5434
|
+
*
|
|
5435
|
+
* @param {string} projectId - Project ID or URL-encoded path
|
|
5436
|
+
* @param {Omit<GetFileBlameOptions,"project_id">} options - file_path, ref, optional range_start/range_end
|
|
5437
|
+
* @returns {Promise<GitLabBlameEntry[]>} Blame entries in source order.
|
|
5438
|
+
*/
|
|
5439
|
+
async function getFileBlame(projectId, options) {
|
|
5440
|
+
projectId = decodeURIComponent(projectId);
|
|
5441
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/repository/files/${encodeURIComponent(options.file_path)}/blame`);
|
|
5442
|
+
url.searchParams.append("ref", options.ref);
|
|
5443
|
+
if (options.range_start !== undefined && options.range_end !== undefined) {
|
|
5444
|
+
url.searchParams.append("range[start]", options.range_start.toString());
|
|
5445
|
+
url.searchParams.append("range[end]", options.range_end.toString());
|
|
5446
|
+
}
|
|
5447
|
+
const response = await fetch(url.toString(), {
|
|
5448
|
+
...getFetchConfig(),
|
|
5449
|
+
});
|
|
5450
|
+
await handleGitLabError(response);
|
|
5451
|
+
const data = await response.json();
|
|
5452
|
+
return z.array(GitLabBlameEntrySchema).parse(data);
|
|
5453
|
+
}
|
|
5419
5454
|
/**
|
|
5420
5455
|
* List statuses for a commit.
|
|
5421
5456
|
*
|
|
@@ -5959,6 +5994,10 @@ async function handleToolCall(params) {
|
|
|
5959
5994
|
delete args.work_item_iid;
|
|
5960
5995
|
}
|
|
5961
5996
|
}
|
|
5997
|
+
// Centralized read-only guard: reject write tools even if client bypasses list_tools filtering
|
|
5998
|
+
if (GITLAB_READ_ONLY_MODE && !readOnlyTools.has(params.name)) {
|
|
5999
|
+
throw new Error(`${params.name} is not allowed in read-only mode`);
|
|
6000
|
+
}
|
|
5962
6001
|
logger.info({ tool: params.name, event: "tool_call_start" }, `tool_call_start: ${params.name}`);
|
|
5963
6002
|
switch (params.name) {
|
|
5964
6003
|
case "execute_graphql": {
|
|
@@ -6009,9 +6048,7 @@ async function handleToolCall(params) {
|
|
|
6009
6048
|
}
|
|
6010
6049
|
}
|
|
6011
6050
|
case "fork_repository": {
|
|
6012
|
-
|
|
6013
|
-
throw new Error("Direct project ID is set. So fork_repository is not allowed");
|
|
6014
|
-
}
|
|
6051
|
+
rejectIfProjectScopedDeployment("fork_repository");
|
|
6015
6052
|
const forkArgs = ForkRepositorySchema.parse(params.arguments);
|
|
6016
6053
|
try {
|
|
6017
6054
|
const forkedProject = await forkProject(forkArgs.project_id, forkArgs.namespace);
|
|
@@ -6110,15 +6147,40 @@ async function handleToolCall(params) {
|
|
|
6110
6147
|
};
|
|
6111
6148
|
}
|
|
6112
6149
|
case "create_repository": {
|
|
6113
|
-
|
|
6114
|
-
throw new Error("Direct project ID is set. So fork_repository is not allowed");
|
|
6115
|
-
}
|
|
6150
|
+
rejectIfProjectScopedDeployment("create_repository");
|
|
6116
6151
|
const args = CreateRepositorySchema.parse(params.arguments);
|
|
6117
6152
|
const repository = await createRepository(args);
|
|
6118
6153
|
return {
|
|
6119
6154
|
content: [{ type: "text", text: JSON.stringify(repository, null, 2) }],
|
|
6120
6155
|
};
|
|
6121
6156
|
}
|
|
6157
|
+
case "create_group": {
|
|
6158
|
+
rejectIfProjectScopedDeployment("create_group");
|
|
6159
|
+
const args = CreateGroupSchema.parse(params.arguments);
|
|
6160
|
+
const url = new URL(`${getEffectiveApiUrl()}/groups`);
|
|
6161
|
+
const body = {
|
|
6162
|
+
name: args.name,
|
|
6163
|
+
path: args.path,
|
|
6164
|
+
};
|
|
6165
|
+
if (args.description)
|
|
6166
|
+
body.description = args.description;
|
|
6167
|
+
if (args.visibility)
|
|
6168
|
+
body.visibility = args.visibility;
|
|
6169
|
+
if (args.parent_id)
|
|
6170
|
+
body.parent_id = args.parent_id;
|
|
6171
|
+
const response = await fetch(url.toString(), {
|
|
6172
|
+
...getFetchConfig(),
|
|
6173
|
+
method: "POST",
|
|
6174
|
+
headers: { ...getFetchConfig().headers, "Content-Type": "application/json" },
|
|
6175
|
+
body: JSON.stringify(body),
|
|
6176
|
+
});
|
|
6177
|
+
await handleGitLabError(response);
|
|
6178
|
+
const data = await response.json();
|
|
6179
|
+
const group = GitLabGroupSchema.parse(data);
|
|
6180
|
+
return {
|
|
6181
|
+
content: [{ type: "text", text: JSON.stringify(group, null, 2) }],
|
|
6182
|
+
};
|
|
6183
|
+
}
|
|
6122
6184
|
case "get_file_contents": {
|
|
6123
6185
|
const args = GetFileContentsSchema.parse(params.arguments);
|
|
6124
6186
|
const contents = await getFileContents(args.project_id, args.file_path, args.ref);
|
|
@@ -7466,6 +7528,14 @@ async function handleToolCall(params) {
|
|
|
7466
7528
|
content: [{ type: "text", text: JSON.stringify(diff, null, 2) }],
|
|
7467
7529
|
};
|
|
7468
7530
|
}
|
|
7531
|
+
case "get_file_blame": {
|
|
7532
|
+
const args = GetFileBlameSchema.parse(params.arguments);
|
|
7533
|
+
const { project_id, ...options } = args;
|
|
7534
|
+
const blame = await getFileBlame(project_id, options);
|
|
7535
|
+
return {
|
|
7536
|
+
content: [{ type: "text", text: JSON.stringify(blame, null, 2) }],
|
|
7537
|
+
};
|
|
7538
|
+
}
|
|
7469
7539
|
case "list_commit_statuses": {
|
|
7470
7540
|
const args = ListCommitStatusesSchema.parse(params.arguments);
|
|
7471
7541
|
const { project_id, sha, ...options } = args;
|
package/build/schemas.js
CHANGED
|
@@ -595,6 +595,39 @@ export const GitLabCurrentUserSchema = z.object({
|
|
|
595
595
|
extern_uid: z.string(),
|
|
596
596
|
})).optional(),
|
|
597
597
|
}).passthrough();
|
|
598
|
+
// Group related schemas
|
|
599
|
+
export const CreateGroupSchema = z.object({
|
|
600
|
+
name: z.string().describe("The name of the group"),
|
|
601
|
+
path: z.string().describe("The path of the group"),
|
|
602
|
+
description: z.string().optional().describe("The group's description"),
|
|
603
|
+
visibility: z.enum(["private", "internal", "public"]).optional().describe("The group's visibility level"),
|
|
604
|
+
parent_id: z.coerce.number().optional().describe("The parent group ID for creating a subgroup"),
|
|
605
|
+
});
|
|
606
|
+
export const GitLabGroupSchema = z.object({
|
|
607
|
+
id: z.coerce.string(),
|
|
608
|
+
name: z.string(),
|
|
609
|
+
path: z.string(),
|
|
610
|
+
description: z.string().nullable(),
|
|
611
|
+
visibility: z.string().optional(),
|
|
612
|
+
share_with_group_lock: z.boolean().optional(),
|
|
613
|
+
require_two_factor_authentication: z.boolean().optional(),
|
|
614
|
+
two_factor_grace_period: z.number().optional(),
|
|
615
|
+
project_creation_level: z.string().optional(),
|
|
616
|
+
auto_devops_enabled: z.boolean().nullable().optional(),
|
|
617
|
+
subgroup_creation_level: z.string().optional(),
|
|
618
|
+
emails_disabled: z.boolean().nullable().optional(),
|
|
619
|
+
mentions_disabled: z.boolean().nullable().optional(),
|
|
620
|
+
lfs_enabled: z.boolean().nullable().optional(),
|
|
621
|
+
avatar_url: z.string().nullable().optional(),
|
|
622
|
+
web_url: z.string(),
|
|
623
|
+
request_access_enabled: z.boolean().nullable().optional(),
|
|
624
|
+
full_name: z.string(),
|
|
625
|
+
full_path: z.string(),
|
|
626
|
+
file_template_project_id: z.number().nullable().optional(),
|
|
627
|
+
parent_id: z.coerce.string().nullable().optional(),
|
|
628
|
+
created_at: z.string().optional(),
|
|
629
|
+
statistics: z.any().optional(),
|
|
630
|
+
});
|
|
598
631
|
// Namespace related schemas
|
|
599
632
|
// Base schema for project-related operations
|
|
600
633
|
const ProjectParamsSchema = z.object({
|
|
@@ -2404,6 +2437,50 @@ export const GetCommitDiffSchema = z.object({
|
|
|
2404
2437
|
.optional()
|
|
2405
2438
|
.describe("Whether to return the full diff or only first page (default: false)"),
|
|
2406
2439
|
});
|
|
2440
|
+
export const GetFileBlameSchema = z
|
|
2441
|
+
.object({
|
|
2442
|
+
project_id: z.coerce.string().describe("Project ID or complete URL-encoded path to project"),
|
|
2443
|
+
file_path: z.string().describe("The full path of the file to blame, relative to repo root"),
|
|
2444
|
+
ref: z
|
|
2445
|
+
.string()
|
|
2446
|
+
.describe("The name of branch, tag or commit (required by GitLab blame API)"),
|
|
2447
|
+
range_start: z
|
|
2448
|
+
.coerce.number()
|
|
2449
|
+
.int()
|
|
2450
|
+
.optional()
|
|
2451
|
+
.describe("First line of the blame range (inclusive, 1-based). Both range[start] and range[end] must be set together."),
|
|
2452
|
+
range_end: z
|
|
2453
|
+
.coerce.number()
|
|
2454
|
+
.int()
|
|
2455
|
+
.optional()
|
|
2456
|
+
.describe("Last line of the blame range (inclusive, 1-based). Both range[start] and range[end] must be set together."),
|
|
2457
|
+
})
|
|
2458
|
+
.refine((v) => (v.range_start === undefined) === (v.range_end === undefined), {
|
|
2459
|
+
message: "range_start and range_end must be provided together (both or neither). Passing only one silently returned full-file blame on GitLab side.",
|
|
2460
|
+
path: ["range_end"],
|
|
2461
|
+
})
|
|
2462
|
+
.refine((v) => v.range_start === undefined ||
|
|
2463
|
+
v.range_end === undefined ||
|
|
2464
|
+
v.range_start <= v.range_end, {
|
|
2465
|
+
message: "range_start must be less than or equal to range_end.",
|
|
2466
|
+
path: ["range_start"],
|
|
2467
|
+
});
|
|
2468
|
+
export const GitLabBlameEntrySchema = z.object({
|
|
2469
|
+
lines: z.array(z.string()).describe("Source lines covered by this blame range"),
|
|
2470
|
+
commit: z
|
|
2471
|
+
.object({
|
|
2472
|
+
id: z.string(),
|
|
2473
|
+
parent_ids: z.array(z.string()).optional(),
|
|
2474
|
+
message: z.string().optional(),
|
|
2475
|
+
authored_date: z.string().optional(),
|
|
2476
|
+
author_name: z.string().optional(),
|
|
2477
|
+
author_email: z.string().optional(),
|
|
2478
|
+
committed_date: z.string().optional(),
|
|
2479
|
+
committer_name: z.string().optional(),
|
|
2480
|
+
committer_email: z.string().optional(),
|
|
2481
|
+
})
|
|
2482
|
+
.passthrough(),
|
|
2483
|
+
});
|
|
2407
2484
|
export const ListCommitStatusesSchema = z
|
|
2408
2485
|
.object({
|
|
2409
2486
|
project_id: z.coerce.string().describe("Project ID or complete URL-encoded path to project"),
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { describe, test, before, after } from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import { spawn } from "child_process";
|
|
4
|
+
import { MockGitLabServer, findMockServerPort } from "./utils/mock-gitlab-server.js";
|
|
5
|
+
const MOCK_TOKEN = "glpat-mock-token-12345";
|
|
6
|
+
const TEST_PROJECT_ID = "123";
|
|
7
|
+
const MOCK_BLAME = [
|
|
8
|
+
{
|
|
9
|
+
lines: ["line one", ""],
|
|
10
|
+
commit: {
|
|
11
|
+
id: "1111111111111111111111111111111111111111",
|
|
12
|
+
message: "feat: initial commit",
|
|
13
|
+
authored_date: "2024-01-01T00:00:00.000Z",
|
|
14
|
+
author_name: "Alice",
|
|
15
|
+
author_email: "alice@example.com",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
lines: ["line three"],
|
|
20
|
+
commit: {
|
|
21
|
+
id: "2222222222222222222222222222222222222222",
|
|
22
|
+
message: "feat: add second change",
|
|
23
|
+
authored_date: "2024-02-02T00:00:00.000Z",
|
|
24
|
+
author_name: "Bob",
|
|
25
|
+
author_email: "bob@example.com",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
async function callGetFileBlame(args, env) {
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
const proc = spawn("node", ["build/index.js"], {
|
|
32
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
33
|
+
env: {
|
|
34
|
+
...process.env,
|
|
35
|
+
...env,
|
|
36
|
+
GITLAB_READ_ONLY_MODE: "true",
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
let output = "";
|
|
40
|
+
let errorOutput = "";
|
|
41
|
+
proc.stdout?.on("data", (d) => (output += d));
|
|
42
|
+
proc.stderr?.on("data", (d) => (errorOutput += d));
|
|
43
|
+
proc.on("close", (code) => {
|
|
44
|
+
if (code !== 0)
|
|
45
|
+
return reject(new Error(`Process exited with code ${code}: ${errorOutput}`));
|
|
46
|
+
const line = output.split("\n").find((l) => l.startsWith("{"));
|
|
47
|
+
if (!line)
|
|
48
|
+
return reject(new Error("No JSON output found"));
|
|
49
|
+
try {
|
|
50
|
+
const response = JSON.parse(line);
|
|
51
|
+
if (response.error)
|
|
52
|
+
return reject(response.error);
|
|
53
|
+
const content = response.result?.content?.[0]?.text;
|
|
54
|
+
if (content)
|
|
55
|
+
return resolve(JSON.parse(content));
|
|
56
|
+
resolve(response.result);
|
|
57
|
+
}
|
|
58
|
+
catch (e) {
|
|
59
|
+
reject(e);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
proc.stdin?.end(JSON.stringify({
|
|
63
|
+
jsonrpc: "2.0",
|
|
64
|
+
id: 1,
|
|
65
|
+
method: "tools/call",
|
|
66
|
+
params: { name: "get_file_blame", arguments: args },
|
|
67
|
+
}) + "\n");
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
describe("get_file_blame", () => {
|
|
71
|
+
let mockGitLab;
|
|
72
|
+
let mockGitLabUrl;
|
|
73
|
+
let lastQuery = {};
|
|
74
|
+
before(async () => {
|
|
75
|
+
const mockPort = await findMockServerPort(9000);
|
|
76
|
+
mockGitLab = new MockGitLabServer({
|
|
77
|
+
port: mockPort,
|
|
78
|
+
validTokens: [MOCK_TOKEN],
|
|
79
|
+
});
|
|
80
|
+
mockGitLab.addMockHandler("get", `/projects/${TEST_PROJECT_ID}/repository/files/src%2Fexample.txt/blame`, (req, res) => {
|
|
81
|
+
lastQuery = req.query;
|
|
82
|
+
res.json(MOCK_BLAME);
|
|
83
|
+
});
|
|
84
|
+
await mockGitLab.start();
|
|
85
|
+
mockGitLabUrl = mockGitLab.getUrl();
|
|
86
|
+
});
|
|
87
|
+
after(async () => {
|
|
88
|
+
await mockGitLab.stop();
|
|
89
|
+
});
|
|
90
|
+
test("returns blame entries for a file at ref", async () => {
|
|
91
|
+
const blame = await callGetFileBlame({
|
|
92
|
+
project_id: TEST_PROJECT_ID,
|
|
93
|
+
file_path: "src/example.txt",
|
|
94
|
+
ref: "main",
|
|
95
|
+
}, {
|
|
96
|
+
GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
|
|
97
|
+
GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN,
|
|
98
|
+
});
|
|
99
|
+
assert.ok(Array.isArray(blame), "Response should be an array");
|
|
100
|
+
assert.strictEqual(blame.length, 2, "Two blame entries expected");
|
|
101
|
+
assert.strictEqual(blame[1].commit.id, "2222222222222222222222222222222222222222", "second entry commit id matches");
|
|
102
|
+
assert.deepStrictEqual(blame[1].lines, ["line three"]);
|
|
103
|
+
assert.strictEqual(lastQuery.ref, "main", "ref propagated to GitLab API");
|
|
104
|
+
assert.ok(!("range[start]" in lastQuery) && !("range[end]" in lastQuery), "no range params when omitted");
|
|
105
|
+
});
|
|
106
|
+
test("passes range[start]/range[end] when both set", async () => {
|
|
107
|
+
await callGetFileBlame({
|
|
108
|
+
project_id: TEST_PROJECT_ID,
|
|
109
|
+
file_path: "src/example.txt",
|
|
110
|
+
ref: "main",
|
|
111
|
+
range_start: 10,
|
|
112
|
+
range_end: 20,
|
|
113
|
+
}, {
|
|
114
|
+
GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
|
|
115
|
+
GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN,
|
|
116
|
+
});
|
|
117
|
+
assert.strictEqual(lastQuery["range[start]"], "10");
|
|
118
|
+
assert.strictEqual(lastQuery["range[end]"], "20");
|
|
119
|
+
});
|
|
120
|
+
test("rejects partial range (range_start only) at schema layer", async () => {
|
|
121
|
+
await assert.rejects(() => callGetFileBlame({
|
|
122
|
+
project_id: TEST_PROJECT_ID,
|
|
123
|
+
file_path: "src/example.txt",
|
|
124
|
+
ref: "main",
|
|
125
|
+
range_start: 10,
|
|
126
|
+
}, {
|
|
127
|
+
GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
|
|
128
|
+
GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN,
|
|
129
|
+
}), (e) => typeof e?.message === "string" &&
|
|
130
|
+
e.message.includes("range_start and range_end must be provided together"));
|
|
131
|
+
});
|
|
132
|
+
test("rejects inverted range (start > end) at schema layer", async () => {
|
|
133
|
+
await assert.rejects(() => callGetFileBlame({
|
|
134
|
+
project_id: TEST_PROJECT_ID,
|
|
135
|
+
file_path: "src/example.txt",
|
|
136
|
+
ref: "main",
|
|
137
|
+
range_start: 20,
|
|
138
|
+
range_end: 10,
|
|
139
|
+
}, {
|
|
140
|
+
GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
|
|
141
|
+
GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN,
|
|
142
|
+
}), (e) => typeof e?.message === "string" &&
|
|
143
|
+
e.message.includes("range_start must be less than or equal to range_end"));
|
|
144
|
+
});
|
|
145
|
+
});
|
|
@@ -6,7 +6,7 @@ import { describe, test, before, after } from 'node:test';
|
|
|
6
6
|
import assert from 'node:assert';
|
|
7
7
|
import { launchServer, findAvailablePort, cleanupServers, TransportMode, HOST } from './utils/server-launcher.js';
|
|
8
8
|
import { MockGitLabServer, findMockServerPort } from './utils/mock-gitlab-server.js';
|
|
9
|
-
import {
|
|
9
|
+
import { CustomHeaderClient } from './clients/custom-header-client.js';
|
|
10
10
|
// Use the same token that will be passed via GITLAB_TOKEN_TEST environment variable
|
|
11
11
|
const MOCK_TOKEN = process.env.GITLAB_TOKEN_TEST || 'glpat-mock-token-12345';
|
|
12
12
|
const DEFAULT_PROJECT_ID = '123';
|
|
@@ -43,6 +43,7 @@ describe('getEffectiveProjectId', { concurrency: 1 }, () => {
|
|
|
43
43
|
timeout: 5000,
|
|
44
44
|
env: {
|
|
45
45
|
STREAMABLE_HTTP: 'true',
|
|
46
|
+
REMOTE_AUTHORIZATION: 'true',
|
|
46
47
|
GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
|
|
47
48
|
GITLAB_PROJECT_ID: DEFAULT_PROJECT_ID,
|
|
48
49
|
GITLAB_READ_ONLY_MODE: 'true',
|
|
@@ -50,7 +51,9 @@ describe('getEffectiveProjectId', { concurrency: 1 }, () => {
|
|
|
50
51
|
});
|
|
51
52
|
servers.push(server);
|
|
52
53
|
mcpUrl = `http://${HOST}:${mcpPort}/mcp`;
|
|
53
|
-
client = new
|
|
54
|
+
client = new CustomHeaderClient({
|
|
55
|
+
authorization: `Bearer ${MOCK_TOKEN}`,
|
|
56
|
+
});
|
|
54
57
|
await client.connect(mcpUrl);
|
|
55
58
|
console.log(`Mock GitLab: ${mockGitLabUrl}`);
|
|
56
59
|
console.log(`MCP Server: ${mcpUrl}`);
|
|
@@ -113,7 +116,7 @@ describe('getEffectiveProjectId', { concurrency: 1 }, () => {
|
|
|
113
116
|
port: mcpPort,
|
|
114
117
|
timeout: 5000,
|
|
115
118
|
env: {
|
|
116
|
-
|
|
119
|
+
REMOTE_AUTHORIZATION: 'true',
|
|
117
120
|
GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
|
|
118
121
|
GITLAB_PROJECT_ID: DEFAULT_PROJECT_ID,
|
|
119
122
|
GITLAB_ALLOWED_PROJECT_IDS: DEFAULT_PROJECT_ID,
|
|
@@ -122,7 +125,9 @@ describe('getEffectiveProjectId', { concurrency: 1 }, () => {
|
|
|
122
125
|
});
|
|
123
126
|
servers.push(server);
|
|
124
127
|
mcpUrl = `http://${HOST}:${mcpPort}/mcp`;
|
|
125
|
-
client = new
|
|
128
|
+
client = new CustomHeaderClient({
|
|
129
|
+
authorization: `Bearer ${MOCK_TOKEN}`,
|
|
130
|
+
});
|
|
126
131
|
await client.connect(mcpUrl);
|
|
127
132
|
console.log(`Mock GitLab: ${mockGitLabUrl}`);
|
|
128
133
|
console.log(`MCP Server: ${mcpUrl}`);
|
|
@@ -183,7 +188,7 @@ describe('getEffectiveProjectId', { concurrency: 1 }, () => {
|
|
|
183
188
|
port: mcpPort,
|
|
184
189
|
timeout: 5000,
|
|
185
190
|
env: {
|
|
186
|
-
|
|
191
|
+
REMOTE_AUTHORIZATION: 'true',
|
|
187
192
|
GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
|
|
188
193
|
GITLAB_ALLOWED_PROJECT_IDS: `${DEFAULT_PROJECT_ID},${OTHER_PROJECT_ID}`,
|
|
189
194
|
GITLAB_READ_ONLY_MODE: 'true',
|
|
@@ -191,7 +196,9 @@ describe('getEffectiveProjectId', { concurrency: 1 }, () => {
|
|
|
191
196
|
});
|
|
192
197
|
servers.push(server);
|
|
193
198
|
mcpUrl = `http://${HOST}:${mcpPort}/mcp`;
|
|
194
|
-
client = new
|
|
199
|
+
client = new CustomHeaderClient({
|
|
200
|
+
authorization: `Bearer ${MOCK_TOKEN}`,
|
|
201
|
+
});
|
|
195
202
|
await client.connect(mcpUrl);
|
|
196
203
|
console.log(`Mock GitLab: ${mockGitLabUrl}`);
|
|
197
204
|
console.log(`MCP Server: ${mcpUrl}`);
|
|
@@ -242,4 +249,221 @@ describe('getEffectiveProjectId', { concurrency: 1 }, () => {
|
|
|
242
249
|
console.log(` ✓ Allowed access to second project ${OTHER_PROJECT_ID}`);
|
|
243
250
|
});
|
|
244
251
|
});
|
|
252
|
+
describe('GITLAB_PROJECT_ID guards repository and group mutators', () => {
|
|
253
|
+
let mcpUrl;
|
|
254
|
+
let mockGitLab;
|
|
255
|
+
let servers = [];
|
|
256
|
+
let client;
|
|
257
|
+
before(async () => {
|
|
258
|
+
const mockPort = await findMockServerPort(9400);
|
|
259
|
+
mockGitLab = new MockGitLabServer({
|
|
260
|
+
port: mockPort,
|
|
261
|
+
validTokens: [MOCK_TOKEN]
|
|
262
|
+
});
|
|
263
|
+
await mockGitLab.start();
|
|
264
|
+
const mockGitLabUrl = mockGitLab.getUrl();
|
|
265
|
+
const mcpPort = await findAvailablePort(3400);
|
|
266
|
+
const server = await launchServer({
|
|
267
|
+
mode: TransportMode.STREAMABLE_HTTP,
|
|
268
|
+
port: mcpPort,
|
|
269
|
+
timeout: 5000,
|
|
270
|
+
env: {
|
|
271
|
+
REMOTE_AUTHORIZATION: 'true',
|
|
272
|
+
GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
|
|
273
|
+
GITLAB_PROJECT_ID: DEFAULT_PROJECT_ID,
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
servers.push(server);
|
|
277
|
+
mcpUrl = `http://${HOST}:${mcpPort}/mcp`;
|
|
278
|
+
client = new CustomHeaderClient({
|
|
279
|
+
authorization: `Bearer ${MOCK_TOKEN}`,
|
|
280
|
+
});
|
|
281
|
+
await client.connect(mcpUrl);
|
|
282
|
+
});
|
|
283
|
+
after(async () => {
|
|
284
|
+
if (client)
|
|
285
|
+
await client.disconnect();
|
|
286
|
+
cleanupServers(servers);
|
|
287
|
+
if (mockGitLab)
|
|
288
|
+
await mockGitLab.stop();
|
|
289
|
+
});
|
|
290
|
+
test('should reject create_repository when GITLAB_PROJECT_ID is set', async () => {
|
|
291
|
+
try {
|
|
292
|
+
await client.callTool('create_repository', { name: 'test-repo' });
|
|
293
|
+
assert.fail('Should have rejected create_repository');
|
|
294
|
+
}
|
|
295
|
+
catch (error) {
|
|
296
|
+
assert.ok(error instanceof Error);
|
|
297
|
+
assert.ok(error.message.includes('create_repository is not allowed'), 'Should mention create_repository');
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
test('should reject fork_repository when GITLAB_PROJECT_ID is set', async () => {
|
|
301
|
+
try {
|
|
302
|
+
await client.callTool('fork_repository', { project_id: '999' });
|
|
303
|
+
assert.fail('Should have rejected fork_repository');
|
|
304
|
+
}
|
|
305
|
+
catch (error) {
|
|
306
|
+
assert.ok(error instanceof Error);
|
|
307
|
+
assert.ok(error.message.includes('fork_repository is not allowed'), 'Should mention fork_repository');
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
test('should reject create_group when GITLAB_PROJECT_ID is set', async () => {
|
|
311
|
+
try {
|
|
312
|
+
await client.callTool('create_group', { name: 'test-group', path: 'test-group' });
|
|
313
|
+
assert.fail('Should have rejected create_group');
|
|
314
|
+
}
|
|
315
|
+
catch (error) {
|
|
316
|
+
assert.ok(error instanceof Error);
|
|
317
|
+
assert.ok(error.message.includes('create_group is not allowed'), 'Should mention create_group');
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
test('should allow get_project (non-mutator) when GITLAB_PROJECT_ID is set', async () => {
|
|
321
|
+
const result = await client.callTool('get_project', { project_id: '' });
|
|
322
|
+
assert.ok(result.content, 'Should have content');
|
|
323
|
+
const content = result.content[0];
|
|
324
|
+
assert.ok('text' in content, 'Content should have text');
|
|
325
|
+
const project = JSON.parse(content.text);
|
|
326
|
+
assert.strictEqual(project.id.toString(), DEFAULT_PROJECT_ID, 'Should use default project');
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
describe('GITLAB_ALLOWED_PROJECT_IDS guards repository and group mutators (allowlist-only, no GITLAB_PROJECT_ID)', () => {
|
|
330
|
+
let mcpUrl;
|
|
331
|
+
let mockGitLab;
|
|
332
|
+
let servers = [];
|
|
333
|
+
let client;
|
|
334
|
+
before(async () => {
|
|
335
|
+
const mockPort = await findMockServerPort(9600);
|
|
336
|
+
mockGitLab = new MockGitLabServer({
|
|
337
|
+
port: mockPort,
|
|
338
|
+
validTokens: [MOCK_TOKEN]
|
|
339
|
+
});
|
|
340
|
+
await mockGitLab.start();
|
|
341
|
+
const mockGitLabUrl = mockGitLab.getUrl();
|
|
342
|
+
const mcpPort = await findAvailablePort(3600);
|
|
343
|
+
const server = await launchServer({
|
|
344
|
+
mode: TransportMode.STREAMABLE_HTTP,
|
|
345
|
+
port: mcpPort,
|
|
346
|
+
timeout: 5000,
|
|
347
|
+
env: {
|
|
348
|
+
REMOTE_AUTHORIZATION: 'true',
|
|
349
|
+
GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
|
|
350
|
+
GITLAB_ALLOWED_PROJECT_IDS: DEFAULT_PROJECT_ID,
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
servers.push(server);
|
|
354
|
+
mcpUrl = `http://${HOST}:${mcpPort}/mcp`;
|
|
355
|
+
client = new CustomHeaderClient({
|
|
356
|
+
authorization: `Bearer ${MOCK_TOKEN}`,
|
|
357
|
+
});
|
|
358
|
+
await client.connect(mcpUrl);
|
|
359
|
+
});
|
|
360
|
+
after(async () => {
|
|
361
|
+
if (client)
|
|
362
|
+
await client.disconnect();
|
|
363
|
+
cleanupServers(servers);
|
|
364
|
+
if (mockGitLab)
|
|
365
|
+
await mockGitLab.stop();
|
|
366
|
+
});
|
|
367
|
+
test('should reject create_repository with GITLAB_ALLOWED_PROJECT_IDS', async () => {
|
|
368
|
+
try {
|
|
369
|
+
await client.callTool('create_repository', { name: 'test-repo' });
|
|
370
|
+
assert.fail('Should have rejected create_repository');
|
|
371
|
+
}
|
|
372
|
+
catch (error) {
|
|
373
|
+
assert.ok(error instanceof Error);
|
|
374
|
+
assert.ok(error.message.includes('create_repository is not allowed'), 'Should mention create_repository');
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
test('should reject fork_repository with GITLAB_ALLOWED_PROJECT_IDS', async () => {
|
|
378
|
+
try {
|
|
379
|
+
await client.callTool('fork_repository', { project_id: '999' });
|
|
380
|
+
assert.fail('Should have rejected fork_repository');
|
|
381
|
+
}
|
|
382
|
+
catch (error) {
|
|
383
|
+
assert.ok(error instanceof Error);
|
|
384
|
+
assert.ok(error.message.includes('fork_repository is not allowed'), 'Should mention fork_repository');
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
test('should reject create_group with GITLAB_ALLOWED_PROJECT_IDS', async () => {
|
|
388
|
+
try {
|
|
389
|
+
await client.callTool('create_group', { name: 'test-group', path: 'test-group' });
|
|
390
|
+
assert.fail('Should have rejected create_group');
|
|
391
|
+
}
|
|
392
|
+
catch (error) {
|
|
393
|
+
assert.ok(error instanceof Error);
|
|
394
|
+
assert.ok(error.message.includes('create_group is not allowed'), 'Should mention create_group');
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
test('should allow get_project (non-mutator) with GITLAB_ALLOWED_PROJECT_IDS', async () => {
|
|
398
|
+
const result = await client.callTool('get_project', { project_id: '' });
|
|
399
|
+
assert.ok(result.content, 'Should have content');
|
|
400
|
+
const content = result.content[0];
|
|
401
|
+
assert.ok('text' in content, 'Content should have text');
|
|
402
|
+
const project = JSON.parse(content.text);
|
|
403
|
+
assert.strictEqual(project.id.toString(), DEFAULT_PROJECT_ID, 'Should use default project');
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
describe('GITLAB_READ_ONLY_MODE enforces read-only for all write tools', () => {
|
|
407
|
+
let mcpUrl;
|
|
408
|
+
let mockGitLab;
|
|
409
|
+
let servers = [];
|
|
410
|
+
let client;
|
|
411
|
+
before(async () => {
|
|
412
|
+
const mockPort = await findMockServerPort(9500);
|
|
413
|
+
mockGitLab = new MockGitLabServer({
|
|
414
|
+
port: mockPort,
|
|
415
|
+
validTokens: [MOCK_TOKEN]
|
|
416
|
+
});
|
|
417
|
+
await mockGitLab.start();
|
|
418
|
+
const mockGitLabUrl = mockGitLab.getUrl();
|
|
419
|
+
const mcpPort = await findAvailablePort(3500);
|
|
420
|
+
const server = await launchServer({
|
|
421
|
+
mode: TransportMode.STREAMABLE_HTTP,
|
|
422
|
+
port: mcpPort,
|
|
423
|
+
timeout: 5000,
|
|
424
|
+
env: {
|
|
425
|
+
REMOTE_AUTHORIZATION: 'true',
|
|
426
|
+
GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
|
|
427
|
+
GITLAB_READ_ONLY_MODE: 'true',
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
servers.push(server);
|
|
431
|
+
mcpUrl = `http://${HOST}:${mcpPort}/mcp`;
|
|
432
|
+
client = new CustomHeaderClient({
|
|
433
|
+
authorization: `Bearer ${MOCK_TOKEN}`,
|
|
434
|
+
});
|
|
435
|
+
await client.connect(mcpUrl);
|
|
436
|
+
});
|
|
437
|
+
after(async () => {
|
|
438
|
+
if (client)
|
|
439
|
+
await client.disconnect();
|
|
440
|
+
cleanupServers(servers);
|
|
441
|
+
if (mockGitLab)
|
|
442
|
+
await mockGitLab.stop();
|
|
443
|
+
});
|
|
444
|
+
test('should reject create_group in read-only mode (no project ID)', async () => {
|
|
445
|
+
try {
|
|
446
|
+
await client.callTool('create_group', { name: 'test-group', path: 'test-group' });
|
|
447
|
+
assert.fail('Should have rejected create_group in read-only mode');
|
|
448
|
+
}
|
|
449
|
+
catch (error) {
|
|
450
|
+
assert.ok(error instanceof Error);
|
|
451
|
+
assert.ok(error.message.includes('create_group is not allowed'), 'Should mention create_group');
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
test('should reject create_repository in read-only mode', async () => {
|
|
455
|
+
try {
|
|
456
|
+
await client.callTool('create_repository', { name: 'test-repo' });
|
|
457
|
+
assert.fail('Should have rejected create_repository in read-only mode');
|
|
458
|
+
}
|
|
459
|
+
catch (error) {
|
|
460
|
+
assert.ok(error instanceof Error);
|
|
461
|
+
assert.ok(error.message.includes('create_repository is not allowed'), 'Should mention create_repository');
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
test('should allow get_project (read-only) in read-only mode', async () => {
|
|
465
|
+
const result = await client.callTool('get_project', { project_id: DEFAULT_PROJECT_ID });
|
|
466
|
+
assert.ok(result.content, 'Should have content');
|
|
467
|
+
});
|
|
468
|
+
});
|
|
245
469
|
}); // end wrapper describe
|
|
@@ -19,7 +19,7 @@ const TOOLSET_TOOL_COUNTS = {
|
|
|
19
19
|
merge_requests: 41,
|
|
20
20
|
issues: 24,
|
|
21
21
|
repositories: 7,
|
|
22
|
-
branches:
|
|
22
|
+
branches: 10,
|
|
23
23
|
projects: 9,
|
|
24
24
|
labels: 5,
|
|
25
25
|
ci: 2,
|
|
@@ -32,6 +32,7 @@ const TOOLSET_TOOL_COUNTS = {
|
|
|
32
32
|
search: 3,
|
|
33
33
|
workitems: 18,
|
|
34
34
|
webhooks: 3,
|
|
35
|
+
groups: 1,
|
|
35
36
|
};
|
|
36
37
|
const LEGACY_PIPELINE_TOOL_COUNT = TOOLSET_TOOL_COUNTS.pipelines + TOOLSET_TOOL_COUNTS.ci;
|
|
37
38
|
const DEFAULT_TOOLSETS = [
|
|
@@ -43,6 +44,7 @@ const DEFAULT_TOOLSETS = [
|
|
|
43
44
|
"labels",
|
|
44
45
|
"ci",
|
|
45
46
|
"users",
|
|
47
|
+
"groups",
|
|
46
48
|
];
|
|
47
49
|
const NON_DEFAULT_TOOLSETS = [
|
|
48
50
|
"pipelines",
|
|
@@ -75,6 +77,7 @@ const TOOLSET_SAMPLE_TOOLS = {
|
|
|
75
77
|
users: ["get_users", "upload_markdown", "download_attachment"],
|
|
76
78
|
search: ["search_code", "search_project_code", "search_group_code"],
|
|
77
79
|
webhooks: ["list_webhooks", "list_webhook_events", "get_webhook_event"],
|
|
80
|
+
groups: ["create_group"],
|
|
78
81
|
};
|
|
79
82
|
// --- Helpers ---
|
|
80
83
|
async function launchMcpServer(mockGitLabUrl, mcpPort, extraEnv = {}) {
|
package/build/tools/registry.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
2
2
|
import { toJSONSchema } from "../utils/schema.js";
|
|
3
3
|
import { USE_GITLAB_WIKI, USE_MILESTONE, USE_PIPELINE, } from "../config.js";
|
|
4
|
-
import { ApproveMergeRequestSchema, BulkPublishDraftNotesSchema, CancelPipelineJobSchema, CancelPipelineSchema, ConvertWorkItemTypeSchema, CreateBranchSchema, CreateDraftNoteSchema, CreateGroupWikiPageSchema, CreateIssueLinkSchema, CreateIssueNoteSchema, CreateIssueSchema, CreateIssueEmojiReactionSchema, CreateIssueNoteEmojiReactionSchema, ListIssueEmojiReactionsSchema, ListIssueNoteEmojiReactionsSchema, CreateLabelSchema, MarkAllTodosDoneSchema, ListTodosSchema, MarkTodoDoneSchema, CreateMergeRequestDiscussionNoteSchema, CreateMergeRequestEmojiReactionSchema, ListMergeRequestEmojiReactionsSchema, ListMergeRequestNoteEmojiReactionsSchema, CreateMergeRequestNoteSchema, CreateMergeRequestNoteEmojiReactionSchema, CreateMergeRequestSchema, CreateMergeRequestThreadSchema, CreateNoteSchema, CreateCommitStatusSchema, CreateOrUpdateFileSchema, CreatePipelineSchema, CreateProjectMilestoneSchema, CreateReleaseEvidenceSchema, CreateReleaseSchema, CreateRepositorySchema, CreateTagSchema, CreateTimelineEventSchema, CreateWikiPageSchema, CreateWorkItemNoteSchema, CreateWorkItemEmojiReactionSchema, CreateWorkItemNoteEmojiReactionSchema, ListWorkItemEmojiReactionsSchema, ListWorkItemNoteEmojiReactionsSchema, CreateWorkItemSchema, DeleteBranchSchema, DeleteDraftNoteSchema, DeleteGroupWikiPageSchema, DeleteIssueLinkSchema, DeleteIssueSchema, DeleteIssueEmojiReactionSchema, DeleteIssueNoteEmojiReactionSchema, DeleteLabelSchema, DeleteMergeRequestDiscussionNoteSchema, DeleteMergeRequestNoteSchema, DeleteMergeRequestEmojiReactionSchema, DeleteMergeRequestNoteEmojiReactionSchema, DeleteProjectMilestoneSchema, DeleteReleaseSchema, DeleteTagSchema, DeleteWikiPageSchema, DeleteWorkItemEmojiReactionSchema, DeleteWorkItemNoteEmojiReactionSchema, DownloadAttachmentSchema, DownloadJobArtifactsSchema, DownloadReleaseAssetSchema, EditProjectMilestoneSchema, ExecuteGraphQLSchema, ForkRepositorySchema, HealthCheckSchema, GetBranchSchema, GetBranchDiffsSchema, GetCommitDiffSchema, GetCommitSchema, GetDeploymentSchema, GetDraftNoteSchema, GetEnvironmentSchema, GetFileContentsSchema, GetGroupWikiPageSchema, GetIssueLinkSchema, GetIssueSchema, GetJobArtifactFileSchema, GetLabelSchema, GetMergeRequestApprovalStateSchema, GetMergeRequestConflictsSchema, GetMergeRequestDiffsSchema, GetMergeRequestFileDiffSchema, GetMergeRequestNoteSchema, GetMergeRequestNotesSchema, GetMergeRequestSchema, GetMergeRequestVersionSchema, GetMilestoneBurndownEventsSchema, GetMilestoneIssuesSchema, GetMilestoneMergeRequestsSchema, GetNamespaceSchema, GetPipelineJobOutputSchema, GetPipelineSchema, GetProjectEventsSchema, GetProjectMilestoneSchema, GetProjectSchema, GetReleaseSchema, GetRepositoryTreeSchema, GetTagSchema, GetTagSignatureSchema, GetTimelineEventsSchema, GetUsersSchema, GetUserSchema, WhoAmISchema, GetWebhookEventSchema, GetWikiPageSchema, GetWorkItemSchema, ListBranchesSchema, ListCommitsSchema, ListCommitStatusesSchema, ListCustomFieldDefinitionsSchema, ListDeploymentsSchema, ListDraftNotesSchema, ListEnvironmentsSchema, ListEventsSchema, ListGroupIterationsSchema, ListGroupProjectsSchema, ListGroupWikiPagesSchema, ListIssueDiscussionsSchema, ListIssueLinksSchema, ListIssuesSchema, ListJobArtifactsSchema, ListLabelsSchema, ListMergeRequestChangedFilesSchema, ListMergeRequestDiffsSchema, ListMergeRequestDiscussionsSchema, ListMergeRequestPipelinesSchema, ListMergeRequestVersionsSchema, ListMergeRequestsSchema, ListNamespacesSchema, ListPipelineJobsSchema, ListPipelineTriggerJobsSchema, ValidateCiLintSchema, ValidateProjectCiLintSchema, ListPipelinesSchema, ListProjectMembersSchema, ListProjectMilestonesSchema, ListProjectsSchema, ListReleasesSchema, ListTagsSchema, ListWebhookEventsSchema, ListWebhooksSchema, ListWikiPagesSchema, ListWorkItemNotesSchema, ListWorkItemStatusesSchema, ListWorkItemsSchema, MarkdownUploadSchema, MergeMergeRequestSchema, MoveWorkItemSchema, MyIssuesSchema, PlayPipelineJobSchema, PromoteProjectMilestoneSchema, PublishDraftNoteSchema, PushFilesSchema, ResolveMergeRequestThreadSchema, RetryPipelineJobSchema, RetryPipelineSchema, SearchCodeSchema, SearchGroupCodeSchema, SearchProjectCodeSchema, SearchRepositoriesSchema, UnapproveMergeRequestSchema, UpdateDraftNoteSchema, UpdateGroupWikiPageSchema, UpdateIssueNoteSchema, UpdateIssueSchema, UpdateIssueDescriptionPatchSchema, UpdateLabelSchema, UpdateMergeRequestDiscussionNoteSchema, UpdateMergeRequestNoteSchema, UpdateMergeRequestSchema, UpdateReleaseSchema, UpdateWikiPageSchema, UpdateWorkItemSchema, VerifyNamespaceSchema, } from "../schemas.js";
|
|
4
|
+
import { ApproveMergeRequestSchema, BulkPublishDraftNotesSchema, CancelPipelineJobSchema, CancelPipelineSchema, ConvertWorkItemTypeSchema, CreateBranchSchema, CreateDraftNoteSchema, CreateGroupSchema, CreateGroupWikiPageSchema, CreateIssueLinkSchema, CreateIssueNoteSchema, CreateIssueSchema, CreateIssueEmojiReactionSchema, CreateIssueNoteEmojiReactionSchema, ListIssueEmojiReactionsSchema, ListIssueNoteEmojiReactionsSchema, CreateLabelSchema, MarkAllTodosDoneSchema, ListTodosSchema, MarkTodoDoneSchema, CreateMergeRequestDiscussionNoteSchema, CreateMergeRequestEmojiReactionSchema, ListMergeRequestEmojiReactionsSchema, ListMergeRequestNoteEmojiReactionsSchema, CreateMergeRequestNoteSchema, CreateMergeRequestNoteEmojiReactionSchema, CreateMergeRequestSchema, CreateMergeRequestThreadSchema, CreateNoteSchema, CreateCommitStatusSchema, CreateOrUpdateFileSchema, CreatePipelineSchema, CreateProjectMilestoneSchema, CreateReleaseEvidenceSchema, CreateReleaseSchema, CreateRepositorySchema, CreateTagSchema, CreateTimelineEventSchema, CreateWikiPageSchema, CreateWorkItemNoteSchema, CreateWorkItemEmojiReactionSchema, CreateWorkItemNoteEmojiReactionSchema, ListWorkItemEmojiReactionsSchema, ListWorkItemNoteEmojiReactionsSchema, CreateWorkItemSchema, DeleteBranchSchema, DeleteDraftNoteSchema, DeleteGroupWikiPageSchema, DeleteIssueLinkSchema, DeleteIssueSchema, DeleteIssueEmojiReactionSchema, DeleteIssueNoteEmojiReactionSchema, DeleteLabelSchema, DeleteMergeRequestDiscussionNoteSchema, DeleteMergeRequestNoteSchema, DeleteMergeRequestEmojiReactionSchema, DeleteMergeRequestNoteEmojiReactionSchema, DeleteProjectMilestoneSchema, DeleteReleaseSchema, DeleteTagSchema, DeleteWikiPageSchema, DeleteWorkItemEmojiReactionSchema, DeleteWorkItemNoteEmojiReactionSchema, DownloadAttachmentSchema, DownloadJobArtifactsSchema, DownloadReleaseAssetSchema, EditProjectMilestoneSchema, ExecuteGraphQLSchema, ForkRepositorySchema, HealthCheckSchema, GetBranchSchema, GetBranchDiffsSchema, GetCommitDiffSchema, GetCommitSchema, GetFileBlameSchema, GetDeploymentSchema, GetDraftNoteSchema, GetEnvironmentSchema, GetFileContentsSchema, GetGroupWikiPageSchema, GetIssueLinkSchema, GetIssueSchema, GetJobArtifactFileSchema, GetLabelSchema, GetMergeRequestApprovalStateSchema, GetMergeRequestConflictsSchema, GetMergeRequestDiffsSchema, GetMergeRequestFileDiffSchema, GetMergeRequestNoteSchema, GetMergeRequestNotesSchema, GetMergeRequestSchema, GetMergeRequestVersionSchema, GetMilestoneBurndownEventsSchema, GetMilestoneIssuesSchema, GetMilestoneMergeRequestsSchema, GetNamespaceSchema, GetPipelineJobOutputSchema, GetPipelineSchema, GetProjectEventsSchema, GetProjectMilestoneSchema, GetProjectSchema, GetReleaseSchema, GetRepositoryTreeSchema, GetTagSchema, GetTagSignatureSchema, GetTimelineEventsSchema, GetUsersSchema, GetUserSchema, WhoAmISchema, GetWebhookEventSchema, GetWikiPageSchema, GetWorkItemSchema, ListBranchesSchema, ListCommitsSchema, ListCommitStatusesSchema, ListCustomFieldDefinitionsSchema, ListDeploymentsSchema, ListDraftNotesSchema, ListEnvironmentsSchema, ListEventsSchema, ListGroupIterationsSchema, ListGroupProjectsSchema, ListGroupWikiPagesSchema, ListIssueDiscussionsSchema, ListIssueLinksSchema, ListIssuesSchema, ListJobArtifactsSchema, ListLabelsSchema, ListMergeRequestChangedFilesSchema, ListMergeRequestDiffsSchema, ListMergeRequestDiscussionsSchema, ListMergeRequestPipelinesSchema, ListMergeRequestVersionsSchema, ListMergeRequestsSchema, ListNamespacesSchema, ListPipelineJobsSchema, ListPipelineTriggerJobsSchema, ValidateCiLintSchema, ValidateProjectCiLintSchema, ListPipelinesSchema, ListProjectMembersSchema, ListProjectMilestonesSchema, ListProjectsSchema, ListReleasesSchema, ListTagsSchema, ListWebhookEventsSchema, ListWebhooksSchema, ListWikiPagesSchema, ListWorkItemNotesSchema, ListWorkItemStatusesSchema, ListWorkItemsSchema, MarkdownUploadSchema, MergeMergeRequestSchema, MoveWorkItemSchema, MyIssuesSchema, PlayPipelineJobSchema, PromoteProjectMilestoneSchema, PublishDraftNoteSchema, PushFilesSchema, ResolveMergeRequestThreadSchema, RetryPipelineJobSchema, RetryPipelineSchema, SearchCodeSchema, SearchGroupCodeSchema, SearchProjectCodeSchema, SearchRepositoriesSchema, UnapproveMergeRequestSchema, UpdateDraftNoteSchema, UpdateGroupWikiPageSchema, UpdateIssueNoteSchema, UpdateIssueSchema, UpdateIssueDescriptionPatchSchema, UpdateLabelSchema, UpdateMergeRequestDiscussionNoteSchema, UpdateMergeRequestNoteSchema, UpdateMergeRequestSchema, UpdateReleaseSchema, UpdateWikiPageSchema, UpdateWorkItemSchema, VerifyNamespaceSchema, } from "../schemas.js";
|
|
5
5
|
// Define all available tools
|
|
6
6
|
export const allTools = [
|
|
7
7
|
{
|
|
@@ -54,6 +54,11 @@ export const allTools = [
|
|
|
54
54
|
description: "Create a new GitLab project",
|
|
55
55
|
inputSchema: toJSONSchema(CreateRepositorySchema),
|
|
56
56
|
},
|
|
57
|
+
{
|
|
58
|
+
name: "create_group",
|
|
59
|
+
description: "Create new group or subgroup",
|
|
60
|
+
inputSchema: toJSONSchema(CreateGroupSchema),
|
|
61
|
+
},
|
|
57
62
|
{
|
|
58
63
|
name: "get_file_contents",
|
|
59
64
|
description: "Get contents of a file or directory from a GitLab project",
|
|
@@ -385,12 +390,12 @@ export const allTools = [
|
|
|
385
390
|
},
|
|
386
391
|
{
|
|
387
392
|
name: "list_namespaces",
|
|
388
|
-
description: "List all namespaces available to the current user",
|
|
393
|
+
description: "List all namespaces (users and groups) available to the current user. Filter by kind='group' for groups only.",
|
|
389
394
|
inputSchema: toJSONSchema(ListNamespacesSchema),
|
|
390
395
|
},
|
|
391
396
|
{
|
|
392
397
|
name: "get_namespace",
|
|
393
|
-
description: "Get details of a namespace by ID or path",
|
|
398
|
+
description: "Get details of a namespace (user or group) by ID or path. Groups are namespaces with kind='group'.",
|
|
394
399
|
inputSchema: toJSONSchema(GetNamespaceSchema),
|
|
395
400
|
},
|
|
396
401
|
{
|
|
@@ -683,6 +688,11 @@ export const allTools = [
|
|
|
683
688
|
description: "Get changes/diffs of a specific commit",
|
|
684
689
|
inputSchema: toJSONSchema(GetCommitDiffSchema),
|
|
685
690
|
},
|
|
691
|
+
{
|
|
692
|
+
name: "get_file_blame",
|
|
693
|
+
description: "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.",
|
|
694
|
+
inputSchema: toJSONSchema(GetFileBlameSchema),
|
|
695
|
+
},
|
|
686
696
|
{
|
|
687
697
|
name: "list_commit_statuses",
|
|
688
698
|
description: "List statuses for a commit",
|
|
@@ -995,6 +1005,7 @@ export const readOnlyTools = new Set([
|
|
|
995
1005
|
"list_commits",
|
|
996
1006
|
"get_commit",
|
|
997
1007
|
"get_commit_diff",
|
|
1008
|
+
"get_file_blame",
|
|
998
1009
|
"list_commit_statuses",
|
|
999
1010
|
"list_group_iterations",
|
|
1000
1011
|
"get_group_iteration",
|
|
@@ -1203,6 +1214,7 @@ export const TOOLSET_DEFINITIONS = [
|
|
|
1203
1214
|
"list_commits",
|
|
1204
1215
|
"get_commit",
|
|
1205
1216
|
"get_commit_diff",
|
|
1217
|
+
"get_file_blame",
|
|
1206
1218
|
"list_commit_statuses",
|
|
1207
1219
|
"create_commit_status",
|
|
1208
1220
|
]),
|
|
@@ -1238,6 +1250,11 @@ export const TOOLSET_DEFINITIONS = [
|
|
|
1238
1250
|
isDefault: true,
|
|
1239
1251
|
tools: new Set(["validate_ci_lint", "validate_project_ci_lint"]),
|
|
1240
1252
|
},
|
|
1253
|
+
{
|
|
1254
|
+
id: "groups",
|
|
1255
|
+
isDefault: true,
|
|
1256
|
+
tools: new Set(["create_group"]),
|
|
1257
|
+
},
|
|
1241
1258
|
{
|
|
1242
1259
|
id: "pipelines",
|
|
1243
1260
|
isDefault: false,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zereight/mcp-gitlab",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.13",
|
|
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": [
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"changelog": "auto-changelog -p",
|
|
52
52
|
"test": "npm run test:all",
|
|
53
53
|
"test:all": "npm run build && npm run test:mock && npm run test:live",
|
|
54
|
-
"test:mock": "node --import tsx/esm --test test/remote-auth-simple-test.ts && node --import tsx/esm --test test/mcp-oauth-tests.ts && node --import tsx/esm --test test/streamable-http-static-token-auth.test.ts && tsx test/oauth-tests.ts && tsx test/test-list-merge-requests.ts && node --import tsx/esm --test test/test-merge-request-pipelines.ts && tsx test/test-list-project-members.ts && tsx test/test-download-attachment.ts && node --import tsx/esm --test test/test-job-artifacts.ts && node --import tsx/esm --test test/test-deployment-tools.ts && node --import tsx/esm --test test/test-merge-request-approval-state-tools.ts && node --import tsx/esm --test test/test-search-code.ts && node --import tsx/esm --test test/test-tags.ts && node --import tsx/esm --test test/test-toolset-filtering.ts && node --import tsx/esm --test test/test-ci-lint.ts && node --import tsx/esm --test test/test-todos.ts && node --import tsx/esm --test test/test-auth-retry.ts && node --import tsx/esm --test test/test-issue-description-patch.ts && node --import tsx/esm --test test/stateless/codec.test.ts test/stateless/client-id.test.ts test/stateless/callback-proxy.test.ts test/stateless/session-id.test.ts test/stateless/session-id-integration.test.ts test/stateless/config-ttl.test.ts",
|
|
54
|
+
"test:mock": "node --import tsx/esm --test test/remote-auth-simple-test.ts && node --import tsx/esm --test test/mcp-oauth-tests.ts && node --import tsx/esm --test test/streamable-http-static-token-auth.test.ts && tsx test/oauth-tests.ts && tsx test/test-list-merge-requests.ts && node --import tsx/esm --test test/test-merge-request-pipelines.ts && tsx test/test-list-project-members.ts && tsx test/test-download-attachment.ts && node --import tsx/esm --test test/test-job-artifacts.ts && node --import tsx/esm --test test/test-deployment-tools.ts && node --import tsx/esm --test test/test-merge-request-approval-state-tools.ts && node --import tsx/esm --test test/test-search-code.ts && node --import tsx/esm --test test/test-tags.ts && node --import tsx/esm --test test/test-toolset-filtering.ts && node --import tsx/esm --test test/test-ci-lint.ts && node --import tsx/esm --test test/test-todos.ts && node --import tsx/esm --test test/test-auth-retry.ts && node --import tsx/esm --test test/test-issue-description-patch.ts && node --import tsx/esm --test test/test-geteffectiveprojectid.ts && node --import tsx/esm --test test/test-get-file-blame.ts && node --import tsx/esm --test test/stateless/codec.test.ts test/stateless/client-id.test.ts test/stateless/callback-proxy.test.ts test/stateless/session-id.test.ts test/stateless/session-id-integration.test.ts test/stateless/config-ttl.test.ts",
|
|
55
55
|
"test:stateless": "npm run build && node --import tsx/esm --test test/stateless/codec.test.ts test/stateless/client-id.test.ts test/stateless/callback-proxy.test.ts test/stateless/session-id.test.ts test/stateless/session-id-integration.test.ts test/stateless/config-ttl.test.ts",
|
|
56
56
|
"test:mcp-oauth": "npm run build && node --import tsx/esm --test test/mcp-oauth-tests.ts",
|
|
57
57
|
"test:live": "node test/validate-api.js",
|