@zereight/mcp-gitlab 2.1.10 → 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 -104
- package/build/index.js +215 -5
- package/build/schemas.js +140 -0
- package/build/test/schema-tests.js +140 -3
- package/build/test/test-issue-description-patch.js +256 -0
- package/build/test/test-merge-request-pipelines.js +106 -0
- package/build/test/test-token-optimizations.js +1 -1
- package/build/test/test-toolset-filtering.js +7 -6
- package/build/test/utils/mock-gitlab-server.js +46 -0
- package/build/test/utils/server-launcher.js +2 -1
- package/build/tools/registry.js +61 -1
- package/build/utils/patch-helper.js +145 -0
- package/package.json +3 -2
|
@@ -0,0 +1,106 @@
|
|
|
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-mr-pipelines-test-token";
|
|
6
|
+
const TEST_PROJECT_ID = "123";
|
|
7
|
+
const TEST_MR_IID = "1";
|
|
8
|
+
async function callTool(toolName, args, env) {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const proc = spawn("node", ["build/index.js"], {
|
|
11
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
12
|
+
env: {
|
|
13
|
+
...process.env,
|
|
14
|
+
...env,
|
|
15
|
+
GITLAB_READ_ONLY_MODE: "true",
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
let output = "";
|
|
19
|
+
let errorOutput = "";
|
|
20
|
+
proc.stdout?.on("data", d => output += d);
|
|
21
|
+
proc.stderr?.on("data", d => errorOutput += d);
|
|
22
|
+
proc.on("close", code => {
|
|
23
|
+
if (code !== 0) {
|
|
24
|
+
reject(new Error(`Process exited with code ${code}: ${errorOutput}`));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const line = output.split("\n").find(l => l.startsWith("{"));
|
|
28
|
+
if (!line) {
|
|
29
|
+
reject(new Error("No JSON output found"));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const response = JSON.parse(line);
|
|
34
|
+
if (response.error) {
|
|
35
|
+
reject(response.error);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const content = response.result?.content?.[0]?.text;
|
|
39
|
+
resolve(content ? JSON.parse(content) : response.result);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
reject(error);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
proc.stdin?.end(JSON.stringify({
|
|
46
|
+
jsonrpc: "2.0",
|
|
47
|
+
id: 1,
|
|
48
|
+
method: "tools/call",
|
|
49
|
+
params: { name: toolName, arguments: args },
|
|
50
|
+
}) + "\n");
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
describe("list_merge_request_pipelines", () => {
|
|
54
|
+
let mockGitLab;
|
|
55
|
+
let mockGitLabUrl;
|
|
56
|
+
let lastQuery = {};
|
|
57
|
+
before(async () => {
|
|
58
|
+
const mockPort = await findMockServerPort(9250);
|
|
59
|
+
mockGitLab = new MockGitLabServer({
|
|
60
|
+
port: mockPort,
|
|
61
|
+
validTokens: [MOCK_TOKEN],
|
|
62
|
+
});
|
|
63
|
+
mockGitLab.addMockHandler("get", `/projects/${TEST_PROJECT_ID}/merge_requests/${TEST_MR_IID}/pipelines`, (req, res) => {
|
|
64
|
+
lastQuery = { ...req.query };
|
|
65
|
+
res.json([
|
|
66
|
+
{
|
|
67
|
+
id: 77,
|
|
68
|
+
sha: "959e04d7c7a30600c894bd3c0cd0e1ce7f42c11d",
|
|
69
|
+
ref: "main",
|
|
70
|
+
status: "success",
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: 78,
|
|
74
|
+
sha: "a59e04d7c7a30600c894bd3c0cd0e1ce7f42c22e",
|
|
75
|
+
ref: "refs/merge-requests/1/head",
|
|
76
|
+
status: "running",
|
|
77
|
+
source: "merge_request_event",
|
|
78
|
+
web_url: "https://gitlab.mock/test/project/-/pipelines/78",
|
|
79
|
+
},
|
|
80
|
+
]);
|
|
81
|
+
});
|
|
82
|
+
await mockGitLab.start();
|
|
83
|
+
mockGitLabUrl = mockGitLab.getUrl();
|
|
84
|
+
});
|
|
85
|
+
after(async () => {
|
|
86
|
+
await mockGitLab.stop();
|
|
87
|
+
});
|
|
88
|
+
test("lists pipelines for a merge request", async () => {
|
|
89
|
+
const pipelines = await callTool("list_merge_request_pipelines", {
|
|
90
|
+
project_id: TEST_PROJECT_ID,
|
|
91
|
+
merge_request_iid: TEST_MR_IID,
|
|
92
|
+
page: 2,
|
|
93
|
+
per_page: 10,
|
|
94
|
+
}, {
|
|
95
|
+
GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
|
|
96
|
+
GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN,
|
|
97
|
+
});
|
|
98
|
+
assert.ok(Array.isArray(pipelines), "Response should be an array");
|
|
99
|
+
assert.strictEqual(pipelines.length, 2);
|
|
100
|
+
assert.strictEqual(pipelines[0].id, "77");
|
|
101
|
+
assert.strictEqual(pipelines[0].status, "success");
|
|
102
|
+
assert.strictEqual(pipelines[1].source, "merge_request_event");
|
|
103
|
+
assert.strictEqual(lastQuery.page, "2");
|
|
104
|
+
assert.strictEqual(lastQuery.per_page, "10");
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -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",
|
|
@@ -16,11 +16,11 @@ const MOCK_PORT_BASE = 9200;
|
|
|
16
16
|
const MCP_PORT_BASE = 3200;
|
|
17
17
|
// Known tool counts per toolset (from TOOLSET_DEFINITIONS)
|
|
18
18
|
const TOOLSET_TOOL_COUNTS = {
|
|
19
|
-
merge_requests:
|
|
20
|
-
issues:
|
|
19
|
+
merge_requests: 41,
|
|
20
|
+
issues: 24,
|
|
21
21
|
repositories: 7,
|
|
22
|
-
branches:
|
|
23
|
-
projects:
|
|
22
|
+
branches: 9,
|
|
23
|
+
projects: 9,
|
|
24
24
|
labels: 5,
|
|
25
25
|
ci: 2,
|
|
26
26
|
pipelines: 19,
|
|
@@ -28,7 +28,7 @@ const TOOLSET_TOOL_COUNTS = {
|
|
|
28
28
|
wiki: 10,
|
|
29
29
|
releases: 7,
|
|
30
30
|
tags: 5,
|
|
31
|
-
users:
|
|
31
|
+
users: 7,
|
|
32
32
|
search: 3,
|
|
33
33
|
workitems: 18,
|
|
34
34
|
webhooks: 3,
|
|
@@ -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
|
{
|
|
@@ -78,11 +78,12 @@ export async function launchServer(config) {
|
|
|
78
78
|
if (!serverProcess.killed) {
|
|
79
79
|
serverProcess.kill("SIGTERM");
|
|
80
80
|
// Force kill if not terminated within 5 seconds
|
|
81
|
-
setTimeout(() => {
|
|
81
|
+
const forceKillTimer = setTimeout(() => {
|
|
82
82
|
if (!serverProcess.killed) {
|
|
83
83
|
serverProcess.kill("SIGKILL");
|
|
84
84
|
}
|
|
85
85
|
}, 5000);
|
|
86
|
+
forceKillTimer.unref();
|
|
86
87
|
}
|
|
87
88
|
},
|
|
88
89
|
};
|
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, 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, GetWebhookEventSchema, GetWikiPageSchema, GetWorkItemSchema, ListCommitsSchema, ListCommitStatusesSchema, ListCustomFieldDefinitionsSchema, ListDeploymentsSchema, ListDraftNotesSchema, ListEnvironmentsSchema, ListEventsSchema, ListGroupIterationsSchema, ListGroupProjectsSchema, ListGroupWikiPagesSchema, ListIssueDiscussionsSchema, ListIssueLinksSchema, ListIssuesSchema, ListJobArtifactsSchema, ListLabelsSchema, ListMergeRequestChangedFilesSchema, ListMergeRequestDiffsSchema, ListMergeRequestDiscussionsSchema, 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
|
{
|
|
@@ -29,6 +29,11 @@ export const allTools = [
|
|
|
29
29
|
description: "Get the conflicts of a merge request",
|
|
30
30
|
inputSchema: toJSONSchema(GetMergeRequestConflictsSchema),
|
|
31
31
|
},
|
|
32
|
+
{
|
|
33
|
+
name: "list_merge_request_pipelines",
|
|
34
|
+
description: "List pipelines for a merge request with pagination",
|
|
35
|
+
inputSchema: toJSONSchema(ListMergeRequestPipelinesSchema),
|
|
36
|
+
},
|
|
32
37
|
{
|
|
33
38
|
name: "execute_graphql",
|
|
34
39
|
description: "Execute a GitLab GraphQL query",
|
|
@@ -79,6 +84,21 @@ export const allTools = [
|
|
|
79
84
|
description: "Create a new branch",
|
|
80
85
|
inputSchema: toJSONSchema(CreateBranchSchema),
|
|
81
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
|
+
},
|
|
82
102
|
{
|
|
83
103
|
name: "get_merge_request",
|
|
84
104
|
description: "Get details of a merge request (mergeRequestIid or branchName required)",
|
|
@@ -311,6 +331,13 @@ export const allTools = [
|
|
|
311
331
|
description: "Update an issue",
|
|
312
332
|
inputSchema: toJSONSchema(UpdateIssueSchema),
|
|
313
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
|
+
},
|
|
314
341
|
{
|
|
315
342
|
name: "delete_issue",
|
|
316
343
|
description: "Delete an issue",
|
|
@@ -631,6 +658,16 @@ export const allTools = [
|
|
|
631
658
|
description: "Get GitLab user details by usernames",
|
|
632
659
|
inputSchema: toJSONSchema(GetUsersSchema),
|
|
633
660
|
},
|
|
661
|
+
{
|
|
662
|
+
name: "get_user",
|
|
663
|
+
description: "Get user details by ID",
|
|
664
|
+
inputSchema: toJSONSchema(GetUserSchema),
|
|
665
|
+
},
|
|
666
|
+
{
|
|
667
|
+
name: "whoami",
|
|
668
|
+
description: "Get current authenticated user details",
|
|
669
|
+
inputSchema: toJSONSchema(WhoAmISchema),
|
|
670
|
+
},
|
|
634
671
|
{
|
|
635
672
|
name: "list_commits",
|
|
636
673
|
description: "List repository commits with filtering options",
|
|
@@ -671,6 +708,11 @@ export const allTools = [
|
|
|
671
708
|
description: "Download an uploaded file from a project (images returned as base64; use local_path to save to disk)",
|
|
672
709
|
inputSchema: toJSONSchema(DownloadAttachmentSchema),
|
|
673
710
|
},
|
|
711
|
+
{
|
|
712
|
+
name: "health_check",
|
|
713
|
+
description: "Verify server status and authentication",
|
|
714
|
+
inputSchema: toJSONSchema(HealthCheckSchema),
|
|
715
|
+
},
|
|
674
716
|
{
|
|
675
717
|
name: "list_events",
|
|
676
718
|
description: "List events for the authenticated user (before/after: YYYY-MM-DD)",
|
|
@@ -882,6 +924,7 @@ export const allTools = [
|
|
|
882
924
|
// Define which tools are read-only
|
|
883
925
|
export const readOnlyTools = new Set([
|
|
884
926
|
"discover_tools",
|
|
927
|
+
"health_check",
|
|
885
928
|
"search_repositories",
|
|
886
929
|
"search_code",
|
|
887
930
|
"search_project_code",
|
|
@@ -895,7 +938,10 @@ export const readOnlyTools = new Set([
|
|
|
895
938
|
"get_merge_request_file_diff",
|
|
896
939
|
"list_merge_request_versions",
|
|
897
940
|
"get_merge_request_version",
|
|
941
|
+
"get_branch",
|
|
942
|
+
"list_branches",
|
|
898
943
|
"get_branch_diffs",
|
|
944
|
+
"list_merge_request_pipelines",
|
|
899
945
|
"get_merge_request_note",
|
|
900
946
|
"get_merge_request_notes",
|
|
901
947
|
"get_draft_note",
|
|
@@ -944,6 +990,8 @@ export const readOnlyTools = new Set([
|
|
|
944
990
|
"list_group_wiki_pages",
|
|
945
991
|
"get_group_wiki_page",
|
|
946
992
|
"get_users",
|
|
993
|
+
"get_user",
|
|
994
|
+
"whoami",
|
|
947
995
|
"list_commits",
|
|
948
996
|
"get_commit",
|
|
949
997
|
"get_commit_diff",
|
|
@@ -996,8 +1044,10 @@ export const destructiveTools = new Set([
|
|
|
996
1044
|
"delete_issue_note_emoji_reaction",
|
|
997
1045
|
"delete_work_item_emoji_reaction",
|
|
998
1046
|
"delete_work_item_note_emoji_reaction",
|
|
1047
|
+
"delete_branch",
|
|
999
1048
|
"merge_merge_request",
|
|
1000
1049
|
"push_files",
|
|
1050
|
+
"delete_branch",
|
|
1001
1051
|
]);
|
|
1002
1052
|
// Define which tools are related to wiki and can be toggled by USE_GITLAB_WIKI
|
|
1003
1053
|
export const wikiToolNames = new Set([
|
|
@@ -1058,7 +1108,10 @@ export const TOOLSET_DEFINITIONS = [
|
|
|
1058
1108
|
"approve_merge_request",
|
|
1059
1109
|
"unapprove_merge_request",
|
|
1060
1110
|
"get_merge_request_approval_state",
|
|
1111
|
+
"get_branch",
|
|
1112
|
+
"list_branches",
|
|
1061
1113
|
"get_merge_request_conflicts",
|
|
1114
|
+
"list_merge_request_pipelines",
|
|
1062
1115
|
"get_merge_request",
|
|
1063
1116
|
"get_merge_request_diffs",
|
|
1064
1117
|
"list_merge_request_changed_files",
|
|
@@ -1105,6 +1158,7 @@ export const TOOLSET_DEFINITIONS = [
|
|
|
1105
1158
|
"my_issues",
|
|
1106
1159
|
"get_issue",
|
|
1107
1160
|
"update_issue",
|
|
1161
|
+
"update_issue_description_patch",
|
|
1108
1162
|
"delete_issue",
|
|
1109
1163
|
"list_todos",
|
|
1110
1164
|
"mark_todo_done",
|
|
@@ -1143,6 +1197,9 @@ export const TOOLSET_DEFINITIONS = [
|
|
|
1143
1197
|
isDefault: true,
|
|
1144
1198
|
tools: new Set([
|
|
1145
1199
|
"create_branch",
|
|
1200
|
+
"get_branch",
|
|
1201
|
+
"list_branches",
|
|
1202
|
+
"delete_branch",
|
|
1146
1203
|
"list_commits",
|
|
1147
1204
|
"get_commit",
|
|
1148
1205
|
"get_commit_diff",
|
|
@@ -1162,6 +1219,7 @@ export const TOOLSET_DEFINITIONS = [
|
|
|
1162
1219
|
"verify_namespace",
|
|
1163
1220
|
"list_group_projects",
|
|
1164
1221
|
"list_group_iterations",
|
|
1222
|
+
"health_check",
|
|
1165
1223
|
]),
|
|
1166
1224
|
},
|
|
1167
1225
|
{
|
|
@@ -1265,6 +1323,8 @@ export const TOOLSET_DEFINITIONS = [
|
|
|
1265
1323
|
isDefault: true,
|
|
1266
1324
|
tools: new Set([
|
|
1267
1325
|
"get_users",
|
|
1326
|
+
"get_user",
|
|
1327
|
+
"whoami",
|
|
1268
1328
|
"list_events",
|
|
1269
1329
|
"get_project_events",
|
|
1270
1330
|
"upload_markdown",
|
|
@@ -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 && 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",
|