@zereight/mcp-gitlab 2.1.11 → 2.1.12
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 +106 -105
- package/build/index.js +129 -3
- package/build/schemas.js +47 -0
- package/build/test/test-issue-description-patch.js +256 -0
- package/build/test/test-token-optimizations.js +1 -1
- package/build/test/test-toolset-filtering.js +4 -3
- package/build/test/utils/mock-gitlab-server.js +46 -0
- package/build/tools/registry.js +33 -1
- package/build/utils/patch-helper.js +145 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -540,111 +540,112 @@ Register the skill directory in your AI client to get optimal tool usage guidanc
|
|
|
540
540
|
47. `my_issues` - List issues assigned to the authenticated user (defaults to open issues)
|
|
541
541
|
48. `get_issue` - Get details of a specific issue in a GitLab project
|
|
542
542
|
49. `update_issue` - Update an issue in a GitLab project
|
|
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. `
|
|
543
|
+
50. `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.
|
|
544
|
+
51. `delete_issue` - Delete an issue from a GitLab project
|
|
545
|
+
52. `list_todos` - List GitLab to-do items for the current user
|
|
546
|
+
53. `mark_todo_done` - Mark a GitLab to-do item as done
|
|
547
|
+
54. `mark_all_todos_done` - Mark all pending GitLab to-do items as done for the current user
|
|
548
|
+
55. `list_issue_links` - List all issue links for a specific issue
|
|
549
|
+
56. `list_issue_discussions` - List discussions for an issue in a GitLab project
|
|
550
|
+
57. `get_issue_link` - Get a specific issue link
|
|
551
|
+
58. `create_issue_link` - Create an issue link between two issues
|
|
552
|
+
59. `delete_issue_link` - Delete an issue link
|
|
553
|
+
60. `list_namespaces` - List all namespaces available to the current user
|
|
554
|
+
61. `get_namespace` - Get details of a namespace by ID or path
|
|
555
|
+
62. `verify_namespace` - Verify if a namespace path exists
|
|
556
|
+
63. `get_project` - Get details of a specific project
|
|
557
|
+
64. `list_projects` - List projects accessible by the current user
|
|
558
|
+
65. `list_project_members` - List members of a GitLab project
|
|
559
|
+
66. `list_group_projects` - List projects in a GitLab group with filtering options
|
|
560
|
+
67. `list_group_iterations` - List group iterations with filtering options
|
|
561
|
+
68. `list_labels` - List labels for a project
|
|
562
|
+
69. `get_label` - Get a single label from a project
|
|
563
|
+
70. `create_label` - Create a new label in a project
|
|
564
|
+
71. `update_label` - Update an existing label in a project
|
|
565
|
+
72. `delete_label` - Delete a label from a project
|
|
566
|
+
73. `list_pipelines` - List pipelines in a GitLab project with filtering options
|
|
567
|
+
74. `get_pipeline` - Get details of a specific pipeline in a GitLab project
|
|
568
|
+
75. `list_pipeline_jobs` - List all jobs in a specific pipeline
|
|
569
|
+
76. `list_pipeline_trigger_jobs` - List all trigger jobs (bridges) in a specific pipeline that trigger downstream pipelines
|
|
570
|
+
77. `get_pipeline_job` - Get details of a GitLab pipeline job number
|
|
571
|
+
78. `get_pipeline_job_output` - Get the output/trace of a GitLab pipeline job with optional pagination to limit context window usage
|
|
572
|
+
79. `validate_ci_lint` - Validate provided GitLab CI/CD YAML content for a project
|
|
573
|
+
80. `validate_project_ci_lint` - Validate an existing `.gitlab-ci.yml` configuration for a project
|
|
574
|
+
81. `create_pipeline` - Create a new pipeline for a branch or tag
|
|
575
|
+
82. `retry_pipeline` - Retry a failed or canceled pipeline
|
|
576
|
+
83. `cancel_pipeline` - Cancel a running pipeline
|
|
577
|
+
84. `play_pipeline_job` - Run a manual pipeline job
|
|
578
|
+
85. `retry_pipeline_job` - Retry a failed or canceled pipeline job
|
|
579
|
+
86. `cancel_pipeline_job` - Cancel a running pipeline job
|
|
580
|
+
87. `list_deployments` - List deployments in a GitLab project with filtering options
|
|
581
|
+
88. `get_deployment` - Get details of a specific deployment in a GitLab project
|
|
582
|
+
89. `list_environments` - List environments in a GitLab project
|
|
583
|
+
90. `get_environment` - Get details of a specific environment in a GitLab project
|
|
584
|
+
91. `list_job_artifacts` - List artifact files in a job's artifacts archive. Returns file names, paths, types, and sizes
|
|
585
|
+
92. `download_job_artifacts` - Download the entire artifact archive (zip) for a job to a local path. Returns the saved file path
|
|
586
|
+
93. `get_job_artifact_file` - Get the content of a single file from a job's artifacts by its path within the archive
|
|
587
|
+
94. `list_milestones` - List milestones in a GitLab project with filtering options
|
|
588
|
+
95. `get_milestone` - Get details of a specific milestone
|
|
589
|
+
96. `create_milestone` - Create a new milestone in a GitLab project
|
|
590
|
+
97. `edit_milestone` - Edit an existing milestone in a GitLab project
|
|
591
|
+
98. `delete_milestone` - Delete a milestone from a GitLab project
|
|
592
|
+
99. `get_milestone_issue` - Get issues associated with a specific milestone
|
|
593
|
+
100. `get_milestone_merge_requests` - Get merge requests associated with a specific milestone
|
|
594
|
+
101. `promote_milestone` - Promote a milestone to the next stage
|
|
595
|
+
102. `get_milestone_burndown_events` - Get burndown events for a specific milestone
|
|
596
|
+
103. `list_wiki_pages` - List wiki pages in a GitLab project
|
|
597
|
+
104. `get_wiki_page` - Get details of a specific wiki page
|
|
598
|
+
105. `create_wiki_page` - Create a new wiki page in a GitLab project
|
|
599
|
+
106. `update_wiki_page` - Update an existing wiki page in a GitLab project
|
|
600
|
+
107. `delete_wiki_page` - Delete a wiki page from a GitLab project
|
|
601
|
+
108. `list_group_wiki_pages` - List wiki pages in a GitLab group
|
|
602
|
+
109. `get_group_wiki_page` - Get details of a specific group wiki page
|
|
603
|
+
110. `create_group_wiki_page` - Create a new wiki page in a GitLab group
|
|
604
|
+
111. `update_group_wiki_page` - Update an existing wiki page in a GitLab group
|
|
605
|
+
112. `delete_group_wiki_page` - Delete a wiki page from a GitLab group
|
|
606
|
+
113. `get_repository_tree` - Get the repository tree for a GitLab project (list files and directories)
|
|
607
|
+
114. `list_commits` - List repository commits with filtering options
|
|
608
|
+
115. `get_commit` - Get details of a specific commit
|
|
609
|
+
116. `get_commit_diff` - Get changes/diffs of a specific commit
|
|
610
|
+
117. `list_commit_statuses` - List statuses for a specific commit
|
|
611
|
+
118. `create_commit_status` - Create or update the status of a specific commit
|
|
612
|
+
119. `list_releases` - List all releases for a project
|
|
613
|
+
120. `get_release` - Get a release by tag name
|
|
614
|
+
121. `create_release` - Create a new release in a GitLab project
|
|
615
|
+
122. `update_release` - Update an existing release in a GitLab project
|
|
616
|
+
123. `delete_release` - Delete a release from a GitLab project (does not delete the associated tag)
|
|
617
|
+
124. `create_release_evidence` - Create release evidence for an existing release (GitLab Premium/Ultimate only)
|
|
618
|
+
125. `download_release_asset` - Download a release asset file by direct asset path
|
|
619
|
+
126. `list_tags` - List repository tags with filtering and pagination support
|
|
620
|
+
127. `get_tag` - Get details of a specific repository tag
|
|
621
|
+
128. `create_tag` - Create a new tag in the repository
|
|
622
|
+
129. `delete_tag` - Delete a tag from the repository
|
|
623
|
+
130. `get_tag_signature` - Get the signature of a signed tag
|
|
624
|
+
131. `get_users` - Get GitLab user details by usernames
|
|
625
|
+
132. `list_events` - List all events for the currently authenticated user
|
|
626
|
+
133. `get_project_events` - List all visible events for a specified project
|
|
627
|
+
134. `upload_markdown` - Upload a file to a GitLab project for use in markdown content
|
|
628
|
+
135. `download_attachment` - Download an uploaded file from a GitLab project by secret and filename
|
|
629
|
+
136. `get_work_item` - Get a single work item with full details including status, hierarchy (parent/children), type, labels, assignees, and all widgets
|
|
630
|
+
137. `list_work_items` - List work items in a project with filters (type, state, search, assignees, labels). Returns items with status and hierarchy info
|
|
631
|
+
138. `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
|
|
632
|
+
139. `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
|
|
633
|
+
140. `convert_work_item_type` - Convert a work item to a different type (e.g. issue to task, task to incident)
|
|
634
|
+
141. `list_work_item_statuses` - List available statuses for a work item type in a project. Requires GitLab Premium/Ultimate with configurable statuses
|
|
635
|
+
142. `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
|
|
636
|
+
143. `move_work_item` - Move a work item (issue, task, etc.) to a different project. Uses GitLab GraphQL issueMove mutation
|
|
637
|
+
144. `list_work_item_notes` - List notes and discussions on a work item. Returns threaded discussions with author, body, timestamps, and system/internal flags
|
|
638
|
+
145. `create_work_item_note` - Add a note/comment to a work item. Supports Markdown, internal notes, and threaded replies
|
|
639
|
+
146. `get_timeline_events` - List timeline events for an incident. Returns chronological events with notes, timestamps, and tags
|
|
640
|
+
147. `create_timeline_event` - Create a timeline event on an incident. Supports tags: 'Start time', 'End time', 'Impact detected', 'Response initiated', 'Impact mitigated', 'Cause identified'
|
|
641
|
+
148. `list_webhooks` - List all configured webhooks for a GitLab project or group. Provide either project_id or group_id
|
|
642
|
+
149. `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
|
|
643
|
+
150. `get_webhook_event` - Get full details of a specific webhook event by ID, including request/response payloads
|
|
644
|
+
151. `search_code` - Search for code across all projects on the GitLab instance (requires advanced search or exact code search to be enabled)
|
|
645
|
+
152. `search_project_code` - Search for code within a specific GitLab project (requires advanced search or exact code search to be enabled)
|
|
646
|
+
153. `search_group_code` - Search for code within a specific GitLab group (requires advanced search or exact code search to be enabled)
|
|
647
|
+
154. `execute_graphql` - Execute a GitLab GraphQL query
|
|
648
|
+
155. `list_merge_request_pipelines` - List pipelines for a merge request with pagination support
|
|
648
649
|
|
|
649
650
|
<!-- TOOLS-END -->
|
|
650
651
|
|
package/build/index.js
CHANGED
|
@@ -21,17 +21,18 @@ import { createGitLabOAuthProvider } from "./oauth-proxy.js";
|
|
|
21
21
|
import { mcpAuthRouter } from "@modelcontextprotocol/sdk/server/auth/router.js";
|
|
22
22
|
import { normalizeGitLabApiUrl } from "./utils/url.js";
|
|
23
23
|
import { estimateMergeCommitCount, filterDiffsByPatterns, summarizeWebhookEvents } from "./utils/helpers.js";
|
|
24
|
+
import { parseSearchReplaceBlocks, applySearchReplace, applyUnifiedDiff, } from "./utils/patch-helper.js";
|
|
24
25
|
import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js";
|
|
25
26
|
import { GitLabClientPool } from "./gitlab-client-pool.js";
|
|
26
27
|
import { allTools, readOnlyTools, destructiveTools, parseEnabledToolsets, parseIndividualTools, buildFeatureFlagOverrides, isToolInEnabledToolset, TOOLSET_DEFINITIONS, ALL_TOOLSET_IDS, } from "./tools/registry.js";
|
|
27
28
|
import { BulkPublishDraftNotesSchema, CancelPipelineJobSchema, CancelPipelineSchema, CreateBranchSchema, CreateDraftNoteSchema, CreateIssueLinkSchema, CreateIssueNoteSchema, CreateIssueSchema, CreateIssueEmojiReactionSchema, CreateIssueNoteEmojiReactionSchema, ListIssueEmojiReactionsSchema, ListIssueNoteEmojiReactionsSchema, CreateLabelSchema, // Added
|
|
28
|
-
CreateMergeRequestNoteSchema, CreateMergeRequestDiscussionNoteSchema, CreateMergeRequestEmojiReactionSchema, CreateMergeRequestNoteEmojiReactionSchema, ListMergeRequestEmojiReactionsSchema, ListMergeRequestNoteEmojiReactionsSchema, CreateMergeRequestSchema, CreateMergeRequestThreadSchema, CreateNoteSchema, CreateCommitStatusSchema, CreateOrUpdateFileSchema, CreatePipelineSchema, CreateProjectMilestoneSchema, CreateRepositorySchema, CreateWikiPageSchema, CreateGroupWikiPageSchema, DeleteDraftNoteSchema, DeleteGroupWikiPageSchema, DeleteIssueLinkSchema, DeleteIssueSchema, DeleteIssueEmojiReactionSchema, DeleteIssueNoteEmojiReactionSchema, DeleteLabelSchema, DeleteProjectMilestoneSchema, DeleteWikiPageSchema, DeleteMergeRequestNoteSchema, DeleteMergeRequestEmojiReactionSchema, DeleteMergeRequestNoteEmojiReactionSchema, EditProjectMilestoneSchema, ForkRepositorySchema, GetBranchDiffsSchema, 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, 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
30
|
// Discussion Schemas
|
|
30
31
|
GitLabDiscussionNoteSchema, // Added
|
|
31
32
|
GitLabDiscussionSchema,
|
|
32
33
|
// Draft Notes Schemas
|
|
33
|
-
GitLabDraftNoteSchema, GitLabForkSchema, 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, ListCommitsSchema, ListDraftNotesSchema, ListGroupIterationsSchema, ListGroupProjectsSchema, ListIssueDiscussionsSchema, ListIssueLinksSchema, ListIssuesSchema, ListTodosSchema, ListLabelsSchema, ListMergeRequestDiffsSchema, // Added
|
|
34
|
-
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, 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";
|
|
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
|
|
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";
|
|
35
36
|
import { randomUUID } from "node:crypto";
|
|
36
37
|
import { pino } from "pino";
|
|
37
38
|
const logger = pino({
|
|
@@ -6683,6 +6684,81 @@ async function handleToolCall(params) {
|
|
|
6683
6684
|
content: [{ type: "text", text: JSON.stringify(issue, null, 2) }],
|
|
6684
6685
|
};
|
|
6685
6686
|
}
|
|
6687
|
+
case "update_issue_description_patch": {
|
|
6688
|
+
const args = UpdateIssueDescriptionPatchSchema.parse(params.arguments);
|
|
6689
|
+
const { project_id, issue_iid, patch_type, patch, dry_run, create_note, allow_multiple } = args;
|
|
6690
|
+
// Fetch current issue description
|
|
6691
|
+
const currentIssue = await getIssue(project_id, issue_iid);
|
|
6692
|
+
const currentDescription = currentIssue.description ?? "";
|
|
6693
|
+
// Apply the patch
|
|
6694
|
+
let result;
|
|
6695
|
+
if (patch_type === "search_replace") {
|
|
6696
|
+
const blocks = parseSearchReplaceBlocks(patch);
|
|
6697
|
+
if (blocks.length === 0) {
|
|
6698
|
+
throw new Error("No valid search/replace blocks found. Expected format: <<<<<<< SEARCH\\ntext\\n=======\\nnew text\\n>>>>>>> REPLACE");
|
|
6699
|
+
}
|
|
6700
|
+
result = applySearchReplace(currentDescription, blocks, allow_multiple);
|
|
6701
|
+
}
|
|
6702
|
+
else {
|
|
6703
|
+
// unified_diff
|
|
6704
|
+
result = applyUnifiedDiff(currentDescription, patch);
|
|
6705
|
+
}
|
|
6706
|
+
// Dry-run: return preview without updating
|
|
6707
|
+
if (dry_run) {
|
|
6708
|
+
return {
|
|
6709
|
+
content: [
|
|
6710
|
+
{
|
|
6711
|
+
type: "text",
|
|
6712
|
+
text: JSON.stringify({
|
|
6713
|
+
status: "preview",
|
|
6714
|
+
dry_run: true,
|
|
6715
|
+
changes: result.changes,
|
|
6716
|
+
summary: result.summary,
|
|
6717
|
+
preview: result.preview,
|
|
6718
|
+
}, null, 2),
|
|
6719
|
+
},
|
|
6720
|
+
],
|
|
6721
|
+
};
|
|
6722
|
+
}
|
|
6723
|
+
// Apply the update
|
|
6724
|
+
const updatedIssue = await updateIssue(project_id, issue_iid, {
|
|
6725
|
+
description: result.description,
|
|
6726
|
+
});
|
|
6727
|
+
// Optionally create a note summarizing the change
|
|
6728
|
+
let noteResult = null;
|
|
6729
|
+
if (create_note) {
|
|
6730
|
+
try {
|
|
6731
|
+
const noteBody = `Updated issue description using patch-based tool.\n\n${result.summary}`;
|
|
6732
|
+
await createIssueNote(project_id, issue_iid, undefined, noteBody);
|
|
6733
|
+
noteResult = { status: "created" };
|
|
6734
|
+
}
|
|
6735
|
+
catch (noteError) {
|
|
6736
|
+
noteResult = {
|
|
6737
|
+
status: "failed",
|
|
6738
|
+
message: `Note creation failed: ${noteError.message ?? noteError}`,
|
|
6739
|
+
};
|
|
6740
|
+
}
|
|
6741
|
+
}
|
|
6742
|
+
return {
|
|
6743
|
+
content: [
|
|
6744
|
+
{
|
|
6745
|
+
type: "text",
|
|
6746
|
+
text: JSON.stringify({
|
|
6747
|
+
status: "success",
|
|
6748
|
+
changes: result.changes,
|
|
6749
|
+
summary: result.summary,
|
|
6750
|
+
note: noteResult,
|
|
6751
|
+
issue: {
|
|
6752
|
+
iid: updatedIssue.iid,
|
|
6753
|
+
title: updatedIssue.title,
|
|
6754
|
+
web_url: updatedIssue.web_url,
|
|
6755
|
+
updated_at: updatedIssue.updated_at,
|
|
6756
|
+
},
|
|
6757
|
+
}, null, 2),
|
|
6758
|
+
},
|
|
6759
|
+
],
|
|
6760
|
+
};
|
|
6761
|
+
}
|
|
6686
6762
|
case "delete_issue": {
|
|
6687
6763
|
const args = DeleteIssueSchema.parse(params.arguments);
|
|
6688
6764
|
await deleteIssue(args.project_id, args.issue_iid);
|
|
@@ -7612,6 +7688,56 @@ async function handleToolCall(params) {
|
|
|
7612
7688
|
content: [{ type: "text", text: JSON.stringify({ status: authenticated ? "ok" : "error", authenticated, gitlab_url: getEffectiveApiUrl() }) }],
|
|
7613
7689
|
};
|
|
7614
7690
|
}
|
|
7691
|
+
case "get_branch": {
|
|
7692
|
+
const args = GetBranchSchema.parse(params.arguments);
|
|
7693
|
+
const projectId = decodeURIComponent(args.project_id);
|
|
7694
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/repository/branches/${encodeURIComponent(args.branch_name)}`);
|
|
7695
|
+
const response = await fetch(url.toString(), {
|
|
7696
|
+
...getFetchConfig(),
|
|
7697
|
+
});
|
|
7698
|
+
await handleGitLabError(response);
|
|
7699
|
+
const data = await response.json();
|
|
7700
|
+
const branch = GitLabBranchSchema.parse(data);
|
|
7701
|
+
return {
|
|
7702
|
+
content: [{ type: "text", text: JSON.stringify(branch, null, 2) }],
|
|
7703
|
+
};
|
|
7704
|
+
}
|
|
7705
|
+
case "list_branches": {
|
|
7706
|
+
const args = ListBranchesSchema.parse(params.arguments);
|
|
7707
|
+
const projectId = decodeURIComponent(args.project_id);
|
|
7708
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/repository/branches`);
|
|
7709
|
+
if (args.search) {
|
|
7710
|
+
url.searchParams.append("search", args.search);
|
|
7711
|
+
}
|
|
7712
|
+
if (args.page) {
|
|
7713
|
+
url.searchParams.append("page", args.page.toString());
|
|
7714
|
+
}
|
|
7715
|
+
if (args.per_page) {
|
|
7716
|
+
url.searchParams.append("per_page", args.per_page.toString());
|
|
7717
|
+
}
|
|
7718
|
+
const response = await fetch(url.toString(), {
|
|
7719
|
+
...getFetchConfig(),
|
|
7720
|
+
});
|
|
7721
|
+
await handleGitLabError(response);
|
|
7722
|
+
const data = await response.json();
|
|
7723
|
+
const branches = z.array(GitLabBranchSchema).parse(data);
|
|
7724
|
+
return {
|
|
7725
|
+
content: [{ type: "text", text: JSON.stringify(branches, null, 2) }],
|
|
7726
|
+
};
|
|
7727
|
+
}
|
|
7728
|
+
case "delete_branch": {
|
|
7729
|
+
const args = DeleteBranchSchema.parse(params.arguments);
|
|
7730
|
+
const projectId = decodeURIComponent(args.project_id);
|
|
7731
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/repository/branches/${encodeURIComponent(args.branch_name)}`);
|
|
7732
|
+
const response = await fetch(url.toString(), {
|
|
7733
|
+
...getFetchConfig(),
|
|
7734
|
+
method: "DELETE",
|
|
7735
|
+
});
|
|
7736
|
+
await handleGitLabError(response);
|
|
7737
|
+
return {
|
|
7738
|
+
content: [{ type: "text", text: JSON.stringify({ status: "deleted", branch: args.branch_name }, null, 2) }],
|
|
7739
|
+
};
|
|
7740
|
+
}
|
|
7615
7741
|
default:
|
|
7616
7742
|
throw new Error(`Unknown tool: ${params.name}`);
|
|
7617
7743
|
}
|
package/build/schemas.js
CHANGED
|
@@ -1472,6 +1472,37 @@ export const CreateBranchSchema = ProjectParamsSchema.extend({
|
|
|
1472
1472
|
branch: z.string().describe("Name for the new branch"),
|
|
1473
1473
|
ref: z.string().optional().describe("Source branch/commit for new branch"),
|
|
1474
1474
|
});
|
|
1475
|
+
export const GetBranchSchema = ProjectParamsSchema.extend({
|
|
1476
|
+
branch_name: z.string().describe("Name of the branch"),
|
|
1477
|
+
});
|
|
1478
|
+
export const ListBranchesSchema = ProjectParamsSchema.extend({
|
|
1479
|
+
search: z.string().optional().describe("Search term to filter branches by name"),
|
|
1480
|
+
}).merge(PaginationOptionsSchema);
|
|
1481
|
+
export const DeleteBranchSchema = ProjectParamsSchema.extend({
|
|
1482
|
+
branch_name: z.string().describe("Name of the branch to delete"),
|
|
1483
|
+
});
|
|
1484
|
+
export const GitLabBranchSchema = z.object({
|
|
1485
|
+
name: z.string(),
|
|
1486
|
+
commit: z.object({
|
|
1487
|
+
id: z.string(),
|
|
1488
|
+
short_id: z.string(),
|
|
1489
|
+
title: z.string(),
|
|
1490
|
+
author_name: z.string(),
|
|
1491
|
+
author_email: z.string(),
|
|
1492
|
+
authored_date: z.string(),
|
|
1493
|
+
committer_name: z.string(),
|
|
1494
|
+
committer_email: z.string(),
|
|
1495
|
+
committed_date: z.string(),
|
|
1496
|
+
web_url: z.string(),
|
|
1497
|
+
}),
|
|
1498
|
+
merged: z.boolean(),
|
|
1499
|
+
protected: z.boolean(),
|
|
1500
|
+
developers_can_push: z.boolean(),
|
|
1501
|
+
developers_can_merge: z.boolean(),
|
|
1502
|
+
can_push: z.boolean(),
|
|
1503
|
+
default: z.boolean(),
|
|
1504
|
+
web_url: z.string().optional(),
|
|
1505
|
+
});
|
|
1475
1506
|
export const GetBranchDiffsSchema = ProjectParamsSchema.extend({
|
|
1476
1507
|
from: z.string().describe("The base branch or commit SHA to compare from"),
|
|
1477
1508
|
to: z.string().describe("The target branch or commit SHA to compare to"),
|
|
@@ -1839,6 +1870,22 @@ export const DeleteIssueSchema = z.object({
|
|
|
1839
1870
|
project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
|
|
1840
1871
|
issue_iid: z.coerce.string().describe("The internal ID of the project issue"),
|
|
1841
1872
|
});
|
|
1873
|
+
export const UpdateIssueDescriptionPatchSchema = z.object({
|
|
1874
|
+
project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
|
|
1875
|
+
issue_iid: z.coerce.string().describe("The internal ID of the project issue"),
|
|
1876
|
+
patch_type: z.enum(["search_replace", "unified_diff"]).describe("Type of patch format to apply"),
|
|
1877
|
+
patch: z
|
|
1878
|
+
.string()
|
|
1879
|
+
.min(1)
|
|
1880
|
+
.max(50000)
|
|
1881
|
+
.describe("The patch content to apply to the issue description"),
|
|
1882
|
+
dry_run: z.coerce.boolean().optional().describe("If true, preview changes without updating the issue"),
|
|
1883
|
+
create_note: z.coerce.boolean().optional().describe("If true, add a note summarizing the change after update"),
|
|
1884
|
+
allow_multiple: z
|
|
1885
|
+
.coerce.boolean()
|
|
1886
|
+
.optional()
|
|
1887
|
+
.describe("For search_replace: allow multiple matches to all be replaced (default: false — fail on duplicate)"),
|
|
1888
|
+
});
|
|
1842
1889
|
// Issue links related schemas
|
|
1843
1890
|
export const GitLabIssueLinkSchema = z.object({
|
|
1844
1891
|
source_issue: GitLabIssueSchema,
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the update_issue_description_patch tool.
|
|
3
|
+
* Tests search_replace, unified_diff, dry_run, create_note, and edge cases.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, test, before, after } from "node:test";
|
|
6
|
+
import assert from "node:assert";
|
|
7
|
+
import { launchServer, cleanupServers, TransportMode, HOST, } from "./utils/server-launcher.js";
|
|
8
|
+
import { MockGitLabServer, findMockServerPort, } from "./utils/mock-gitlab-server.js";
|
|
9
|
+
import { CustomHeaderClient } from "./clients/custom-header-client.js";
|
|
10
|
+
import { parseSearchReplaceBlocks, applySearchReplace, applyUnifiedDiff, } from "../utils/patch-helper.js";
|
|
11
|
+
const MOCK_TOKEN = "glpat-patch-test-token-12345";
|
|
12
|
+
// ---- Unit tests for patch helper ----
|
|
13
|
+
describe("parseSearchReplaceBlocks", () => {
|
|
14
|
+
test("parses a single block", () => {
|
|
15
|
+
const blocks = parseSearchReplaceBlocks("<<<<<<< SEARCH\nold text\n=======\nnew text\n>>>>>>> REPLACE");
|
|
16
|
+
assert.strictEqual(blocks.length, 1);
|
|
17
|
+
assert.strictEqual(blocks[0].search, "old text");
|
|
18
|
+
assert.strictEqual(blocks[0].replace, "new text");
|
|
19
|
+
});
|
|
20
|
+
test("parses multiple blocks", () => {
|
|
21
|
+
const blocks = parseSearchReplaceBlocks("<<<<<<< SEARCH\nfirst\n=======\nfirst new\n>>>>>>> REPLACE\n<<<<<<< SEARCH\nsecond\n=======\nsecond new\n>>>>>>> REPLACE");
|
|
22
|
+
assert.strictEqual(blocks.length, 2);
|
|
23
|
+
});
|
|
24
|
+
test("handles empty content", () => {
|
|
25
|
+
const blocks = parseSearchReplaceBlocks("");
|
|
26
|
+
assert.strictEqual(blocks.length, 0);
|
|
27
|
+
});
|
|
28
|
+
test("ignores text outside blocks", () => {
|
|
29
|
+
const blocks = parseSearchReplaceBlocks("prefix\n<<<<<<< SEARCH\nx\n=======\ny\n>>>>>>> REPLACE\nsuffix");
|
|
30
|
+
assert.strictEqual(blocks.length, 1);
|
|
31
|
+
});
|
|
32
|
+
test("rejects malformed block with missing REPLACE marker", () => {
|
|
33
|
+
assert.throws(() => parseSearchReplaceBlocks("<<<<<<< SEARCH\nfirst\n=======\nfirst new\n>>>>>>> REPLACE\n" +
|
|
34
|
+
"<<<<<<< SEARCH\nsecond\n=======\nsecond new\n>>>>>>> TYPO"), /malformed|Marker|marker/);
|
|
35
|
+
});
|
|
36
|
+
test("rejects block with missing ======= marker", () => {
|
|
37
|
+
assert.throws(() => parseSearchReplaceBlocks("<<<<<<< SEARCH\nfoo\n\nbar\n>>>>>>> REPLACE"), /malformed|Marker|marker/);
|
|
38
|
+
});
|
|
39
|
+
test("allows prose around valid blocks", () => {
|
|
40
|
+
const blocks = parseSearchReplaceBlocks("# Some notes\n\n<<<<<<< SEARCH\nfoo\n=======\nbar\n>>>>>>> REPLACE\n\nMore context");
|
|
41
|
+
assert.strictEqual(blocks.length, 1);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
describe("applySearchReplace", () => {
|
|
45
|
+
test("replaces single occurrence", () => {
|
|
46
|
+
const result = applySearchReplace("Status: In progress\nDone.", [
|
|
47
|
+
{ search: "Status: In progress", replace: "Status: Done" },
|
|
48
|
+
]);
|
|
49
|
+
assert.strictEqual(result.description, "Status: Done\nDone.");
|
|
50
|
+
assert.strictEqual(result.changes, 1);
|
|
51
|
+
assert.ok(result.summary.includes("Status: In progress"));
|
|
52
|
+
assert.ok(result.preview.includes("-Status: In progress"));
|
|
53
|
+
});
|
|
54
|
+
test("fails on no match", () => {
|
|
55
|
+
assert.throws(() => applySearchReplace("Some text.", [{ search: "Nonexistent", replace: "x" }]), /Search text not found/);
|
|
56
|
+
});
|
|
57
|
+
test("rejects empty SEARCH body", () => {
|
|
58
|
+
assert.throws(() => applySearchReplace("Some text.", [{ search: "", replace: "x" }], true), /Empty SEARCH/);
|
|
59
|
+
});
|
|
60
|
+
test("fails on duplicate without allowMultiple", () => {
|
|
61
|
+
assert.throws(() => applySearchReplace("x\ny\nx\n", [{ search: "x", replace: "z" }]), /matches 2 times/);
|
|
62
|
+
});
|
|
63
|
+
test("replaces all with allowMultiple", () => {
|
|
64
|
+
const result = applySearchReplace("x\ny\nx\n", [{ search: "x", replace: "z" }], true);
|
|
65
|
+
assert.strictEqual(result.changes, 2);
|
|
66
|
+
assert.strictEqual(result.description, "z\ny\nz\n");
|
|
67
|
+
});
|
|
68
|
+
test("fails on identical replacement", () => {
|
|
69
|
+
assert.throws(() => applySearchReplace("Keep this.", [{ search: "Keep this.", replace: "Keep this." }]), /did not change/);
|
|
70
|
+
});
|
|
71
|
+
test("preserves leading blank line in SEARCH block", () => {
|
|
72
|
+
const source = "\n\nStatus: In progress\n";
|
|
73
|
+
const result = applySearchReplace(source, [
|
|
74
|
+
{ search: "\n\nStatus: In progress", replace: "\n\nStatus: Done" },
|
|
75
|
+
]);
|
|
76
|
+
assert.strictEqual(result.description, "\n\nStatus: Done\n");
|
|
77
|
+
});
|
|
78
|
+
test("preserves leading blank line when patch starts with blank line", () => {
|
|
79
|
+
const blocks = parseSearchReplaceBlocks("<<<<<<< SEARCH\n\nfoo\n=======\n\nbar\n>>>>>>> REPLACE");
|
|
80
|
+
assert.strictEqual(blocks.length, 1);
|
|
81
|
+
assert.strictEqual(blocks[0].search, "\nfoo");
|
|
82
|
+
assert.strictEqual(blocks[0].replace, "\nbar");
|
|
83
|
+
});
|
|
84
|
+
test("replacement with leading blank line works", () => {
|
|
85
|
+
const result = applySearchReplace("Header\n\nContent\n", [
|
|
86
|
+
{ search: "Content", replace: "\nNewContent" },
|
|
87
|
+
]);
|
|
88
|
+
assert.strictEqual(result.description, "Header\n\n\nNewContent\n");
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
describe("applyUnifiedDiff", () => {
|
|
92
|
+
test("applies simple diff", () => {
|
|
93
|
+
const source = "Line 1\nLine 2\nLine 3\n";
|
|
94
|
+
const patch = "--- old\n+++ new\n@@ -1,3 +1,3 @@\n Line 1\n-Line 2\n+Line 2 modified\n Line 3\n";
|
|
95
|
+
const result = applyUnifiedDiff(source, patch);
|
|
96
|
+
assert.ok(result.description.includes("Line 2 modified"));
|
|
97
|
+
});
|
|
98
|
+
test("fails on non-matching diff", () => {
|
|
99
|
+
const source = "AAA\nBBB\n";
|
|
100
|
+
const patch = "--- old\n+++ new\n@@ -1,2 +1,2 @@\n-XXX\n+YYY\n BBB\n";
|
|
101
|
+
assert.throws(() => applyUnifiedDiff(source, patch), /could not be applied/);
|
|
102
|
+
});
|
|
103
|
+
test("fails on malformed patch", () => {
|
|
104
|
+
assert.throws(() => applyUnifiedDiff("text", "not a patch"), /no valid hunks/i);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
// ---- Integration tests via MCP client ----
|
|
108
|
+
describe("update_issue_description_patch MCP tool", () => {
|
|
109
|
+
let mockGitLab;
|
|
110
|
+
let server;
|
|
111
|
+
let client;
|
|
112
|
+
const MOCK_PORT_BASE = 9600;
|
|
113
|
+
const MCP_PORT_BASE = 3600;
|
|
114
|
+
let portCounter = 0;
|
|
115
|
+
async function launchMcp(mockGitLabUrl, extraEnv = {}) {
|
|
116
|
+
const port = MCP_PORT_BASE + portCounter++ * 10;
|
|
117
|
+
return launchServer({
|
|
118
|
+
mode: TransportMode.STREAMABLE_HTTP,
|
|
119
|
+
port,
|
|
120
|
+
timeout: 10000,
|
|
121
|
+
env: {
|
|
122
|
+
STREAMABLE_HTTP: "true",
|
|
123
|
+
REMOTE_AUTHORIZATION: "true",
|
|
124
|
+
GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
|
|
125
|
+
GITLAB_ACCESS_TOKEN: MOCK_TOKEN,
|
|
126
|
+
...extraEnv,
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
async function getClient(port) {
|
|
131
|
+
const client = new CustomHeaderClient({
|
|
132
|
+
headers: {
|
|
133
|
+
authorization: `Bearer ${MOCK_TOKEN}`,
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
await client.connect(`http://${HOST}:${port}/mcp`);
|
|
137
|
+
return client;
|
|
138
|
+
}
|
|
139
|
+
before(async () => {
|
|
140
|
+
const mockPort = await findMockServerPort(MOCK_PORT_BASE);
|
|
141
|
+
mockGitLab = new MockGitLabServer({
|
|
142
|
+
port: mockPort,
|
|
143
|
+
validTokens: [MOCK_TOKEN],
|
|
144
|
+
});
|
|
145
|
+
await mockGitLab.start();
|
|
146
|
+
const mockGitLabUrl = mockGitLab.getUrl();
|
|
147
|
+
server = await launchMcp(mockGitLabUrl, { GITLAB_TOOLSETS: "issues" });
|
|
148
|
+
client = await getClient(server.port ?? 0);
|
|
149
|
+
});
|
|
150
|
+
after(async () => {
|
|
151
|
+
await client?.disconnect();
|
|
152
|
+
cleanupServers([server]);
|
|
153
|
+
await mockGitLab?.stop();
|
|
154
|
+
});
|
|
155
|
+
test("tool appears in tool list", async () => {
|
|
156
|
+
const result = await client.listTools();
|
|
157
|
+
const names = result.tools.map((t) => t.name);
|
|
158
|
+
assert.ok(names.includes("update_issue_description_patch"), "tool should be in list");
|
|
159
|
+
});
|
|
160
|
+
test("dry_run: search_replace returns preview without modifying", async () => {
|
|
161
|
+
// Get current description
|
|
162
|
+
const getResult = await client.callTool("get_issue", {
|
|
163
|
+
project_id: "test/project",
|
|
164
|
+
issue_iid: "1",
|
|
165
|
+
});
|
|
166
|
+
const issue = JSON.parse(getResult.content[0]?.text || "{}");
|
|
167
|
+
const originalDesc = issue.description;
|
|
168
|
+
// Dry-run
|
|
169
|
+
const result = await client.callTool("update_issue_description_patch", {
|
|
170
|
+
project_id: "test/project",
|
|
171
|
+
issue_iid: "1",
|
|
172
|
+
patch_type: "search_replace",
|
|
173
|
+
patch: `<<<<<<< SEARCH\n${originalDesc}\n=======\nShould NOT persist\n>>>>>>> REPLACE`,
|
|
174
|
+
dry_run: true,
|
|
175
|
+
});
|
|
176
|
+
const data = JSON.parse(result.content[0]?.text || "{}");
|
|
177
|
+
assert.strictEqual(data.status, "preview");
|
|
178
|
+
assert.strictEqual(data.dry_run, true);
|
|
179
|
+
assert.strictEqual(data.changes, 1);
|
|
180
|
+
// Verify NOT updated
|
|
181
|
+
const getAgain = await client.callTool("get_issue", {
|
|
182
|
+
project_id: "test/project",
|
|
183
|
+
issue_iid: "1",
|
|
184
|
+
});
|
|
185
|
+
const issueAgain = JSON.parse(getAgain.content[0]?.text || "{}");
|
|
186
|
+
assert.strictEqual(issueAgain.description, originalDesc, "should be unchanged after dry_run");
|
|
187
|
+
});
|
|
188
|
+
test("search_replace: applies the patch", async () => {
|
|
189
|
+
const result = await client.callTool("update_issue_description_patch", {
|
|
190
|
+
project_id: "test/project",
|
|
191
|
+
issue_iid: "1",
|
|
192
|
+
patch_type: "search_replace",
|
|
193
|
+
patch: `<<<<<<< SEARCH\nDescription for issue 1\n=======\nPatched description\n>>>>>>> REPLACE`,
|
|
194
|
+
});
|
|
195
|
+
const data = JSON.parse(result.content[0]?.text || "{}");
|
|
196
|
+
assert.strictEqual(data.status, "success");
|
|
197
|
+
assert.strictEqual(data.changes, 1);
|
|
198
|
+
// Verify persisted
|
|
199
|
+
const getResult = await client.callTool("get_issue", {
|
|
200
|
+
project_id: "test/project",
|
|
201
|
+
issue_iid: "1",
|
|
202
|
+
});
|
|
203
|
+
const issue = JSON.parse(getResult.content[0]?.text || "{}");
|
|
204
|
+
assert.strictEqual(issue.description, "Patched description");
|
|
205
|
+
});
|
|
206
|
+
test("create_note: note is attempted after update", async () => {
|
|
207
|
+
const result = await client.callTool("update_issue_description_patch", {
|
|
208
|
+
project_id: "test/project",
|
|
209
|
+
issue_iid: "1",
|
|
210
|
+
patch_type: "search_replace",
|
|
211
|
+
patch: `<<<<<<< SEARCH\nPatched description\n=======\nDescription with note\n>>>>>>> REPLACE`,
|
|
212
|
+
create_note: true,
|
|
213
|
+
});
|
|
214
|
+
const data = JSON.parse(result.content[0]?.text || "{}");
|
|
215
|
+
assert.strictEqual(data.status, "success");
|
|
216
|
+
assert.ok(data.note !== undefined, "note result should be present");
|
|
217
|
+
});
|
|
218
|
+
test("search_replace: fails on no match", async () => {
|
|
219
|
+
try {
|
|
220
|
+
await client.callTool("update_issue_description_patch", {
|
|
221
|
+
project_id: "test/project",
|
|
222
|
+
issue_iid: "1",
|
|
223
|
+
patch_type: "search_replace",
|
|
224
|
+
patch: `<<<<<<< SEARCH\nNonExistentText_{UNIQUE}_\n=======\nShould fail\n>>>>>>> REPLACE`,
|
|
225
|
+
});
|
|
226
|
+
assert.fail("Should have thrown");
|
|
227
|
+
}
|
|
228
|
+
catch (err) {
|
|
229
|
+
const msg = err.message ?? String(err);
|
|
230
|
+
assert.ok(msg.includes("not found"), `Error should mention 'not found': ${msg}`);
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
test("unified_diff: applies the patch", async () => {
|
|
234
|
+
// Set known state
|
|
235
|
+
await client.callTool("update_issue_description_patch", {
|
|
236
|
+
project_id: "test/project",
|
|
237
|
+
issue_iid: "1",
|
|
238
|
+
patch_type: "search_replace",
|
|
239
|
+
patch: `<<<<<<< SEARCH\nDescription with note\n=======\nLine 1\nLine 2\nLine 3\n>>>>>>> REPLACE`,
|
|
240
|
+
});
|
|
241
|
+
const result = await client.callTool("update_issue_description_patch", {
|
|
242
|
+
project_id: "test/project",
|
|
243
|
+
issue_iid: "1",
|
|
244
|
+
patch_type: "unified_diff",
|
|
245
|
+
patch: "--- old\n+++ new\n@@ -1,3 +1,3 @@\n Line 1\n-Line 2\n+Line 2 changed\n Line 3\n",
|
|
246
|
+
});
|
|
247
|
+
const data = JSON.parse(result.content[0]?.text || "{}");
|
|
248
|
+
assert.strictEqual(data.status, "success");
|
|
249
|
+
const getResult = await client.callTool("get_issue", {
|
|
250
|
+
project_id: "test/project",
|
|
251
|
+
issue_iid: "1",
|
|
252
|
+
});
|
|
253
|
+
const issue = JSON.parse(getResult.content[0]?.text || "{}");
|
|
254
|
+
assert.ok(issue.description.includes("Line 2 changed"));
|
|
255
|
+
});
|
|
256
|
+
});
|
|
@@ -389,7 +389,7 @@ describe("Policy Edge Cases", { concurrency: 1 }, () => {
|
|
|
389
389
|
test("hiding all toolset tools leaves only discover_tools", async () => {
|
|
390
390
|
const allIssueTools = [
|
|
391
391
|
"create_issue", "list_issues", "my_issues", "get_issue",
|
|
392
|
-
"update_issue", "delete_issue", "create_issue_note", "update_issue_note",
|
|
392
|
+
"update_issue", "update_issue_description_patch", "delete_issue", "list_todos", "mark_todo_done", "mark_all_todos_done", "create_issue_note", "update_issue_note",
|
|
393
393
|
"list_issue_links", "list_issue_discussions", "get_issue_link",
|
|
394
394
|
"create_issue_link", "delete_issue_link", "create_note",
|
|
395
395
|
"list_issue_emoji_reactions", "list_issue_note_emoji_reactions",
|
|
@@ -17,9 +17,9 @@ const MCP_PORT_BASE = 3200;
|
|
|
17
17
|
// Known tool counts per toolset (from TOOLSET_DEFINITIONS)
|
|
18
18
|
const TOOLSET_TOOL_COUNTS = {
|
|
19
19
|
merge_requests: 41,
|
|
20
|
-
issues:
|
|
20
|
+
issues: 24,
|
|
21
21
|
repositories: 7,
|
|
22
|
-
branches:
|
|
22
|
+
branches: 9,
|
|
23
23
|
projects: 9,
|
|
24
24
|
labels: 5,
|
|
25
25
|
ci: 2,
|
|
@@ -63,7 +63,7 @@ const TOOLSET_SAMPLE_TOOLS = {
|
|
|
63
63
|
merge_requests: ["merge_merge_request", "create_merge_request_thread", "list_draft_notes"],
|
|
64
64
|
issues: ["create_issue", "list_issues", "create_note", "list_todos"],
|
|
65
65
|
repositories: ["search_repositories", "get_file_contents", "push_files"],
|
|
66
|
-
branches: ["create_branch", "list_commits", "list_commit_statuses", "create_commit_status"],
|
|
66
|
+
branches: ["create_branch", "get_branch", "list_branches", "delete_branch", "list_commits", "list_commit_statuses", "create_commit_status"],
|
|
67
67
|
projects: ["get_project", "list_namespaces", "list_group_iterations"],
|
|
68
68
|
labels: ["list_labels", "create_label"],
|
|
69
69
|
ci: ["validate_ci_lint", "validate_project_ci_lint"],
|
|
@@ -308,6 +308,7 @@ describe("Toolset Filtering", { concurrency: 1 }, () => {
|
|
|
308
308
|
const writeIssueTools = [
|
|
309
309
|
"create_issue",
|
|
310
310
|
"update_issue",
|
|
311
|
+
"update_issue_description_patch",
|
|
311
312
|
"delete_issue",
|
|
312
313
|
"mark_todo_done",
|
|
313
314
|
"mark_all_todos_done",
|
|
@@ -13,6 +13,8 @@ export class MockGitLabServer {
|
|
|
13
13
|
// Root-level dynamic router (for OAuth paths not under /api/v4)
|
|
14
14
|
rootRouter;
|
|
15
15
|
rootHandlers = new Map();
|
|
16
|
+
// In-memory store for mutable resources (issues, etc.)
|
|
17
|
+
issueStore = new Map();
|
|
16
18
|
constructor(config) {
|
|
17
19
|
this.config = config;
|
|
18
20
|
this.app = express();
|
|
@@ -314,6 +316,12 @@ export class MockGitLabServer {
|
|
|
314
316
|
this.app.get("/api/v4/projects/:projectId/issues/:issue_iid", (req, res) => {
|
|
315
317
|
const issueIid = parseInt(req.params.issue_iid);
|
|
316
318
|
const projectId = req.params.projectId;
|
|
319
|
+
const storeKey = `${projectId}:${issueIid}`;
|
|
320
|
+
const stored = this.issueStore.get(storeKey);
|
|
321
|
+
if (stored) {
|
|
322
|
+
res.json(stored);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
317
325
|
res.json({
|
|
318
326
|
id: issueIid,
|
|
319
327
|
iid: issueIid,
|
|
@@ -337,6 +345,44 @@ export class MockGitLabServer {
|
|
|
337
345
|
milestone: null,
|
|
338
346
|
});
|
|
339
347
|
});
|
|
348
|
+
// PUT /api/v4/projects/:projectId/issues/:issue_iid - Update issue
|
|
349
|
+
this.app.put("/api/v4/projects/:projectId/issues/:issue_iid", (req, res) => {
|
|
350
|
+
const issueIid = parseInt(req.params.issue_iid);
|
|
351
|
+
const projectId = req.params.projectId;
|
|
352
|
+
const storeKey = `${projectId}:${issueIid}`;
|
|
353
|
+
// Build response from stored data (if previously updated) or defaults
|
|
354
|
+
const stored = this.issueStore.get(storeKey) || {};
|
|
355
|
+
const description = req.body?.description ?? stored.description ?? `Description for issue ${issueIid}`;
|
|
356
|
+
const title = req.body?.title ?? stored.title ?? `Test Issue ${issueIid}`;
|
|
357
|
+
const state = req.body?.state_event === "close" ? "closed" :
|
|
358
|
+
req.body?.state_event === "reopen" ? "opened" :
|
|
359
|
+
(stored.state ?? "opened");
|
|
360
|
+
const updatedIssue = {
|
|
361
|
+
id: issueIid,
|
|
362
|
+
iid: issueIid,
|
|
363
|
+
project_id: projectId,
|
|
364
|
+
title,
|
|
365
|
+
description,
|
|
366
|
+
state,
|
|
367
|
+
created_at: stored.created_at ?? "2024-01-01T00:00:00Z",
|
|
368
|
+
updated_at: new Date().toISOString(),
|
|
369
|
+
closed_at: state === "closed" ? new Date().toISOString() : null,
|
|
370
|
+
web_url: `https://gitlab.mock/project/${projectId}/issues/${issueIid}`,
|
|
371
|
+
author: {
|
|
372
|
+
id: 1,
|
|
373
|
+
username: "test-user",
|
|
374
|
+
name: "Test User",
|
|
375
|
+
avatar_url: null,
|
|
376
|
+
web_url: "https://gitlab.mock/test-user",
|
|
377
|
+
},
|
|
378
|
+
assignees: [],
|
|
379
|
+
labels: [],
|
|
380
|
+
milestone: null,
|
|
381
|
+
};
|
|
382
|
+
// Store for subsequent GET requests
|
|
383
|
+
this.issueStore.set(storeKey, updatedIssue);
|
|
384
|
+
res.json(updatedIssue);
|
|
385
|
+
});
|
|
340
386
|
// Mock blob search result
|
|
341
387
|
const mockBlobResults = [
|
|
342
388
|
{
|
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, DeleteDraftNoteSchema, DeleteGroupWikiPageSchema, DeleteIssueLinkSchema, DeleteIssueSchema, DeleteIssueEmojiReactionSchema, DeleteIssueNoteEmojiReactionSchema, DeleteLabelSchema, DeleteMergeRequestDiscussionNoteSchema, DeleteMergeRequestNoteSchema, DeleteMergeRequestEmojiReactionSchema, DeleteMergeRequestNoteEmojiReactionSchema, DeleteProjectMilestoneSchema, DeleteReleaseSchema, DeleteTagSchema, DeleteWikiPageSchema, DeleteWorkItemEmojiReactionSchema, DeleteWorkItemNoteEmojiReactionSchema, DownloadAttachmentSchema, DownloadJobArtifactsSchema, DownloadReleaseAssetSchema, EditProjectMilestoneSchema, ExecuteGraphQLSchema, ForkRepositorySchema, HealthCheckSchema, 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, 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, UpdateLabelSchema, UpdateMergeRequestDiscussionNoteSchema, UpdateMergeRequestNoteSchema, UpdateMergeRequestSchema, UpdateReleaseSchema, UpdateWikiPageSchema, UpdateWorkItemSchema, VerifyNamespaceSchema, } from "../schemas.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";
|
|
5
5
|
// Define all available tools
|
|
6
6
|
export const allTools = [
|
|
7
7
|
{
|
|
@@ -84,6 +84,21 @@ export const allTools = [
|
|
|
84
84
|
description: "Create a new branch",
|
|
85
85
|
inputSchema: toJSONSchema(CreateBranchSchema),
|
|
86
86
|
},
|
|
87
|
+
{
|
|
88
|
+
name: "get_branch",
|
|
89
|
+
description: "Get branch details (commit, protection status)",
|
|
90
|
+
inputSchema: toJSONSchema(GetBranchSchema),
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: "list_branches",
|
|
94
|
+
description: "List branches in project with search filter",
|
|
95
|
+
inputSchema: toJSONSchema(ListBranchesSchema),
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: "delete_branch",
|
|
99
|
+
description: "Delete branch from project",
|
|
100
|
+
inputSchema: toJSONSchema(DeleteBranchSchema),
|
|
101
|
+
},
|
|
87
102
|
{
|
|
88
103
|
name: "get_merge_request",
|
|
89
104
|
description: "Get details of a merge request (mergeRequestIid or branchName required)",
|
|
@@ -316,6 +331,13 @@ export const allTools = [
|
|
|
316
331
|
description: "Update an issue",
|
|
317
332
|
inputSchema: toJSONSchema(UpdateIssueSchema),
|
|
318
333
|
},
|
|
334
|
+
{
|
|
335
|
+
name: "update_issue_description_patch",
|
|
336
|
+
description: "Apply a patch (search/replace or unified diff) to an issue description. " +
|
|
337
|
+
"Reduces token usage by allowing small changes without sending the full description. " +
|
|
338
|
+
"Supports dry_run to preview changes and create_note to summarize updates.",
|
|
339
|
+
inputSchema: toJSONSchema(UpdateIssueDescriptionPatchSchema),
|
|
340
|
+
},
|
|
319
341
|
{
|
|
320
342
|
name: "delete_issue",
|
|
321
343
|
description: "Delete an issue",
|
|
@@ -916,6 +938,8 @@ export const readOnlyTools = new Set([
|
|
|
916
938
|
"get_merge_request_file_diff",
|
|
917
939
|
"list_merge_request_versions",
|
|
918
940
|
"get_merge_request_version",
|
|
941
|
+
"get_branch",
|
|
942
|
+
"list_branches",
|
|
919
943
|
"get_branch_diffs",
|
|
920
944
|
"list_merge_request_pipelines",
|
|
921
945
|
"get_merge_request_note",
|
|
@@ -1020,8 +1044,10 @@ export const destructiveTools = new Set([
|
|
|
1020
1044
|
"delete_issue_note_emoji_reaction",
|
|
1021
1045
|
"delete_work_item_emoji_reaction",
|
|
1022
1046
|
"delete_work_item_note_emoji_reaction",
|
|
1047
|
+
"delete_branch",
|
|
1023
1048
|
"merge_merge_request",
|
|
1024
1049
|
"push_files",
|
|
1050
|
+
"delete_branch",
|
|
1025
1051
|
]);
|
|
1026
1052
|
// Define which tools are related to wiki and can be toggled by USE_GITLAB_WIKI
|
|
1027
1053
|
export const wikiToolNames = new Set([
|
|
@@ -1082,6 +1108,8 @@ export const TOOLSET_DEFINITIONS = [
|
|
|
1082
1108
|
"approve_merge_request",
|
|
1083
1109
|
"unapprove_merge_request",
|
|
1084
1110
|
"get_merge_request_approval_state",
|
|
1111
|
+
"get_branch",
|
|
1112
|
+
"list_branches",
|
|
1085
1113
|
"get_merge_request_conflicts",
|
|
1086
1114
|
"list_merge_request_pipelines",
|
|
1087
1115
|
"get_merge_request",
|
|
@@ -1130,6 +1158,7 @@ export const TOOLSET_DEFINITIONS = [
|
|
|
1130
1158
|
"my_issues",
|
|
1131
1159
|
"get_issue",
|
|
1132
1160
|
"update_issue",
|
|
1161
|
+
"update_issue_description_patch",
|
|
1133
1162
|
"delete_issue",
|
|
1134
1163
|
"list_todos",
|
|
1135
1164
|
"mark_todo_done",
|
|
@@ -1168,6 +1197,9 @@ export const TOOLSET_DEFINITIONS = [
|
|
|
1168
1197
|
isDefault: true,
|
|
1169
1198
|
tools: new Set([
|
|
1170
1199
|
"create_branch",
|
|
1200
|
+
"get_branch",
|
|
1201
|
+
"list_branches",
|
|
1202
|
+
"delete_branch",
|
|
1171
1203
|
"list_commits",
|
|
1172
1204
|
"get_commit",
|
|
1173
1205
|
"get_commit_diff",
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Patch helper for issue description updates.
|
|
3
|
+
* Supports two patch formats:
|
|
4
|
+
* - search_replace: exact text search/replace blocks
|
|
5
|
+
* - unified_diff: standard unified diff via the `diff` library
|
|
6
|
+
*/
|
|
7
|
+
import { applyPatch, createTwoFilesPatch, parsePatch } from "diff";
|
|
8
|
+
/**
|
|
9
|
+
* Parse a search/replace patch string into blocks.
|
|
10
|
+
* Format:
|
|
11
|
+
* <<<<<<< SEARCH
|
|
12
|
+
* text to find
|
|
13
|
+
* =======
|
|
14
|
+
* text to replace with
|
|
15
|
+
* >>>>>>> REPLACE
|
|
16
|
+
*
|
|
17
|
+
* Supports multiple blocks.
|
|
18
|
+
*/
|
|
19
|
+
export function parseSearchReplaceBlocks(patch) {
|
|
20
|
+
const blocks = [];
|
|
21
|
+
// Match SEARCH...REPLACE blocks (greedy multiline)
|
|
22
|
+
const regex = /<<<<<<< SEARCH[^\S\n]*\n([\s\S]*?)=======[^\S\n]*\n([\s\S]*?)>>>>>>> REPLACE[^\S\n]*/g;
|
|
23
|
+
let match;
|
|
24
|
+
while ((match = regex.exec(patch)) !== null) {
|
|
25
|
+
let search = match[1];
|
|
26
|
+
let replace = match[2];
|
|
27
|
+
// Trim trailing newline from each side (the \n before ======= and before >>>>>>>)
|
|
28
|
+
if (search.endsWith("\n"))
|
|
29
|
+
search = search.slice(0, -1);
|
|
30
|
+
if (replace.endsWith("\n"))
|
|
31
|
+
replace = replace.slice(0, -1);
|
|
32
|
+
blocks.push({ search, replace });
|
|
33
|
+
}
|
|
34
|
+
// Detect malformed blocks: check if SEARCH/REPLACE markers exist but weren't captured
|
|
35
|
+
const searchMarkers = (patch.match(/<<<<<<<\s+SEARCH/g) || []).length;
|
|
36
|
+
const replaceMarkers = (patch.match(/>>>>>>>\s+REPLACE/g) || []).length;
|
|
37
|
+
if (searchMarkers !== blocks.length || replaceMarkers !== blocks.length) {
|
|
38
|
+
throw new Error(`Found ${searchMarkers} SEARCH marker(s) and ${replaceMarkers} REPLACE marker(s), ` +
|
|
39
|
+
`but only parsed ${blocks.length} valid block(s). ` +
|
|
40
|
+
"Some blocks may be malformed (e.g. missing ======= or >>>>>>> REPLACE). " +
|
|
41
|
+
"Each block must follow the exact format: <<<<<<< SEARCH\ntext\n=======\nnew text\n>>>>>>> REPLACE");
|
|
42
|
+
}
|
|
43
|
+
return blocks;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Apply search/replace blocks to source text.
|
|
47
|
+
*
|
|
48
|
+
* @param source - Current text
|
|
49
|
+
* @param blocks - Search/replace blocks
|
|
50
|
+
* @param allowMultiple - If true, replace all occurrences; if false, fail on duplicate match
|
|
51
|
+
* @returns PatchResult or throws on error
|
|
52
|
+
*/
|
|
53
|
+
export function applySearchReplace(source, blocks, allowMultiple = false) {
|
|
54
|
+
let current = source;
|
|
55
|
+
let totalChanges = 0;
|
|
56
|
+
const changeLines = [];
|
|
57
|
+
for (const block of blocks) {
|
|
58
|
+
// Empty SEARCH body would produce an empty regex that corrupts the description
|
|
59
|
+
if (block.search.length === 0) {
|
|
60
|
+
throw new Error("Empty SEARCH block is not allowed. " +
|
|
61
|
+
"Each SEARCH block must contain the text to find. " +
|
|
62
|
+
"Block:\n---\n" + block.replace.slice(0, 200) + "\n---");
|
|
63
|
+
}
|
|
64
|
+
// Count occurrences
|
|
65
|
+
const escapedSearch = escapeRegex(block.search);
|
|
66
|
+
const regex = new RegExp(escapedSearch, "g");
|
|
67
|
+
const occurrences = current.match(regex);
|
|
68
|
+
if (!occurrences || occurrences.length === 0) {
|
|
69
|
+
throw new Error(`Search text not found in issue description. Search block:\n---\n${block.search.slice(0, 200)}${block.search.length > 200 ? "..." : ""}\n---`);
|
|
70
|
+
}
|
|
71
|
+
if (occurrences.length > 1 && !allowMultiple) {
|
|
72
|
+
throw new Error(`Search text matches ${occurrences.length} times (expected exactly 1). ` +
|
|
73
|
+
"Use 'allow_multiple: true' to replace all occurrences.\n" +
|
|
74
|
+
`Search block:\n---\n${block.search.slice(0, 200)}${block.search.length > 200 ? "..." : ""}\n---`);
|
|
75
|
+
}
|
|
76
|
+
// Apply replacement
|
|
77
|
+
const replacement = block.replace;
|
|
78
|
+
const replaced = current.replace(regex, () => replacement);
|
|
79
|
+
// Detect no-op
|
|
80
|
+
if (replaced === current) {
|
|
81
|
+
throw new Error(`Replacement did not change the description (identical result). Search block:\n---\n${block.search.slice(0, 200)}${block.search.length > 200 ? "..." : ""}\n---`);
|
|
82
|
+
}
|
|
83
|
+
const count = occurrences.length;
|
|
84
|
+
totalChanges += count;
|
|
85
|
+
changeLines.push(`Replaced ${count} occurrence(s): "${truncate(block.search, 60)}" → "${truncate(block.replace, 60)}"`);
|
|
86
|
+
current = replaced;
|
|
87
|
+
}
|
|
88
|
+
const summary = changeLines.join("\n");
|
|
89
|
+
// Generate preview diff
|
|
90
|
+
const preview = createTwoFilesPatch("current", "updated", source, current);
|
|
91
|
+
return {
|
|
92
|
+
description: current,
|
|
93
|
+
changes: totalChanges,
|
|
94
|
+
summary,
|
|
95
|
+
preview,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Apply a unified diff patch to source text.
|
|
100
|
+
* Delegates to the `diff` library's applyPatch.
|
|
101
|
+
*/
|
|
102
|
+
export function applyUnifiedDiff(source, patch) {
|
|
103
|
+
// Validate the patch can be parsed first
|
|
104
|
+
const parsed = parsePatch(patch);
|
|
105
|
+
if (parsed.length === 0 || parsed.every((p) => p.hunks.length === 0)) {
|
|
106
|
+
throw new Error("Could not parse unified diff: no valid hunks found. " +
|
|
107
|
+
"Expected format: '--- old\\n+++ new\\n@@ -line,count +line,count @@\\n context\\n-old\\n+new\\n'");
|
|
108
|
+
}
|
|
109
|
+
const result = applyPatch(source, patch);
|
|
110
|
+
if (result === false) {
|
|
111
|
+
throw new Error("Unified diff could not be applied to the current issue description. " +
|
|
112
|
+
"The diff context may not match. Use 'dry_run: true' to debug.");
|
|
113
|
+
}
|
|
114
|
+
// Detect no-op: patch applied but nothing changed
|
|
115
|
+
if (result === source) {
|
|
116
|
+
throw new Error("Unified diff applied but did not change the issue description. " +
|
|
117
|
+
"The source text already matches the patched result.");
|
|
118
|
+
}
|
|
119
|
+
const changes = parsed.reduce((sum, p) => sum + p.hunks.reduce((hSum, h) => hSum + h.lines.filter((l) => l.startsWith("-") || l.startsWith("+")).length, 0), 0);
|
|
120
|
+
// Generate preview of what actually changed
|
|
121
|
+
const preview = createTwoFilesPatch("current", "updated", source, result);
|
|
122
|
+
// Build summary from hunk headers
|
|
123
|
+
const summaryLines = [];
|
|
124
|
+
for (const p of parsed) {
|
|
125
|
+
for (const hunk of p.hunks) {
|
|
126
|
+
const added = hunk.lines.filter((l) => l.startsWith("+")).length;
|
|
127
|
+
const removed = hunk.lines.filter((l) => l.startsWith("-")).length;
|
|
128
|
+
summaryLines.push(`Hunk at line ${hunk.oldStart}: ${removed} removed, ${added} added`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
description: result,
|
|
133
|
+
changes,
|
|
134
|
+
summary: summaryLines.join("\n"),
|
|
135
|
+
preview,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function escapeRegex(str) {
|
|
139
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
140
|
+
}
|
|
141
|
+
function truncate(str, maxLen) {
|
|
142
|
+
if (str.length <= maxLen)
|
|
143
|
+
return str;
|
|
144
|
+
return str.slice(0, maxLen) + "...";
|
|
145
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zereight/mcp-gitlab",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.12",
|
|
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/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/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",
|
|
@@ -70,6 +70,7 @@
|
|
|
70
70
|
"dependencies": {
|
|
71
71
|
"@modelcontextprotocol/sdk": "^1.24.2",
|
|
72
72
|
"@types/node-fetch": "^2.6.12",
|
|
73
|
+
"diff": "^9.0.0",
|
|
73
74
|
"express": "^5.1.0",
|
|
74
75
|
"fetch-cookie": "^3.1.0",
|
|
75
76
|
"form-data": "^4.0.0",
|