@zereight/mcp-gitlab 1.0.63 → 1.0.65
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 +62 -54
- package/build/index.js +248 -2
- package/build/schemas.js +78 -24
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -36,7 +36,9 @@ When using with the Claude App, you need to set up your API key and URLs directl
|
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
#### Docker
|
|
39
|
+
|
|
39
40
|
- stdio
|
|
41
|
+
|
|
40
42
|
```mcp.json
|
|
41
43
|
{
|
|
42
44
|
"mcpServers": {
|
|
@@ -74,6 +76,7 @@ When using with the Claude App, you need to set up your API key and URLs directl
|
|
|
74
76
|
```
|
|
75
77
|
|
|
76
78
|
- sse
|
|
79
|
+
|
|
77
80
|
```shell
|
|
78
81
|
docker run -i --rm \
|
|
79
82
|
-e GITLAB_PERSONAL_ACCESS_TOKEN=your_gitlab_token \
|
|
@@ -111,10 +114,14 @@ $ sh scripts/image_push.sh docker_user_name
|
|
|
111
114
|
- `USE_GITLAB_WIKI`: When set to 'true', enables the wiki-related tools (list_wiki_pages, get_wiki_page, create_wiki_page, update_wiki_page, delete_wiki_page). By default, wiki features are disabled.
|
|
112
115
|
- `USE_MILESTONE`: When set to 'true', enables the milestone-related tools (list_milestones, get_milestone, create_milestone, edit_milestone, delete_milestone, get_milestone_issue, get_milestone_merge_requests, promote_milestone, get_milestone_burndown_events). By default, milestone features are disabled.
|
|
113
116
|
- `USE_PIPELINE`: When set to 'true', enables the pipeline-related tools (list_pipelines, get_pipeline, list_pipeline_jobs, get_pipeline_job, get_pipeline_job_output, create_pipeline, retry_pipeline, cancel_pipeline). By default, pipeline features are disabled.
|
|
117
|
+
- `GITLAB_AUTH_COOKIE_PATH`: Path to an authentication cookie file for GitLab instances that require cookie-based authentication. When provided, the cookie will be included in all GitLab API requests.
|
|
118
|
+
|
|
119
|
+
[](https://www.star-history.com/#zereight/gitlab-mcp&Date)
|
|
114
120
|
|
|
115
121
|
## Tools 🛠️
|
|
116
122
|
|
|
117
123
|
+<!-- TOOLS-START -->
|
|
124
|
+
|
|
118
125
|
1. `create_or_update_file` - Create or update a single file in a GitLab project
|
|
119
126
|
2. `search_repositories` - Search for GitLab projects
|
|
120
127
|
3. `create_repository` - Create a new GitLab project
|
|
@@ -126,58 +133,59 @@ $ sh scripts/image_push.sh docker_user_name
|
|
|
126
133
|
9. `create_branch` - Create a new branch in a GitLab project
|
|
127
134
|
10. `get_merge_request` - Get details of a merge request (Either mergeRequestIid or branchName must be provided)
|
|
128
135
|
11. `get_merge_request_diffs` - Get the changes/diffs of a merge request (Either mergeRequestIid or branchName must be provided)
|
|
129
|
-
12. `
|
|
130
|
-
13. `
|
|
131
|
-
14. `
|
|
132
|
-
15. `
|
|
133
|
-
16. `
|
|
134
|
-
17. `
|
|
135
|
-
18. `
|
|
136
|
-
19. `
|
|
137
|
-
20. `
|
|
138
|
-
21. `
|
|
139
|
-
22. `
|
|
140
|
-
23. `
|
|
141
|
-
24. `
|
|
142
|
-
25. `
|
|
143
|
-
26. `
|
|
144
|
-
27. `
|
|
145
|
-
28. `
|
|
146
|
-
29. `
|
|
147
|
-
30. `
|
|
148
|
-
31. `
|
|
149
|
-
32. `
|
|
150
|
-
33. `
|
|
151
|
-
34. `
|
|
152
|
-
35. `
|
|
153
|
-
36. `
|
|
154
|
-
37. `
|
|
155
|
-
38. `
|
|
156
|
-
39. `
|
|
157
|
-
40. `
|
|
158
|
-
41. `
|
|
159
|
-
42. `
|
|
160
|
-
43. `
|
|
161
|
-
44. `
|
|
162
|
-
45. `
|
|
163
|
-
46. `
|
|
164
|
-
47. `
|
|
165
|
-
48. `
|
|
166
|
-
49. `
|
|
167
|
-
50. `
|
|
168
|
-
51. `
|
|
169
|
-
52. `
|
|
170
|
-
53. `
|
|
171
|
-
54. `
|
|
172
|
-
55. `
|
|
173
|
-
56. `
|
|
174
|
-
57. `
|
|
175
|
-
58. `
|
|
176
|
-
59. `
|
|
177
|
-
60. `
|
|
178
|
-
61. `
|
|
179
|
-
62. `
|
|
180
|
-
63. `
|
|
181
|
-
64. `
|
|
182
|
-
65. `
|
|
136
|
+
12. `list_merge_request_diffs` - List merge request diffs with pagination support (Either mergeRequestIid or branchName must be provided)
|
|
137
|
+
13. `get_branch_diffs` - Get the changes/diffs between two branches or commits in a GitLab project
|
|
138
|
+
14. `update_merge_request` - Update a merge request (Either mergeRequestIid or branchName must be provided)
|
|
139
|
+
15. `create_note` - Create a new note (comment) to an issue or merge request
|
|
140
|
+
16. `create_merge_request_thread` - Create a new thread on a merge request
|
|
141
|
+
17. `mr_discussions` - List discussion items for a merge request
|
|
142
|
+
18. `update_merge_request_note` - Modify an existing merge request thread note
|
|
143
|
+
19. `create_merge_request_note` - Add a new note to an existing merge request thread
|
|
144
|
+
20. `update_issue_note` - Modify an existing issue thread note
|
|
145
|
+
21. `create_issue_note` - Add a new note to an existing issue thread
|
|
146
|
+
22. `list_issues` - List issues in a GitLab project with filtering options
|
|
147
|
+
23. `get_issue` - Get details of a specific issue in a GitLab project
|
|
148
|
+
24. `update_issue` - Update an issue in a GitLab project
|
|
149
|
+
25. `delete_issue` - Delete an issue from a GitLab project
|
|
150
|
+
26. `list_issue_links` - List all issue links for a specific issue
|
|
151
|
+
27. `list_issue_discussions` - List discussions for an issue in a GitLab project
|
|
152
|
+
28. `get_issue_link` - Get a specific issue link
|
|
153
|
+
29. `create_issue_link` - Create an issue link between two issues
|
|
154
|
+
30. `delete_issue_link` - Delete an issue link
|
|
155
|
+
31. `list_namespaces` - List all namespaces available to the current user
|
|
156
|
+
32. `get_namespace` - Get details of a namespace by ID or path
|
|
157
|
+
33. `verify_namespace` - Verify if a namespace path exists
|
|
158
|
+
34. `get_project` - Get details of a specific project
|
|
159
|
+
35. `list_projects` - List projects accessible by the current user
|
|
160
|
+
36. `list_labels` - List labels for a project
|
|
161
|
+
37. `get_label` - Get a single label from a project
|
|
162
|
+
38. `create_label` - Create a new label in a project
|
|
163
|
+
39. `update_label` - Update an existing label in a project
|
|
164
|
+
40. `delete_label` - Delete a label from a project
|
|
165
|
+
41. `list_group_projects` - List projects in a GitLab group with filtering options
|
|
166
|
+
42. `list_wiki_pages` - List wiki pages in a GitLab project
|
|
167
|
+
43. `get_wiki_page` - Get details of a specific wiki page
|
|
168
|
+
44. `create_wiki_page` - Create a new wiki page in a GitLab project
|
|
169
|
+
45. `update_wiki_page` - Update an existing wiki page in a GitLab project
|
|
170
|
+
46. `delete_wiki_page` - Delete a wiki page from a GitLab project
|
|
171
|
+
47. `get_repository_tree` - Get the repository tree for a GitLab project (list files and directories)
|
|
172
|
+
48. `list_pipelines` - List pipelines in a GitLab project with filtering options
|
|
173
|
+
49. `get_pipeline` - Get details of a specific pipeline in a GitLab project
|
|
174
|
+
50. `list_pipeline_jobs` - List all jobs in a specific pipeline
|
|
175
|
+
51. `get_pipeline_job` - Get details of a GitLab pipeline job number
|
|
176
|
+
52. `get_pipeline_job_output` - Get the output/trace of a GitLab pipeline job number
|
|
177
|
+
53. `create_pipeline` - Create a new pipeline for a branch or tag
|
|
178
|
+
54. `retry_pipeline` - Retry a failed or canceled pipeline
|
|
179
|
+
55. `cancel_pipeline` - Cancel a running pipeline
|
|
180
|
+
56. `list_merge_requests` - List merge requests in a GitLab project with filtering options
|
|
181
|
+
57. `list_milestones` - List milestones in a GitLab project with filtering options
|
|
182
|
+
58. `get_milestone` - Get details of a specific milestone
|
|
183
|
+
59. `create_milestone` - Create a new milestone in a GitLab project
|
|
184
|
+
60. `edit_milestone` - Edit an existing milestone in a GitLab project
|
|
185
|
+
61. `delete_milestone` - Delete a milestone from a GitLab project
|
|
186
|
+
62. `get_milestone_issue` - Get issues associated with a specific milestone
|
|
187
|
+
63. `get_milestone_merge_requests` - Get merge requests associated with a specific milestone
|
|
188
|
+
64. `promote_milestone` - Promote a milestone to the next stage
|
|
189
|
+
65. `get_milestone_burndown_events` - Get burndown events for a specific milestone
|
|
190
|
+
66. `get_users` - Get GitLab user details by usernames
|
|
183
191
|
<!-- TOOLS-END -->
|
package/build/index.js
CHANGED
|
@@ -3,7 +3,9 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
5
5
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
6
|
-
import
|
|
6
|
+
import nodeFetch from "node-fetch";
|
|
7
|
+
import fetchCookie from "fetch-cookie";
|
|
8
|
+
import { CookieJar, parse as parseCookie } from "tough-cookie";
|
|
7
9
|
import { SocksProxyAgent } from "socks-proxy-agent";
|
|
8
10
|
import { HttpsProxyAgent } from "https-proxy-agent";
|
|
9
11
|
import { HttpProxyAgent } from "http-proxy-agent";
|
|
@@ -25,7 +27,7 @@ GetPipelineJobOutputSchema, GitLabPipelineJobSchema,
|
|
|
25
27
|
GitLabDiscussionNoteSchema, // Added
|
|
26
28
|
GitLabDiscussionSchema, PaginatedDiscussionsResponseSchema, UpdateMergeRequestNoteSchema, // Added
|
|
27
29
|
CreateMergeRequestNoteSchema, // Added
|
|
28
|
-
ListMergeRequestDiscussionsSchema, UpdateIssueNoteSchema, CreateIssueNoteSchema, ListMergeRequestsSchema, GitLabMilestonesSchema, ListProjectMilestonesSchema, GetProjectMilestoneSchema, CreateProjectMilestoneSchema, EditProjectMilestoneSchema, DeleteProjectMilestoneSchema, GetMilestoneIssuesSchema, GetMilestoneMergeRequestsSchema, PromoteProjectMilestoneSchema, GetMilestoneBurndownEventsSchema, GitLabCompareResultSchema, GetBranchDiffsSchema, } from "./schemas.js";
|
|
30
|
+
ListMergeRequestDiscussionsSchema, UpdateIssueNoteSchema, CreateIssueNoteSchema, ListMergeRequestsSchema, GitLabMilestonesSchema, ListProjectMilestonesSchema, GetProjectMilestoneSchema, CreateProjectMilestoneSchema, EditProjectMilestoneSchema, DeleteProjectMilestoneSchema, GetMilestoneIssuesSchema, GetMilestoneMergeRequestsSchema, PromoteProjectMilestoneSchema, GetMilestoneBurndownEventsSchema, GitLabCompareResultSchema, GetBranchDiffsSchema, ListCommitsSchema, GetCommitSchema, GetCommitDiffSchema, ListMergeRequestDiffsSchema, } from "./schemas.js";
|
|
29
31
|
/**
|
|
30
32
|
* Read version from package.json
|
|
31
33
|
*/
|
|
@@ -51,6 +53,7 @@ const server = new Server({
|
|
|
51
53
|
},
|
|
52
54
|
});
|
|
53
55
|
const GITLAB_PERSONAL_ACCESS_TOKEN = process.env.GITLAB_PERSONAL_ACCESS_TOKEN;
|
|
56
|
+
const GITLAB_AUTH_COOKIE_PATH = process.env.GITLAB_AUTH_COOKIE_PATH;
|
|
54
57
|
const IS_OLD = process.env.GITLAB_IS_OLD === "true";
|
|
55
58
|
const GITLAB_READ_ONLY_MODE = process.env.GITLAB_READ_ONLY_MODE === "true";
|
|
56
59
|
const USE_GITLAB_WIKI = process.env.USE_GITLAB_WIKI === "true";
|
|
@@ -91,6 +94,76 @@ if (HTTPS_PROXY) {
|
|
|
91
94
|
}
|
|
92
95
|
httpsAgent = httpsAgent || new HttpsAgent(sslOptions);
|
|
93
96
|
httpAgent = httpAgent || new Agent();
|
|
97
|
+
// Create cookie jar with clean Netscape file parsing
|
|
98
|
+
const createCookieJar = () => {
|
|
99
|
+
if (!GITLAB_AUTH_COOKIE_PATH)
|
|
100
|
+
return null;
|
|
101
|
+
try {
|
|
102
|
+
const cookiePath = GITLAB_AUTH_COOKIE_PATH.startsWith("~/")
|
|
103
|
+
? path.join(process.env.HOME || "", GITLAB_AUTH_COOKIE_PATH.slice(2))
|
|
104
|
+
: GITLAB_AUTH_COOKIE_PATH;
|
|
105
|
+
const jar = new CookieJar();
|
|
106
|
+
const cookieContent = fs.readFileSync(cookiePath, "utf8");
|
|
107
|
+
cookieContent.split("\n").forEach(line => {
|
|
108
|
+
// Handle #HttpOnly_ prefix
|
|
109
|
+
if (line.startsWith("#HttpOnly_")) {
|
|
110
|
+
line = line.slice(10);
|
|
111
|
+
}
|
|
112
|
+
// Skip comments and empty lines
|
|
113
|
+
if (line.startsWith("#") || !line.trim()) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
// Parse Netscape format: domain, flag, path, secure, expires, name, value
|
|
117
|
+
const parts = line.split("\t");
|
|
118
|
+
if (parts.length >= 7) {
|
|
119
|
+
const [domain, , path, secure, expires, name, value] = parts;
|
|
120
|
+
// Build cookie string in standard format
|
|
121
|
+
const cookieStr = `${name}=${value}; Domain=${domain}; Path=${path}${secure === "TRUE" ? "; Secure" : ""}${expires !== "0" ? `; Expires=${new Date(parseInt(expires) * 1000).toUTCString()}` : ""}`;
|
|
122
|
+
// Use tough-cookie's parse function for robust parsing
|
|
123
|
+
const cookie = parseCookie(cookieStr);
|
|
124
|
+
if (cookie) {
|
|
125
|
+
const url = `${secure === "TRUE" ? "https" : "http"}://${domain.startsWith(".") ? domain.slice(1) : domain}`;
|
|
126
|
+
jar.setCookieSync(cookie, url);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
return jar;
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
console.error("Error loading cookie file:", error);
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
// Initialize cookie jar and fetch
|
|
138
|
+
const cookieJar = createCookieJar();
|
|
139
|
+
const fetch = cookieJar ? fetchCookie(nodeFetch, cookieJar) : nodeFetch;
|
|
140
|
+
// Ensure session is established for the current request
|
|
141
|
+
async function ensureSessionForRequest() {
|
|
142
|
+
if (!cookieJar || !GITLAB_AUTH_COOKIE_PATH)
|
|
143
|
+
return;
|
|
144
|
+
// Extract the base URL from GITLAB_API_URL
|
|
145
|
+
const apiUrl = new URL(GITLAB_API_URL);
|
|
146
|
+
const baseUrl = `${apiUrl.protocol}//${apiUrl.hostname}`;
|
|
147
|
+
// Check if we already have GitLab session cookies
|
|
148
|
+
const gitlabCookies = cookieJar.getCookiesSync(baseUrl);
|
|
149
|
+
const hasSessionCookie = gitlabCookies.some(cookie => cookie.key === '_gitlab_session' || cookie.key === 'remember_user_token');
|
|
150
|
+
if (!hasSessionCookie) {
|
|
151
|
+
try {
|
|
152
|
+
// Establish session with a lightweight request
|
|
153
|
+
await fetch(`${GITLAB_API_URL}/user`, {
|
|
154
|
+
...DEFAULT_FETCH_CONFIG,
|
|
155
|
+
redirect: 'follow'
|
|
156
|
+
}).catch(() => {
|
|
157
|
+
// Ignore errors - the important thing is that cookies get set during redirects
|
|
158
|
+
});
|
|
159
|
+
// Small delay to ensure cookies are fully processed
|
|
160
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
// Ignore session establishment errors
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
94
167
|
// Modify DEFAULT_HEADERS to include agent configuration
|
|
95
168
|
const DEFAULT_HEADERS = {
|
|
96
169
|
Accept: "application/json",
|
|
@@ -169,6 +242,11 @@ const allTools = [
|
|
|
169
242
|
description: "Get the changes/diffs of a merge request (Either mergeRequestIid or branchName must be provided)",
|
|
170
243
|
inputSchema: zodToJsonSchema(GetMergeRequestDiffsSchema),
|
|
171
244
|
},
|
|
245
|
+
{
|
|
246
|
+
name: "list_merge_request_diffs",
|
|
247
|
+
description: "List merge request diffs with pagination support (Either mergeRequestIid or branchName must be provided)",
|
|
248
|
+
inputSchema: zodToJsonSchema(ListMergeRequestDiffsSchema),
|
|
249
|
+
},
|
|
172
250
|
{
|
|
173
251
|
name: "get_branch_diffs",
|
|
174
252
|
description: "Get the changes/diffs between two branches or commits in a GitLab project",
|
|
@@ -439,6 +517,21 @@ const allTools = [
|
|
|
439
517
|
description: "Get GitLab user details by usernames",
|
|
440
518
|
inputSchema: zodToJsonSchema(GetUsersSchema),
|
|
441
519
|
},
|
|
520
|
+
{
|
|
521
|
+
name: "list_commits",
|
|
522
|
+
description: "List repository commits with filtering options",
|
|
523
|
+
inputSchema: zodToJsonSchema(ListCommitsSchema),
|
|
524
|
+
},
|
|
525
|
+
{
|
|
526
|
+
name: "get_commit",
|
|
527
|
+
description: "Get details of a specific commit",
|
|
528
|
+
inputSchema: zodToJsonSchema(GetCommitSchema),
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
name: "get_commit_diff",
|
|
532
|
+
description: "Get changes/diffs of a specific commit",
|
|
533
|
+
inputSchema: zodToJsonSchema(GetCommitDiffSchema),
|
|
534
|
+
},
|
|
442
535
|
];
|
|
443
536
|
// Define which tools are read-only
|
|
444
537
|
const readOnlyTools = [
|
|
@@ -476,6 +569,9 @@ const readOnlyTools = [
|
|
|
476
569
|
"list_wiki_pages",
|
|
477
570
|
"get_wiki_page",
|
|
478
571
|
"get_users",
|
|
572
|
+
"list_commits",
|
|
573
|
+
"get_commit",
|
|
574
|
+
"get_commit_diff",
|
|
479
575
|
];
|
|
480
576
|
// Define which tools are related to wiki and can be toggled by USE_GITLAB_WIKI
|
|
481
577
|
const wikiToolNames = [
|
|
@@ -1383,6 +1479,41 @@ async function getMergeRequestDiffs(projectId, mergeRequestIid, branchName, view
|
|
|
1383
1479
|
const data = (await response.json());
|
|
1384
1480
|
return z.array(GitLabDiffSchema).parse(data.changes);
|
|
1385
1481
|
}
|
|
1482
|
+
/**
|
|
1483
|
+
* Get merge request changes with detailed information including commits, diff_refs, and more
|
|
1484
|
+
* 마지막으로 추가된 상세한 MR 변경사항 조회 함수 (Detailed merge request changes retrieval function)
|
|
1485
|
+
*
|
|
1486
|
+
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
1487
|
+
* @param {number} mergeRequestIid - The internal ID of the merge request (Either mergeRequestIid or branchName must be provided)
|
|
1488
|
+
* @param {string} [branchName] - The name of the branch to search for merge request by branch name (Either mergeRequestIid or branchName must be provided)
|
|
1489
|
+
* @param {boolean} [unidiff] - Return diff in unidiff format
|
|
1490
|
+
* @returns {Promise<any>} The complete merge request changes response
|
|
1491
|
+
*/
|
|
1492
|
+
async function listMergeRequestDiffs(projectId, mergeRequestIid, branchName, page, perPage, unidiff) {
|
|
1493
|
+
projectId = decodeURIComponent(projectId); // Decode project ID
|
|
1494
|
+
if (!mergeRequestIid && !branchName) {
|
|
1495
|
+
throw new Error("Either mergeRequestIid or branchName must be provided");
|
|
1496
|
+
}
|
|
1497
|
+
if (branchName && !mergeRequestIid) {
|
|
1498
|
+
const mergeRequest = await getMergeRequest(projectId, undefined, branchName);
|
|
1499
|
+
mergeRequestIid = mergeRequest.iid;
|
|
1500
|
+
}
|
|
1501
|
+
const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/merge_requests/${mergeRequestIid}/diffs`);
|
|
1502
|
+
if (page) {
|
|
1503
|
+
url.searchParams.append("page", page.toString());
|
|
1504
|
+
}
|
|
1505
|
+
if (perPage) {
|
|
1506
|
+
url.searchParams.append("per_page", perPage.toString());
|
|
1507
|
+
}
|
|
1508
|
+
if (unidiff) {
|
|
1509
|
+
url.searchParams.append("unidiff", "true");
|
|
1510
|
+
}
|
|
1511
|
+
const response = await fetch(url.toString(), {
|
|
1512
|
+
...DEFAULT_FETCH_CONFIG,
|
|
1513
|
+
});
|
|
1514
|
+
await handleGitLabError(response);
|
|
1515
|
+
return await response.json(); // Return full response including commits, diff_refs, changes, etc.
|
|
1516
|
+
}
|
|
1386
1517
|
/**
|
|
1387
1518
|
* Get branch comparison diffs
|
|
1388
1519
|
*
|
|
@@ -2322,6 +2453,89 @@ async function getUsers(usernames) {
|
|
|
2322
2453
|
}
|
|
2323
2454
|
return GitLabUsersResponseSchema.parse(users);
|
|
2324
2455
|
}
|
|
2456
|
+
/**
|
|
2457
|
+
* List repository commits
|
|
2458
|
+
* 저장소 커밋 목록 조회
|
|
2459
|
+
*
|
|
2460
|
+
* @param {string} projectId - Project ID or URL-encoded path
|
|
2461
|
+
* @param {ListCommitsOptions} options - List commits options
|
|
2462
|
+
* @returns {Promise<GitLabCommit[]>} List of commits
|
|
2463
|
+
*/
|
|
2464
|
+
async function listCommits(projectId, options = {}) {
|
|
2465
|
+
projectId = decodeURIComponent(projectId);
|
|
2466
|
+
const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/commits`);
|
|
2467
|
+
// Add query parameters
|
|
2468
|
+
if (options.ref_name)
|
|
2469
|
+
url.searchParams.append("ref_name", options.ref_name);
|
|
2470
|
+
if (options.since)
|
|
2471
|
+
url.searchParams.append("since", options.since);
|
|
2472
|
+
if (options.until)
|
|
2473
|
+
url.searchParams.append("until", options.until);
|
|
2474
|
+
if (options.path)
|
|
2475
|
+
url.searchParams.append("path", options.path);
|
|
2476
|
+
if (options.author)
|
|
2477
|
+
url.searchParams.append("author", options.author);
|
|
2478
|
+
if (options.all)
|
|
2479
|
+
url.searchParams.append("all", options.all.toString());
|
|
2480
|
+
if (options.with_stats)
|
|
2481
|
+
url.searchParams.append("with_stats", options.with_stats.toString());
|
|
2482
|
+
if (options.first_parent)
|
|
2483
|
+
url.searchParams.append("first_parent", options.first_parent.toString());
|
|
2484
|
+
if (options.order)
|
|
2485
|
+
url.searchParams.append("order", options.order);
|
|
2486
|
+
if (options.trailers)
|
|
2487
|
+
url.searchParams.append("trailers", options.trailers.toString());
|
|
2488
|
+
if (options.page)
|
|
2489
|
+
url.searchParams.append("page", options.page.toString());
|
|
2490
|
+
if (options.per_page)
|
|
2491
|
+
url.searchParams.append("per_page", options.per_page.toString());
|
|
2492
|
+
const response = await fetch(url.toString(), {
|
|
2493
|
+
...DEFAULT_FETCH_CONFIG,
|
|
2494
|
+
});
|
|
2495
|
+
await handleGitLabError(response);
|
|
2496
|
+
const data = await response.json();
|
|
2497
|
+
return z.array(GitLabCommitSchema).parse(data);
|
|
2498
|
+
}
|
|
2499
|
+
/**
|
|
2500
|
+
* Get a single commit
|
|
2501
|
+
* 단일 커밋 정보 조회
|
|
2502
|
+
*
|
|
2503
|
+
* @param {string} projectId - Project ID or URL-encoded path
|
|
2504
|
+
* @param {string} sha - The commit hash or name of a repository branch or tag
|
|
2505
|
+
* @param {boolean} [stats] - Include commit stats
|
|
2506
|
+
* @returns {Promise<GitLabCommit>} The commit details
|
|
2507
|
+
*/
|
|
2508
|
+
async function getCommit(projectId, sha, stats) {
|
|
2509
|
+
projectId = decodeURIComponent(projectId);
|
|
2510
|
+
const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/commits/${encodeURIComponent(sha)}`);
|
|
2511
|
+
if (stats) {
|
|
2512
|
+
url.searchParams.append("stats", "true");
|
|
2513
|
+
}
|
|
2514
|
+
const response = await fetch(url.toString(), {
|
|
2515
|
+
...DEFAULT_FETCH_CONFIG,
|
|
2516
|
+
});
|
|
2517
|
+
await handleGitLabError(response);
|
|
2518
|
+
const data = await response.json();
|
|
2519
|
+
return GitLabCommitSchema.parse(data);
|
|
2520
|
+
}
|
|
2521
|
+
/**
|
|
2522
|
+
* Get commit diff
|
|
2523
|
+
* 커밋 변경사항 조회
|
|
2524
|
+
*
|
|
2525
|
+
* @param {string} projectId - Project ID or URL-encoded path
|
|
2526
|
+
* @param {string} sha - The commit hash or name of a repository branch or tag
|
|
2527
|
+
* @returns {Promise<GitLabMergeRequestDiff[]>} The commit diffs
|
|
2528
|
+
*/
|
|
2529
|
+
async function getCommitDiff(projectId, sha) {
|
|
2530
|
+
projectId = decodeURIComponent(projectId);
|
|
2531
|
+
const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/commits/${encodeURIComponent(sha)}/diff`);
|
|
2532
|
+
const response = await fetch(url.toString(), {
|
|
2533
|
+
...DEFAULT_FETCH_CONFIG,
|
|
2534
|
+
});
|
|
2535
|
+
await handleGitLabError(response);
|
|
2536
|
+
const data = await response.json();
|
|
2537
|
+
return z.array(GitLabDiffSchema).parse(data);
|
|
2538
|
+
}
|
|
2325
2539
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
2326
2540
|
// Apply read-only filter first
|
|
2327
2541
|
const tools0 = GITLAB_READ_ONLY_MODE
|
|
@@ -2362,6 +2576,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2362
2576
|
if (!request.params.arguments) {
|
|
2363
2577
|
throw new Error("Arguments are required");
|
|
2364
2578
|
}
|
|
2579
|
+
// Ensure session is established for every request if cookie authentication is enabled
|
|
2580
|
+
if (GITLAB_AUTH_COOKIE_PATH) {
|
|
2581
|
+
await ensureSessionForRequest();
|
|
2582
|
+
}
|
|
2365
2583
|
switch (request.params.name) {
|
|
2366
2584
|
case "fork_repository": {
|
|
2367
2585
|
const forkArgs = ForkRepositorySchema.parse(request.params.arguments);
|
|
@@ -2514,6 +2732,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2514
2732
|
content: [{ type: "text", text: JSON.stringify(diffs, null, 2) }],
|
|
2515
2733
|
};
|
|
2516
2734
|
}
|
|
2735
|
+
case "list_merge_request_diffs": {
|
|
2736
|
+
const args = ListMergeRequestDiffsSchema.parse(request.params.arguments);
|
|
2737
|
+
const changes = await listMergeRequestDiffs(args.project_id, args.merge_request_iid, args.source_branch, args.page, args.per_page, args.unidiff);
|
|
2738
|
+
return {
|
|
2739
|
+
content: [{ type: "text", text: JSON.stringify(changes, null, 2) }],
|
|
2740
|
+
};
|
|
2741
|
+
}
|
|
2517
2742
|
case "update_merge_request": {
|
|
2518
2743
|
const args = UpdateMergeRequestSchema.parse(request.params.arguments);
|
|
2519
2744
|
const { project_id, merge_request_iid, source_branch, ...options } = args;
|
|
@@ -3011,6 +3236,27 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3011
3236
|
],
|
|
3012
3237
|
};
|
|
3013
3238
|
}
|
|
3239
|
+
case "list_commits": {
|
|
3240
|
+
const args = ListCommitsSchema.parse(request.params.arguments);
|
|
3241
|
+
const commits = await listCommits(args.project_id, args);
|
|
3242
|
+
return {
|
|
3243
|
+
content: [{ type: "text", text: JSON.stringify(commits, null, 2) }],
|
|
3244
|
+
};
|
|
3245
|
+
}
|
|
3246
|
+
case "get_commit": {
|
|
3247
|
+
const args = GetCommitSchema.parse(request.params.arguments);
|
|
3248
|
+
const commit = await getCommit(args.project_id, args.sha, args.stats);
|
|
3249
|
+
return {
|
|
3250
|
+
content: [{ type: "text", text: JSON.stringify(commit, null, 2) }],
|
|
3251
|
+
};
|
|
3252
|
+
}
|
|
3253
|
+
case "get_commit_diff": {
|
|
3254
|
+
const args = GetCommitDiffSchema.parse(request.params.arguments);
|
|
3255
|
+
const diff = await getCommitDiff(args.project_id, args.sha);
|
|
3256
|
+
return {
|
|
3257
|
+
content: [{ type: "text", text: JSON.stringify(diff, null, 2) }],
|
|
3258
|
+
};
|
|
3259
|
+
}
|
|
3014
3260
|
default:
|
|
3015
3261
|
throw new Error(`Unknown tool: ${request.params.name}`);
|
|
3016
3262
|
}
|
package/build/schemas.js
CHANGED
|
@@ -274,14 +274,14 @@ export const GitLabRepositorySchema = z.object({
|
|
|
274
274
|
project_access: z
|
|
275
275
|
.object({
|
|
276
276
|
access_level: z.number(),
|
|
277
|
-
notification_level: z.number().optional(),
|
|
277
|
+
notification_level: z.number().nullable().optional(),
|
|
278
278
|
})
|
|
279
279
|
.optional()
|
|
280
280
|
.nullable(),
|
|
281
281
|
group_access: z
|
|
282
282
|
.object({
|
|
283
283
|
access_level: z.number(),
|
|
284
|
-
notification_level: z.number().optional(),
|
|
284
|
+
notification_level: z.number().nullable().optional(),
|
|
285
285
|
})
|
|
286
286
|
.optional()
|
|
287
287
|
.nullable(),
|
|
@@ -374,8 +374,17 @@ export const GitLabCommitSchema = z.object({
|
|
|
374
374
|
committer_name: z.string(),
|
|
375
375
|
committer_email: z.string(),
|
|
376
376
|
committed_date: z.string(),
|
|
377
|
+
created_at: z.string().optional(), // Add created_at field
|
|
378
|
+
message: z.string().optional(), // Add full message field
|
|
377
379
|
web_url: z.string(), // Changed from html_url to match GitLab API
|
|
378
380
|
parent_ids: z.array(z.string()), // Changed from parents to match GitLab API
|
|
381
|
+
stats: z.object({
|
|
382
|
+
additions: z.number().optional().nullable(),
|
|
383
|
+
deletions: z.number().optional().nullable(),
|
|
384
|
+
total: z.number().optional().nullable(),
|
|
385
|
+
}).optional(), // Only present when with_stats=true
|
|
386
|
+
trailers: z.record(z.string()).optional().default({}), // Git trailers, may be empty object
|
|
387
|
+
extended_trailers: z.record(z.array(z.string())).optional().default({}), // Extended trailers, may be empty object
|
|
379
388
|
});
|
|
380
389
|
// Reference schema
|
|
381
390
|
export const GitLabReferenceSchema = z.object({
|
|
@@ -596,6 +605,20 @@ export const GitLabMergeRequestSchema = z.object({
|
|
|
596
605
|
squash: z.boolean().optional(),
|
|
597
606
|
labels: z.array(z.string()).optional(),
|
|
598
607
|
});
|
|
608
|
+
export const LineRangeSchema = z.object({
|
|
609
|
+
start: z.object({
|
|
610
|
+
line_code: z.string().nullable().optional().describe("CRITICAL: Line identifier in format '{file_path_sha1_hash}_{old_line_number}_{new_line_number}'. USUALLY REQUIRED for GitLab diff comments despite being optional in schema. Example: 'a1b2c3d4e5f6_10_15'. Get this from GitLab diff API response, never fabricate."),
|
|
611
|
+
type: z.enum(["new", "old"]).nullable().optional().describe("Line type: 'old' = deleted/original line, 'new' = added/modified line, null = unchanged context. MUST match the line_code format and old_line/new_line values."),
|
|
612
|
+
old_line: z.number().nullable().optional().describe("Line number in original file (before changes). REQUIRED when type='old', NULL when type='new' (for purely added lines), can be present for context lines."),
|
|
613
|
+
new_line: z.number().nullable().optional().describe("Line number in modified file (after changes). REQUIRED when type='new', NULL when type='old' (for purely deleted lines), can be present for context lines."),
|
|
614
|
+
}).describe("Start line position for multiline comment range. MUST specify either old_line OR new_line (or both for context), never neither."),
|
|
615
|
+
end: z.object({
|
|
616
|
+
line_code: z.string().nullable().optional().describe("CRITICAL: Line identifier in format '{file_path_sha1_hash}_{old_line_number}_{new_line_number}'. USUALLY REQUIRED for GitLab diff comments despite being optional in schema. Example: 'a1b2c3d4e5f6_12_17'. Must be from same file as start.line_code."),
|
|
617
|
+
type: z.enum(["new", "old"]).nullable().optional().describe("Line type: 'old' = deleted/original line, 'new' = added/modified line, null = unchanged context. SHOULD MATCH start.type for consistent ranges (don't mix old/new types)."),
|
|
618
|
+
old_line: z.number().nullable().optional().describe("Line number in original file (before changes). REQUIRED when type='old', NULL when type='new' (for purely added lines), can be present for context lines. MUST be >= start.old_line if both specified."),
|
|
619
|
+
new_line: z.number().nullable().optional().describe("Line number in modified file (after changes). REQUIRED when type='new', NULL when type='old' (for purely deleted lines), can be present for context lines. MUST be >= start.new_line if both specified."),
|
|
620
|
+
}).describe("End line position for multiline comment range. MUST specify either old_line OR new_line (or both for context), never neither. Range must be valid (end >= start)."),
|
|
621
|
+
}).describe("Line range for multiline comments on GitLab merge request diffs. VALIDATION RULES: 1) line_code is critical for GitLab API success, 2) start/end must have consistent types, 3) line numbers must form valid range, 4) get line_code from GitLab diff API, never generate manually.");
|
|
599
622
|
// Discussion related schemas
|
|
600
623
|
export const GitLabDiscussionNoteSchema = z.object({
|
|
601
624
|
id: z.number(),
|
|
@@ -620,24 +643,24 @@ export const GitLabDiscussionNoteSchema = z.object({
|
|
|
620
643
|
base_sha: z.string(),
|
|
621
644
|
start_sha: z.string(),
|
|
622
645
|
head_sha: z.string(),
|
|
623
|
-
old_path: z.string(),
|
|
624
|
-
new_path: z.string(),
|
|
646
|
+
old_path: z.string().optional().describe("File path before change"),
|
|
647
|
+
new_path: z.string().optional().describe("File path after change"),
|
|
625
648
|
position_type: z.enum(["text", "image", "file"]),
|
|
626
|
-
|
|
627
|
-
|
|
649
|
+
new_line: z.number().nullable().optional().describe("Line number in the modified file (after changes). Used for added lines and context lines. Null for deleted lines."),
|
|
650
|
+
old_line: z.number().nullable().optional().describe("Line number in the original file (before changes). Used for deleted lines and context lines. Null for newly added lines."),
|
|
628
651
|
line_range: z
|
|
629
652
|
.object({
|
|
630
653
|
start: z.object({
|
|
631
|
-
line_code: z.string(),
|
|
654
|
+
line_code: z.string().nullable().optional().describe("Line identifier in format: '{file_path_sha1_hash}_{old_line_number}_{new_line_number}'. Used to uniquely identify a specific line in the diff."),
|
|
632
655
|
type: z.enum(["new", "old", "expanded"]),
|
|
633
|
-
old_line: z.number().
|
|
634
|
-
new_line: z.number().
|
|
656
|
+
old_line: z.number().nullable().optional().describe("Line number in the original file (before changes). Null for newly added lines or unchanged context lines."),
|
|
657
|
+
new_line: z.number().nullable().optional().describe("Line number in the modified file (after changes). Null for deleted lines or unchanged context lines."),
|
|
635
658
|
}),
|
|
636
659
|
end: z.object({
|
|
637
|
-
line_code: z.string(),
|
|
660
|
+
line_code: z.string().nullable().optional().describe("Line identifier in format: '{file_path_sha1_hash}_{old_line_number}_{new_line_number}'. Used to uniquely identify a specific line in the diff."),
|
|
638
661
|
type: z.enum(["new", "old", "expanded"]),
|
|
639
|
-
old_line: z.number().
|
|
640
|
-
new_line: z.number().
|
|
662
|
+
old_line: z.number().nullable().optional().describe("Line number in the original file (before changes). Null for newly added lines or unchanged context lines."),
|
|
663
|
+
new_line: z.number().nullable().optional().describe("Line number in the modified file (after changes). Null for deleted lines or unchanged context lines."),
|
|
641
664
|
}),
|
|
642
665
|
})
|
|
643
666
|
.nullable()
|
|
@@ -818,6 +841,11 @@ export const UpdateMergeRequestSchema = GetMergeRequestSchema.extend({
|
|
|
818
841
|
export const GetMergeRequestDiffsSchema = GetMergeRequestSchema.extend({
|
|
819
842
|
view: z.enum(["inline", "parallel"]).optional().describe("Diff view type"),
|
|
820
843
|
});
|
|
844
|
+
export const ListMergeRequestDiffsSchema = GetMergeRequestSchema.extend({
|
|
845
|
+
page: z.number().optional().describe("Page number for pagination (default: 1)"),
|
|
846
|
+
per_page: z.number().optional().describe("Number of items per page (max: 100, default: 20)"),
|
|
847
|
+
unidiff: z.boolean().optional().describe("Present diffs in the unified diff format. Default is false. Introduced in GitLab 16.5."),
|
|
848
|
+
});
|
|
821
849
|
export const CreateNoteSchema = z.object({
|
|
822
850
|
project_id: z.string().describe("Project ID or namespace/project_path"),
|
|
823
851
|
noteable_type: z
|
|
@@ -1124,18 +1152,19 @@ export const GitLabWikiPageSchema = z.object({
|
|
|
1124
1152
|
});
|
|
1125
1153
|
// Merge Request Thread position schema - used for diff notes
|
|
1126
1154
|
export const MergeRequestThreadPositionSchema = z.object({
|
|
1127
|
-
base_sha: z.string().describe("Base commit SHA in the source branch"),
|
|
1128
|
-
head_sha: z.string().describe("SHA referencing HEAD of the source branch"),
|
|
1129
|
-
start_sha: z.string().describe("SHA referencing the start commit of the source branch"),
|
|
1130
|
-
position_type: z.enum(["text", "image", "file"]).describe("
|
|
1131
|
-
new_path: z.string().optional().describe("File path after
|
|
1132
|
-
old_path: z.string().optional().describe("File path before
|
|
1133
|
-
new_line: z.number().nullable().optional().describe("Line number after
|
|
1134
|
-
old_line: z.number().nullable().optional().describe("Line number before
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1155
|
+
base_sha: z.string().describe("REQUIRED: Base commit SHA in the source branch. Get this from merge request diff_refs.base_sha."),
|
|
1156
|
+
head_sha: z.string().describe("REQUIRED: SHA referencing HEAD of the source branch. Get this from merge request diff_refs.head_sha."),
|
|
1157
|
+
start_sha: z.string().describe("REQUIRED: SHA referencing the start commit of the source branch. Get this from merge request diff_refs.start_sha."),
|
|
1158
|
+
position_type: z.enum(["text", "image", "file"]).describe("REQUIRED: Position type. Use 'text' for code diffs, 'image' for image diffs, 'file' for file-level comments."),
|
|
1159
|
+
new_path: z.string().optional().describe("File path after changes. REQUIRED for most diff comments. Use same as old_path if file wasn't renamed."),
|
|
1160
|
+
old_path: z.string().optional().describe("File path before changes. REQUIRED for most diff comments. Use same as new_path if file wasn't renamed."),
|
|
1161
|
+
new_line: z.number().nullable().optional().describe("Line number in modified file (after changes). Use for added lines or context lines. NULL for deleted lines. For single-line comments on new lines."),
|
|
1162
|
+
old_line: z.number().nullable().optional().describe("Line number in original file (before changes). Use for deleted lines or context lines. NULL for added lines. For single-line comments on old lines."),
|
|
1163
|
+
line_range: LineRangeSchema.optional().describe("MULTILINE COMMENTS: Specify start/end line positions for commenting on multiple lines. Alternative to single old_line/new_line."),
|
|
1164
|
+
width: z.number().optional().describe("IMAGE DIFFS ONLY: Width of the image (for position_type='image')."),
|
|
1165
|
+
height: z.number().optional().describe("IMAGE DIFFS ONLY: Height of the image (for position_type='image')."),
|
|
1166
|
+
x: z.number().optional().describe("IMAGE DIFFS ONLY: X coordinate on the image (for position_type='image')."),
|
|
1167
|
+
y: z.number().optional().describe("IMAGE DIFFS ONLY: Y coordinate on the image (for position_type='image')."),
|
|
1139
1168
|
});
|
|
1140
1169
|
// Schema for creating a new merge request thread
|
|
1141
1170
|
export const CreateMergeRequestThreadSchema = ProjectParamsSchema.extend({
|
|
@@ -1202,3 +1231,28 @@ export const GetMilestoneMergeRequestsSchema = GetProjectMilestoneSchema.merge(P
|
|
|
1202
1231
|
export const PromoteProjectMilestoneSchema = GetProjectMilestoneSchema;
|
|
1203
1232
|
// Schema for getting burndown chart events for a milestone
|
|
1204
1233
|
export const GetMilestoneBurndownEventsSchema = GetProjectMilestoneSchema.merge(PaginationOptionsSchema);
|
|
1234
|
+
// Add schemas for commit operations
|
|
1235
|
+
export const ListCommitsSchema = z.object({
|
|
1236
|
+
project_id: z.string().describe("Project ID or complete URL-encoded path to project"),
|
|
1237
|
+
ref_name: z.string().optional().describe("The name of a repository branch, tag or revision range, or if not given the default branch"),
|
|
1238
|
+
since: z.string().optional().describe("Only commits after or on this date are returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ"),
|
|
1239
|
+
until: z.string().optional().describe("Only commits before or on this date are returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ"),
|
|
1240
|
+
path: z.string().optional().describe("The file path"),
|
|
1241
|
+
author: z.string().optional().describe("Search commits by commit author"),
|
|
1242
|
+
all: z.boolean().optional().describe("Retrieve every commit from the repository"),
|
|
1243
|
+
with_stats: z.boolean().optional().describe("Stats about each commit are added to the response"),
|
|
1244
|
+
first_parent: z.boolean().optional().describe("Follow only the first parent commit upon seeing a merge commit"),
|
|
1245
|
+
order: z.enum(["default", "topo"]).optional().describe("List commits in order"),
|
|
1246
|
+
trailers: z.boolean().optional().describe("Parse and include Git trailers for every commit"),
|
|
1247
|
+
page: z.number().optional().describe("Page number for pagination (default: 1)"),
|
|
1248
|
+
per_page: z.number().optional().describe("Number of items per page (max: 100, default: 20)"),
|
|
1249
|
+
});
|
|
1250
|
+
export const GetCommitSchema = z.object({
|
|
1251
|
+
project_id: z.string().describe("Project ID or complete URL-encoded path to project"),
|
|
1252
|
+
sha: z.string().describe("The commit hash or name of a repository branch or tag"),
|
|
1253
|
+
stats: z.boolean().optional().describe("Include commit stats"),
|
|
1254
|
+
});
|
|
1255
|
+
export const GetCommitDiffSchema = z.object({
|
|
1256
|
+
project_id: z.string().describe("Project ID or complete URL-encoded path to project"),
|
|
1257
|
+
sha: z.string().describe("The commit hash or name of a repository branch or tag"),
|
|
1258
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zereight/mcp-gitlab",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.65",
|
|
4
4
|
"description": "MCP server for using the GitLab API",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "zereight",
|
|
@@ -33,11 +33,13 @@
|
|
|
33
33
|
"@modelcontextprotocol/sdk": "1.8.0",
|
|
34
34
|
"@types/node-fetch": "^2.6.12",
|
|
35
35
|
"express": "^5.1.0",
|
|
36
|
+
"fetch-cookie": "^3.1.0",
|
|
36
37
|
"form-data": "^4.0.0",
|
|
37
38
|
"http-proxy-agent": "^7.0.2",
|
|
38
39
|
"https-proxy-agent": "^7.0.6",
|
|
39
40
|
"node-fetch": "^3.3.2",
|
|
40
41
|
"socks-proxy-agent": "^8.0.5",
|
|
42
|
+
"tough-cookie": "^5.1.2",
|
|
41
43
|
"zod-to-json-schema": "^3.23.5"
|
|
42
44
|
},
|
|
43
45
|
"devDependencies": {
|