From dc6cc59434a14d102a8357034cbce719142c3b0f Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 22 May 2025 13:14:32 +0200 Subject: [PATCH 01/39] feat: add support for creating and updating issue notes - Added create_issue_note to add a new note to an existing issue thread - Added update_issue_note to modify an existing issue thread note - Similar to existing merge request note functions but for issues --- README.md | 54 ++++++++++++------------ index.ts | 119 +++++++++++++++++++++++++++++++++++++++++++++++++++++ schemas.ts | 18 ++++++++ 3 files changed, 165 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 56f49d6..8b34cc4 100644 --- a/README.md +++ b/README.md @@ -93,30 +93,32 @@ When using with the Claude App, you need to set up your API key and URLs directl 15. `mr_discussions` - List discussion items for a merge request 16. `update_merge_request_note` - Modify an existing merge request thread note 17. `create_merge_request_note` - Add a new note to an existing merge request thread -18. `list_issues` - List issues in a GitLab project with filtering options -19. `get_issue` - Get details of a specific issue in a GitLab project -20. `update_issue` - Update an issue in a GitLab project -21. `delete_issue` - Delete an issue from a GitLab project -22. `list_issue_links` - List all issue links for a specific issue -23. `list_issue_discussions` - List discussions for an issue in a GitLab project -24. `get_issue_link` - Get a specific issue link -25. `create_issue_link` - Create an issue link between two issues -26. `delete_issue_link` - Delete an issue link -27. `list_namespaces` - List all namespaces available to the current user -28. `get_namespace` - Get details of a namespace by ID or path -29. `verify_namespace` - Verify if a namespace path exists -30. `get_project` - Get details of a specific project -31. `list_projects` - List projects accessible by the current user -32. `list_labels` - List labels for a project -33. `get_label` - Get a single label from a project -34. `create_label` - Create a new label in a project -35. `update_label` - Update an existing label in a project -36. `delete_label` - Delete a label from a project -37. `list_group_projects` - List projects in a GitLab group with filtering options -38. `list_wiki_pages` - List wiki pages in a GitLab project -39. `get_wiki_page` - Get details of a specific wiki page -40. `create_wiki_page` - Create a new wiki page in a GitLab project -41. `update_wiki_page` - Update an existing wiki page in a GitLab project -42. `delete_wiki_page` - Delete a wiki page from a GitLab project -43. `get_repository_tree` - Get the repository tree for a GitLab project (list files and directories) +18. `update_issue_note` - Update the content of an existing issue note +19. `create_issue_note` - Add a new note to an existing issue thread +20. `list_issues` - List issues in a GitLab project with filtering options +21. `get_issue` - Get details of a specific issue in a GitLab project +22. `update_issue` - Update an issue in a GitLab project +23. `delete_issue` - Delete an issue from a GitLab project +24. `list_issue_links` - List all issue links for a specific issue +25. `list_issue_discussions` - List discussions for an issue in a GitLab project +26. `get_issue_link` - Get a specific issue link +27. `create_issue_link` - Create an issue link between two issues +28. `delete_issue_link` - Delete an issue link +29. `list_namespaces` - List all namespaces available to the current user +30. `get_namespace` - Get details of a namespace by ID or path +31. `verify_namespace` - Verify if a namespace path exists +32. `get_project` - Get details of a specific project +33. `list_projects` - List projects accessible by the current user +34. `list_labels` - List labels for a project +35. `get_label` - Get a single label from a project +36. `create_label` - Create a new label in a project +37. `update_label` - Update an existing label in a project +38. `delete_label` - Delete a label from a project +39. `list_group_projects` - List projects in a GitLab group with filtering options +40. `list_wiki_pages` - List wiki pages in a GitLab project +41. `get_wiki_page` - Get details of a specific wiki page +42. `create_wiki_page` - Create a new wiki page in a GitLab project +43. `update_wiki_page` - Update an existing wiki page in a GitLab project +44. `delete_wiki_page` - Delete a wiki page from a GitLab project +45. `get_repository_tree` - Get the repository tree for a GitLab project (list files and directories) diff --git a/index.ts b/index.ts index 1aaebee..5a7a045 100644 --- a/index.ts +++ b/index.ts @@ -121,6 +121,8 @@ import { GetRepositoryTreeSchema, type GitLabTreeItem, type GetRepositoryTreeOptions, + UpdateIssueNoteSchema, + CreateIssueNoteSchema, } from "./schemas.js"; /** @@ -287,6 +289,16 @@ const allTools = [ description: "Add a new note to an existing merge request thread", inputSchema: zodToJsonSchema(CreateMergeRequestNoteSchema), }, + { + name: "update_issue_note", + description: "Modify an existing issue thread note", + inputSchema: zodToJsonSchema(UpdateIssueNoteSchema), + }, + { + name: "create_issue_note", + description: "Add a new note to an existing issue thread", + inputSchema: zodToJsonSchema(CreateIssueNoteSchema), + }, { name: "list_issues", description: "List issues in a GitLab project with filtering options", @@ -1126,6 +1138,81 @@ async function updateMergeRequestNote( return GitLabDiscussionNoteSchema.parse(data); } +/** + * Update an issue discussion note + * @param {string} projectId - The ID or URL-encoded path of the project + * @param {number} issueIid - The IID of an issue + * @param {string} discussionId - The ID of a thread + * @param {number} noteId - The ID of a thread note + * @param {string} body - The new content of the note + * @returns {Promise} The updated note + */ +async function updateIssueNote( + projectId: string, + issueIid: number, + discussionId: string, + noteId: number, + body: string +): Promise { + projectId = decodeURIComponent(projectId); // Decode project ID + const url = new URL( + `${GITLAB_API_URL}/projects/${encodeURIComponent( + projectId + )}/issues/${issueIid}/discussions/${discussionId}/notes/${noteId}` + ); + + const payload = { body }; + + const response = await fetch(url.toString(), { + ...DEFAULT_FETCH_CONFIG, + method: "PUT", + body: JSON.stringify(payload), + }); + + await handleGitLabError(response); + const data = await response.json(); + return GitLabDiscussionNoteSchema.parse(data); +} + +/** + * Create a note in an issue discussion + * @param {string} projectId - The ID or URL-encoded path of the project + * @param {number} issueIid - The IID of an issue + * @param {string} discussionId - The ID of a thread + * @param {string} body - The content of the new note + * @param {string} [createdAt] - The creation date of the note (ISO 8601 format) + * @returns {Promise} The created note + */ +async function createIssueNote( + projectId: string, + issueIid: number, + discussionId: string, + body: string, + createdAt?: string +): Promise { + projectId = decodeURIComponent(projectId); // Decode project ID + const url = new URL( + `${GITLAB_API_URL}/projects/${encodeURIComponent( + projectId + )}/issues/${issueIid}/discussions/${discussionId}/notes` + ); + + const payload: { body: string; created_at?: string } = { body }; + if (createdAt) { + payload.created_at = createdAt; + } + + const response = await fetch(url.toString(), { + ...DEFAULT_FETCH_CONFIG, + method: "POST", + body: JSON.stringify(payload), + }); + + await handleGitLabError(response); + const data = await response.json(); + return GitLabDiscussionNoteSchema.parse(data); +} + /** * Add a new note to an existing merge request thread * 기존 병합 요청 스레드에 새 노트 추가 @@ -2461,6 +2548,38 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { }; } + case "update_issue_note": { + const args = UpdateIssueNoteSchema.parse( + request.params.arguments + ); + const note = await updateIssueNote( + args.project_id, + args.issue_iid, + args.discussion_id, + args.note_id, + args.body + ); + return { + content: [{ type: "text", text: JSON.stringify(note, null, 2) }], + }; + } + + case "create_issue_note": { + const args = CreateIssueNoteSchema.parse( + request.params.arguments + ); + const note = await createIssueNote( + args.project_id, + args.issue_iid, + args.discussion_id, + args.body, + args.created_at + ); + return { + content: [{ type: "text", text: JSON.stringify(note, null, 2) }], + }; + } + case "get_merge_request": { const args = GetMergeRequestSchema.parse(request.params.arguments); const mergeRequest = await getMergeRequest( diff --git a/schemas.ts b/schemas.ts index 071ee5f..6ae2fce 100644 --- a/schemas.ts +++ b/schemas.ts @@ -491,6 +491,22 @@ export const CreateMergeRequestNoteSchema = ProjectParamsSchema.extend({ created_at: z.string().optional().describe("Date the note was created at (ISO 8601 format)"), }); +// Input schema for updating an issue discussion note +export const UpdateIssueNoteSchema = ProjectParamsSchema.extend({ + issue_iid: z.number().describe("The IID of an issue"), + discussion_id: z.string().describe("The ID of a thread"), + note_id: z.number().describe("The ID of a thread note"), + body: z.string().describe("The content of the note or reply"), +}); + +// Input schema for adding a note to an existing issue discussion +export const CreateIssueNoteSchema = ProjectParamsSchema.extend({ + issue_iid: z.number().describe("The IID of an issue"), + discussion_id: z.string().describe("The ID of a thread"), + body: z.string().describe("The content of the note or reply"), + created_at: z.string().optional().describe("Date the note was created at (ISO 8601 format)"), +}); + // API Operation Parameter Schemas export const CreateOrUpdateFileSchema = ProjectParamsSchema.extend({ @@ -1085,6 +1101,8 @@ export type GitLabMergeRequestDiff = z.infer< export type CreateNoteOptions = z.infer; export type GitLabIssueLink = z.infer; export type ListIssueDiscussionsOptions = z.infer; +export type UpdateIssueNoteOptions = z.infer; +export type CreateIssueNoteOptions = z.infer; export type GitLabNamespace = z.infer; export type GitLabNamespaceExistsResponse = z.infer< typeof GitLabNamespaceExistsResponseSchema From 25be1947b98ffe1e5cffbfce9e04928f4180d2f8 Mon Sep 17 00:00:00 2001 From: simple Date: Thu, 22 May 2025 21:24:29 +0900 Subject: [PATCH 02/39] =?UTF-8?q?chore(release):=201.0.42=20-=20issue=20no?= =?UTF-8?q?te=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20(#47)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 12 ++++++++++++ package.json | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d09722..41a3cfb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +## [1.0.42] - 2025-05-22 + +### Added + +- 이슈(issues)에 노트(note)를 생성하고 수정할 수 있는 기능이 추가되었습니다. +- 이제 버그나 할 일 같은 이슈에도 댓글(메모)을 달거나, 이미 단 댓글을 고칠 수 있습니다. +- 예시: "버그를 고쳤어요!"라는 댓글을 이슈에 달 수 있고, 필요하면 "버그를 완전히 고쳤어요!"로 바꿀 수 있습니다. +- 함수형 프로그래밍 원칙과 SOLID 원칙을 준수하여, 코드의 재사용성과 유지보수성이 높아졌습니다. +- 출처: [PR #47](https://github.com/zereight/gitlab-mcp/pull/47) + +--- + ## [1.0.38] - 2025-05-17 ### Fixed diff --git a/package.json b/package.json index 3fbd295..d0768d5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@zereight/mcp-gitlab", - "version": "1.0.40", + "version": "1.0.42", "description": "MCP server for using the GitLab API", "license": "MIT", "author": "zereight", From 3d7aa8035d996a312559e15f7dd1457e1f32a826 Mon Sep 17 00:00:00 2001 From: simple Date: Thu, 22 May 2025 21:28:34 +0900 Subject: [PATCH 03/39] docs: translate issue notes changelog from Korean to English --- CHANGELOG.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41a3cfb..ad95772 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,9 @@ ### Added -- 이슈(issues)에 노트(note)를 생성하고 수정할 수 있는 기능이 추가되었습니다. -- 이제 버그나 할 일 같은 이슈에도 댓글(메모)을 달거나, 이미 단 댓글을 고칠 수 있습니다. -- 예시: "버그를 고쳤어요!"라는 댓글을 이슈에 달 수 있고, 필요하면 "버그를 완전히 고쳤어요!"로 바꿀 수 있습니다. -- 함수형 프로그래밍 원칙과 SOLID 원칙을 준수하여, 코드의 재사용성과 유지보수성이 높아졌습니다. -- 출처: [PR #47](https://github.com/zereight/gitlab-mcp/pull/47) +- Added support for creating and updating issue notes (comments) in GitLab. +- You can now add or edit comments on issues. +- See: [PR #47](https://github.com/zereight/gitlab-mcp/pull/47) --- From 140620397ba88ee6abbd6da01147a466905e1f22 Mon Sep 17 00:00:00 2001 From: simple Date: Fri, 23 May 2025 00:41:56 +0900 Subject: [PATCH 04/39] chore(release): 1.0.43 - get_repository_tree is added read_only_mode --- index.ts | 34 ++++++++++++++++++++-------------- package.json | 2 +- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/index.ts b/index.ts index 5a7a045..65d7b98 100644 --- a/index.ts +++ b/index.ts @@ -452,6 +452,7 @@ const readOnlyTools = [ "list_labels", "get_label", "list_group_projects", + "get_repository_tree", ]; // Define which tools are related to wiki and can be toggled by USE_GITLAB_WIKI @@ -1054,10 +1055,10 @@ async function listIssueDiscussions( projectId: string, issueIid: number, options: { - page?: number, - per_page?: number, - sort?: "asc" | "desc", - order_by?: "created_at" | "updated_at" + page?: number; + per_page?: number; + sort?: "asc" | "desc"; + order_by?: "created_at" | "updated_at"; } = {} ): Promise { projectId = decodeURIComponent(projectId); // Decode project ID @@ -2549,9 +2550,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { } case "update_issue_note": { - const args = UpdateIssueNoteSchema.parse( - request.params.arguments - ); + const args = UpdateIssueNoteSchema.parse(request.params.arguments); const note = await updateIssueNote( args.project_id, args.issue_iid, @@ -2565,9 +2564,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { } case "create_issue_note": { - const args = CreateIssueNoteSchema.parse( - request.params.arguments - ); + const args = CreateIssueNoteSchema.parse(request.params.arguments); const note = await createIssueNote( args.project_id, args.issue_iid, @@ -2757,8 +2754,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { } case "create_merge_request_thread": { - const args = CreateMergeRequestThreadSchema.parse(request.params.arguments); - const { project_id, merge_request_iid, body, position, created_at } = args; + const args = CreateMergeRequestThreadSchema.parse( + request.params.arguments + ); + const { project_id, merge_request_iid, body, position, created_at } = + args; const thread = await createMergeRequestThread( project_id, @@ -2827,9 +2827,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { const args = ListIssueDiscussionsSchema.parse(request.params.arguments); const { project_id, issue_iid, ...options } = args; - const discussions = await listIssueDiscussions(project_id, issue_iid, options); + const discussions = await listIssueDiscussions( + project_id, + issue_iid, + options + ); return { - content: [{ type: "text", text: JSON.stringify(discussions, null, 2) }], + content: [ + { type: "text", text: JSON.stringify(discussions, null, 2) }, + ], }; } diff --git a/package.json b/package.json index d0768d5..064aa3f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@zereight/mcp-gitlab", - "version": "1.0.42", + "version": "1.0.43", "description": "MCP server for using the GitLab API", "license": "MIT", "author": "zereight", From ea06c21f298feb84e93540fa3bfb8b315562fe1f Mon Sep 17 00:00:00 2001 From: Vicen Dominguez Date: Sat, 24 May 2025 12:44:24 +0200 Subject: [PATCH 05/39] feat(release): 1.0.44 adds pipeline jobs tool --- README.md | 8 +- index.ts | 276 ++++++++++++++++++++++++++++++++++++++++++++++++++- package.json | 2 +- schemas.ts | 118 ++++++++++++++++++++++ 4 files changed, 398 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 8b34cc4..d04d6e5 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,6 @@ When using with the Claude App, you need to set up your API key and URLs directl ## Tools 🛠️ + - 1. `create_or_update_file` - Create or update a single file in a GitLab project 2. `search_repositories` - Search for GitLab projects 3. `create_repository` - Create a new GitLab project @@ -93,7 +92,7 @@ When using with the Claude App, you need to set up your API key and URLs directl 15. `mr_discussions` - List discussion items for a merge request 16. `update_merge_request_note` - Modify an existing merge request thread note 17. `create_merge_request_note` - Add a new note to an existing merge request thread -18. `update_issue_note` - Update the content of an existing issue note +18. `update_issue_note` - Modify an existing issue thread note 19. `create_issue_note` - Add a new note to an existing issue thread 20. `list_issues` - List issues in a GitLab project with filtering options 21. `get_issue` - Get details of a specific issue in a GitLab project @@ -121,4 +120,9 @@ When using with the Claude App, you need to set up your API key and URLs directl 43. `update_wiki_page` - Update an existing wiki page in a GitLab project 44. `delete_wiki_page` - Delete a wiki page from a GitLab project 45. `get_repository_tree` - Get the repository tree for a GitLab project (list files and directories) +46. `list_pipelines` - List pipelines in a GitLab project with filtering options +47. `get_pipeline` - Get details of a specific pipeline in a GitLab project +48. `list_pipeline_jobs` - List all jobs in a specific pipeline +49. `get_pipeline_job` - Get details of a GitLab pipeline job number +50. `get_pipeline_job_output` - Get the output/trace of a GitLab pipeline job number diff --git a/index.ts b/index.ts index 65d7b98..b1ea41d 100644 --- a/index.ts +++ b/index.ts @@ -17,7 +17,6 @@ import { fileURLToPath } from "url"; import { dirname } from "path"; import fs from "fs"; import path from "path"; - // Add type imports for proxy agents import { Agent } from "http"; import { URL } from "url"; @@ -84,6 +83,15 @@ import { UpdateWikiPageSchema, DeleteWikiPageSchema, GitLabWikiPageSchema, + GetRepositoryTreeSchema, + GitLabTreeItemSchema, + GitLabPipelineSchema, + GetPipelineSchema, + ListPipelinesSchema, + ListPipelineJobsSchema, + // pipeline job schemas + GetPipelineJobOutputSchema, + GitLabPipelineJobSchema, // Discussion Schemas GitLabDiscussionNoteSchema, // Added GitLabDiscussionSchema, @@ -108,6 +116,11 @@ import { type GitLabNamespaceExistsResponse, type GitLabProject, type GitLabLabel, + type GitLabPipeline, + type ListPipelinesOptions, + type GetPipelineOptions, + type ListPipelineJobsOptions, + type GitLabPipelineJob, // Discussion Types type GitLabDiscussionNote, // Added type GitLabDiscussion, @@ -117,8 +130,6 @@ import { type UpdateWikiPageOptions, type DeleteWikiPageOptions, type GitLabWikiPage, - GitLabTreeItemSchema, - GetRepositoryTreeSchema, type GitLabTreeItem, type GetRepositoryTreeOptions, UpdateIssueNoteSchema, @@ -430,6 +441,31 @@ const allTools = [ "Get the repository tree for a GitLab project (list files and directories)", inputSchema: zodToJsonSchema(GetRepositoryTreeSchema), }, + { + name: "list_pipelines", + description: "List pipelines in a GitLab project with filtering options", + inputSchema: zodToJsonSchema(ListPipelinesSchema), + }, + { + name: "get_pipeline", + description: "Get details of a specific pipeline in a GitLab project", + inputSchema: zodToJsonSchema(GetPipelineSchema), + }, + { + name: "list_pipeline_jobs", + description: "List all jobs in a specific pipeline", + inputSchema: zodToJsonSchema(ListPipelineJobsSchema), + }, + { + name: "get_pipeline_job", + description: "Get details of a GitLab pipeline job number", + inputSchema: zodToJsonSchema(GetPipelineJobOutputSchema), + }, + { + name: "get_pipeline_job_output", + description: "Get the output/trace of a GitLab pipeline job number", + inputSchema: zodToJsonSchema(GetPipelineJobOutputSchema), + }, ]; // Define which tools are read-only @@ -448,6 +484,11 @@ const readOnlyTools = [ "get_namespace", "verify_namespace", "get_project", + "get_pipeline", + "list_pipelines", + "list_pipeline_jobs", + "get_pipeline_job", + "get_pipeline_job_output", "list_projects", "list_labels", "get_label", @@ -2300,6 +2341,166 @@ async function deleteWikiPage(projectId: string, slug: string): Promise { await handleGitLabError(response); } +/** + * List pipelines in a GitLab project + * + * @param {string} projectId - The ID or URL-encoded path of the project + * @param {ListPipelinesOptions} options - Options for filtering pipelines + * @returns {Promise} List of pipelines + */ +async function listPipelines( + projectId: string, + options: Omit = {} +): Promise { + projectId = decodeURIComponent(projectId); // Decode project ID + const url = new URL( + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/pipelines` + ); + + // Add all query parameters + Object.entries(options).forEach(([key, value]) => { + if (value !== undefined) { + url.searchParams.append(key, value.toString()); + } + }); + + const response = await fetch(url.toString(), { + ...DEFAULT_FETCH_CONFIG, + }); + + await handleGitLabError(response); + const data = await response.json(); + return z.array(GitLabPipelineSchema).parse(data); +} + +/** + * Get details of a specific pipeline + * + * @param {string} projectId - The ID or URL-encoded path of the project + * @param {number} pipelineId - The ID of the pipeline + * @returns {Promise} Pipeline details + */ +async function getPipeline( + projectId: string, + pipelineId: number +): Promise { + projectId = decodeURIComponent(projectId); // Decode project ID + const url = new URL( + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/pipelines/${pipelineId}` + ); + + const response = await fetch(url.toString(), { + ...DEFAULT_FETCH_CONFIG, + }); + + if (response.status === 404) { + throw new Error(`Pipeline not found`); + } + + await handleGitLabError(response); + const data = await response.json(); + return GitLabPipelineSchema.parse(data); +} + +/** + * List all jobs in a specific pipeline + * + * @param {string} projectId - The ID or URL-encoded path of the project + * @param {number} pipelineId - The ID of the pipeline + * @param {Object} options - Options for filtering jobs + * @returns {Promise} List of pipeline jobs + */ +async function listPipelineJobs( + projectId: string, + pipelineId: number, + options: Omit = {} +): Promise { + projectId = decodeURIComponent(projectId); // Decode project ID + const url = new URL( + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/pipelines/${pipelineId}/jobs` + ); + + // Add all query parameters + Object.entries(options).forEach(([key, value]) => { + if (value !== undefined) { + if (typeof value === "boolean") { + url.searchParams.append(key, value ? "true" : "false"); + } else { + url.searchParams.append(key, value.toString()); + } + } + }); + + const response = await fetch(url.toString(), { + ...DEFAULT_FETCH_CONFIG, + }); + + if (response.status === 404) { + throw new Error(`Pipeline not found`); + } + + await handleGitLabError(response); + const data = await response.json(); + return z.array(GitLabPipelineJobSchema).parse(data); +} +async function getPipelineJob( + projectId: string, + jobId: number +): Promise { + projectId = decodeURIComponent(projectId); // Decode project ID + const url = new URL( + `${GITLAB_API_URL}/projects/${encodeURIComponent( + projectId + )}/jobs/${jobId}` + ); + + const response = await fetch(url.toString(), { + ...DEFAULT_FETCH_CONFIG, + }); + + if (response.status === 404) { + throw new Error(`Job not found`); + } + + await handleGitLabError(response); + const data = await response.json(); + return GitLabPipelineJobSchema.parse(data); +} + +/** + * Get the output/trace of a pipeline job + * + * @param {string} projectId - The ID or URL-encoded path of the project + * @param {number} jobId - The ID of the job + * @returns {Promise} The job output/trace + */ +async function getPipelineJobOutput( + projectId: string, + jobId: number +): Promise { + projectId = decodeURIComponent(projectId); // Decode project ID + const url = new URL( + `${GITLAB_API_URL}/projects/${encodeURIComponent( + projectId + )}/jobs/${jobId}/trace` + ); + + const response = await fetch(url.toString(), { + ...DEFAULT_FETCH_CONFIG, + headers: { + ...DEFAULT_HEADERS, + Accept: "text/plain", // Override Accept header to get plain text + }, + }); + + if (response.status === 404) { + throw new Error(`Job trace not found or job is not finished yet`); + } + + await handleGitLabError(response); + return await response.text(); +} + /** * Get the repository tree for a project * @param {string} projectId - The ID or URL-encoded path of the project @@ -3030,6 +3231,75 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { }; } + case "list_pipelines": { + const args = ListPipelinesSchema.parse(request.params.arguments); + const { project_id, ...options } = args; + const pipelines = await listPipelines(project_id, options); + return { + content: [{ type: "text", text: JSON.stringify(pipelines, null, 2) }], + }; + } + + case "get_pipeline": { + const { project_id, pipeline_id } = GetPipelineSchema.parse( + request.params.arguments + ); + const pipeline = await getPipeline(project_id, pipeline_id); + return { + content: [ + { + type: "text", + text: JSON.stringify(pipeline, null, 2), + }, + ], + }; + } + + case "list_pipeline_jobs": { + const { project_id, pipeline_id, ...options } = ListPipelineJobsSchema.parse( + request.params.arguments + ); + const jobs = await listPipelineJobs(project_id, pipeline_id, options); + return { + content: [ + { + type: "text", + text: JSON.stringify(jobs, null, 2), + }, + ], + }; + } + + case "get_pipeline_job": { + const { project_id, job_id } = GetPipelineJobOutputSchema.parse( + request.params.arguments + ); + const jobDetails = await getPipelineJob(project_id, job_id); + return { + content: [ + { + type: "text", + text: JSON.stringify(jobDetails, null, 2), + }, + ], + }; + } + + case "get_pipeline_job_output": { + const { project_id, job_id } = GetPipelineJobOutputSchema.parse( + request.params.arguments + ); + const jobOutput = await getPipelineJobOutput(project_id, job_id); + return { + content: [ + { + type: "text", + text: jobOutput, + }, + ], + }; + } + default: throw new Error(`Unknown tool: ${request.params.name}`); } diff --git a/package.json b/package.json index 064aa3f..c76f7a9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@zereight/mcp-gitlab", - "version": "1.0.43", + "version": "1.0.44", "description": "MCP server for using the GitLab API", "license": "MIT", "author": "zereight", diff --git a/schemas.ts b/schemas.ts index 6ae2fce..f8154ef 100644 --- a/schemas.ts +++ b/schemas.ts @@ -7,6 +7,119 @@ export const GitLabAuthorSchema = z.object({ date: z.string(), }); +// Pipeline related schemas +export const GitLabPipelineSchema = z.object({ + id: z.number(), + project_id: z.number(), + sha: z.string(), + ref: z.string(), + status: z.string(), + source: z.string().optional(), + created_at: z.string(), + updated_at: z.string(), + web_url: z.string(), + duration: z.number().nullable().optional(), + started_at: z.string().nullable().optional(), + finished_at: z.string().nullable().optional(), + coverage: z.number().nullable().optional(), + user: z.object({ + id: z.number(), + name: z.string(), + username: z.string(), + avatar_url: z.string().nullable().optional(), + }).optional(), + detailed_status: z.object({ + icon: z.string().optional(), + text: z.string().optional(), + label: z.string().optional(), + group: z.string().optional(), + tooltip: z.string().optional(), + has_details: z.boolean().optional(), + details_path: z.string().optional(), + illustration: z.object({ + image: z.string().optional(), + size: z.string().optional(), + title: z.string().optional(), + }).optional(), + favicon: z.string().optional(), + }).optional(), +}); + +// Pipeline job related schemas +export const GitLabPipelineJobSchema = z.object({ + id: z.number(), + status: z.string(), + stage: z.string(), + name: z.string(), + ref: z.string(), + tag: z.boolean(), + coverage: z.number().nullable().optional(), + created_at: z.string(), + started_at: z.string().nullable().optional(), + finished_at: z.string().nullable().optional(), + duration: z.number().nullable().optional(), + user: z.object({ + id: z.number(), + name: z.string(), + username: z.string(), + avatar_url: z.string().nullable().optional(), + }).optional(), + commit: z.object({ + id: z.string(), + short_id: z.string(), + title: z.string(), + author_name: z.string(), + author_email: z.string(), + }).optional(), + pipeline: z.object({ + id: z.number(), + project_id: z.number(), + status: z.string(), + ref: z.string(), + sha: z.string(), + }).optional(), + web_url: z.string().optional(), +}); + +// Schema for listing pipelines +export const ListPipelinesSchema = z.object({ + project_id: z.string().describe("Project ID or URL-encoded path"), + scope: z.enum(['running', 'pending', 'finished', 'branches', 'tags']).optional().describe("The scope of pipelines"), + status: z.enum(['created', 'waiting_for_resource', 'preparing', 'pending', 'running', 'success', 'failed', 'canceled', 'skipped', 'manual', 'scheduled']).optional().describe("The status of pipelines"), + ref: z.string().optional().describe("The ref of pipelines"), + sha: z.string().optional().describe("The SHA of pipelines"), + yaml_errors: z.boolean().optional().describe("Returns pipelines with invalid configurations"), + username: z.string().optional().describe("The username of the user who triggered pipelines"), + updated_after: z.string().optional().describe("Return pipelines updated after the specified date"), + updated_before: z.string().optional().describe("Return pipelines updated before the specified date"), + order_by: z.enum(['id', 'status', 'ref', 'updated_at', 'user_id']).optional().describe("Order pipelines by"), + sort: z.enum(['asc', 'desc']).optional().describe("Sort pipelines"), + page: z.number().optional().describe("Page number for pagination"), + per_page: z.number().optional().describe("Number of items per page (max 100)"), +}); + +// Schema for getting a specific pipeline +export const GetPipelineSchema = z.object({ + project_id: z.string().describe("Project ID or URL-encoded path"), + pipeline_id: z.number().describe("The ID of the pipeline"), +}); + +// Schema for listing jobs in a pipeline +export const ListPipelineJobsSchema = z.object({ + project_id: z.string().describe("Project ID or URL-encoded path"), + pipeline_id: z.number().describe("The ID of the pipeline"), + scope: z.enum(['created', 'pending', 'running', 'failed', 'success', 'canceled', 'skipped', 'manual']).optional().describe("The scope of jobs to show"), + include_retried: z.boolean().optional().describe("Whether to include retried jobs"), + page: z.number().optional().describe("Page number for pagination"), + per_page: z.number().optional().describe("Number of items per page (max 100)"), +}); + +// Schema for the input parameters for pipeline job operations +export const GetPipelineJobOutputSchema = z.object({ + project_id: z.string().describe("Project ID or URL-encoded path"), + job_id: z.number().describe("The ID of the job"), +}); + // Namespace related schemas // Base schema for project-related operations @@ -1120,3 +1233,8 @@ export type GetRepositoryTreeOptions = z.infer; export type MergeRequestThreadPosition = z.infer; export type CreateMergeRequestThreadOptions = z.infer; export type CreateMergeRequestNoteOptions = z.infer; +export type GitLabPipelineJob = z.infer; +export type GitLabPipeline = z.infer; +export type ListPipelinesOptions = z.infer; +export type GetPipelineOptions = z.infer; +export type ListPipelineJobsOptions = z.infer; From 64a936446e525bb3fd4f0d8c1f53cd63b680eb44 Mon Sep 17 00:00:00 2001 From: simple Date: Sat, 24 May 2025 20:57:15 +0900 Subject: [PATCH 06/39] [release] feat: update version to 1.0.45 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🚀 Breaking Changes: - Version updated from 1.0.44 to 1.0.45 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c76f7a9..3266b53 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@zereight/mcp-gitlab", - "version": "1.0.44", + "version": "1.0.45", "description": "MCP server for using the GitLab API", "license": "MIT", "author": "zereight", From 8ba33986f3da8eae4079b179aa3580a1712586a1 Mon Sep 17 00:00:00 2001 From: simple Date: Sat, 24 May 2025 21:02:58 +0900 Subject: [PATCH 07/39] [main] docs: update changelog for v1.0.45 pipeline tools MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🚀 Breaking Changes: - None 📝 Details: - Added 5 new pipeline management tools - Pipeline status monitoring and analysis support --- CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad95772..a7a0bf0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,20 @@ +## [1.0.45] - 2025-05-24 + +### Added + +- 🔄 **Pipeline Management Tools**: Added GitLab pipeline status monitoring and management functionality + - `list_pipelines`: List project pipelines with various filtering options + - `get_pipeline`: Get detailed information about a specific pipeline + - `list_pipeline_jobs`: List all jobs in a specific pipeline + - `get_pipeline_job`: Get detailed information about a specific pipeline job + - `get_pipeline_job_output`: Get execution logs/output from pipeline jobs +- 📊 Pipeline status summary and analysis support + - Example: "How many of the last N pipelines are successful?" + - Example: "Can you make a summary of the output in the last pipeline?" +- See: [PR #52](https://github.com/zereight/gitlab-mcp/pull/52) + +--- + ## [1.0.42] - 2025-05-22 ### Added From dc99f864ca64207cc19400fe3828de82f650dde3 Mon Sep 17 00:00:00 2001 From: iwakitakuma33 Date: Tue, 27 May 2025 02:00:36 +0900 Subject: [PATCH 08/39] FIX: description null error --- schemas.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/schemas.ts b/schemas.ts index f8154ef..9393db0 100644 --- a/schemas.ts +++ b/schemas.ts @@ -407,7 +407,7 @@ export const GitLabMilestoneSchema = z.object({ id: z.number(), iid: z.number(), // Added to match GitLab API title: z.string(), - description: z.string(), + description: z.string().nullable().default(""), state: z.string(), web_url: z.string(), // Changed from html_url to match GitLab API }); @@ -417,7 +417,7 @@ export const GitLabIssueSchema = z.object({ iid: z.number(), // Added to match GitLab API project_id: z.number(), // Added to match GitLab API title: z.string(), - description: z.string(), // Changed from body to match GitLab API + description: z.string().nullable().default(""), // Changed from body to match GitLab API state: z.string(), author: GitLabUserSchema, assignees: z.array(GitLabUserSchema), From f8b1444afd5932307ae743ec11380189e59daafa Mon Sep 17 00:00:00 2001 From: simple Date: Tue, 27 May 2025 12:25:31 +0900 Subject: [PATCH 09/39] [main] fix: description null error handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 📝 Details: - GitLab issues/milestones의 null description 처리 - schemas.ts에서 description을 nullable로 변경 --- CHANGELOG.md | 11 +++++++++++ package.json | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7a0bf0..4c5bc7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +## [1.0.46] - 2025-05-27 + +### Fixed + +- Fixed issue where GitLab issues and milestones with null descriptions would cause JSON-RPC errors + - Changed `description` field to be nullable with default empty string in schemas + - This allows proper handling of GitLab issues/milestones without descriptions + - See: [PR #53](https://github.com/zereight/gitlab-mcp/pull/53), [Issue #51](https://github.com/zereight/gitlab-mcp/issues/51) + +--- + ## [1.0.45] - 2025-05-24 ### Added diff --git a/package.json b/package.json index 3266b53..ff81a9e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@zereight/mcp-gitlab", - "version": "1.0.45", + "version": "1.0.46", "description": "MCP server for using the GitLab API", "license": "MIT", "author": "zereight", From ab571d211de494f116a74bb403267b10e75460a8 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Wed, 28 May 2025 15:44:15 +0200 Subject: [PATCH 10/39] fix(schemas): make avatar_url nullable in GitLabUserSchema Users without profile pictures have null avatar_url values in GitLab API responses. This change prevents errors when calling get_issue tool. --- schemas.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/schemas.ts b/schemas.ts index 9393db0..d8c78f1 100644 --- a/schemas.ts +++ b/schemas.ts @@ -399,7 +399,7 @@ export const GitLabUserSchema = z.object({ username: z.string(), // Changed from login to match GitLab API id: z.number(), name: z.string(), - avatar_url: z.string(), + avatar_url: z.string().nullable(), web_url: z.string(), // Changed from html_url to match GitLab API }); From cc847772f1f8560d9ce9cba25acbb232cbbf618d Mon Sep 17 00:00:00 2001 From: Jiaqi Wang Date: Wed, 28 May 2025 16:54:30 +0200 Subject: [PATCH 11/39] feat: implement list_merge_requests functionality - Add ListMergeRequestsSchema with comprehensive filtering options - Implement listMergeRequests function following GitLab API - Add tool definition and switch case handler - Include in readOnlyTools array - Update README.md with new tool documentation --- README.md | 1 + index.ts | 52 ++++++++++++++++++++++++++++++ package-lock.json | 4 +-- schemas.ts | 82 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 137 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d04d6e5..42d679e 100644 --- a/README.md +++ b/README.md @@ -125,4 +125,5 @@ When using with the Claude App, you need to set up your API key and URLs directl 48. `list_pipeline_jobs` - List all jobs in a specific pipeline 49. `get_pipeline_job` - Get details of a GitLab pipeline job number 50. `get_pipeline_job_output` - Get the output/trace of a GitLab pipeline job number +51. `list_merge_requests` - List merge requests in a GitLab project with filtering options diff --git a/index.ts b/index.ts index b1ea41d..a6b00a6 100644 --- a/index.ts +++ b/index.ts @@ -134,6 +134,7 @@ import { type GetRepositoryTreeOptions, UpdateIssueNoteSchema, CreateIssueNoteSchema, + ListMergeRequestsSchema, } from "./schemas.js"; /** @@ -466,6 +467,11 @@ const allTools = [ description: "Get the output/trace of a GitLab pipeline job number", inputSchema: zodToJsonSchema(GetPipelineJobOutputSchema), }, + { + name: "list_merge_requests", + description: "List merge requests in a GitLab project with filtering options", + inputSchema: zodToJsonSchema(ListMergeRequestsSchema), + }, ]; // Define which tools are read-only @@ -476,6 +482,7 @@ const readOnlyTools = [ "get_merge_request_diffs", "mr_discussions", "list_issues", + "list_merge_requests", "get_issue", "list_issue_links", "list_issue_discussions", @@ -791,6 +798,43 @@ async function listIssues( return z.array(GitLabIssueSchema).parse(data); } +/** + * List merge requests in a GitLab project with optional filtering + * + * @param {string} projectId - The ID or URL-encoded path of the project + * @param {Object} options - Optional filtering parameters + * @returns {Promise} List of merge requests + */ +async function listMergeRequests( + projectId: string, + options: Omit, "project_id"> = {} +): Promise { + projectId = decodeURIComponent(projectId); // Decode project ID + const url = new URL( + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/merge_requests` + ); + + // Add all query parameters + Object.entries(options).forEach(([key, value]) => { + if (value !== undefined) { + if (key === "labels" && Array.isArray(value)) { + // Handle array of labels + url.searchParams.append(key, value.join(",")); + } else { + url.searchParams.append(key, value.toString()); + } + } + }); + + const response = await fetch(url.toString(), { + ...DEFAULT_FETCH_CONFIG, + }); + + await handleGitLabError(response); + const data = await response.json(); + return z.array(GitLabMergeRequestSchema).parse(data); +} + /** * Get a single issue from a GitLab project * 단일 이슈 조회 @@ -3300,6 +3344,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { }; } + case "list_merge_requests": { + const args = ListMergeRequestsSchema.parse(request.params.arguments); + const mergeRequests = await listMergeRequests(args.project_id, args); + return { + content: [{ type: "text", text: JSON.stringify(mergeRequests, null, 2) }], + }; + } + default: throw new Error(`Unknown tool: ${request.params.name}`); } diff --git a/package-lock.json b/package-lock.json index 991408d..2002944 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@zereight/mcp-gitlab", - "version": "1.0.38", + "version": "1.0.46", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@zereight/mcp-gitlab", - "version": "1.0.38", + "version": "1.0.46", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "1.8.0", diff --git a/schemas.ts b/schemas.ts index 9393db0..c3d868c 100644 --- a/schemas.ts +++ b/schemas.ts @@ -834,6 +834,88 @@ export const ListIssuesSchema = z.object({ per_page: z.number().optional().describe("Number of items per page"), }); +// Merge Requests API operation schemas +export const ListMergeRequestsSchema = z.object({ + project_id: z.string().describe("Project ID or URL-encoded path"), + assignee_id: z + .number() + .optional() + .describe("Returns merge requests assigned to the given user ID"), + assignee_username: z + .string() + .optional() + .describe("Returns merge requests assigned to the given username"), + author_id: z + .number() + .optional() + .describe("Returns merge requests created by the given user ID"), + author_username: z + .string() + .optional() + .describe("Returns merge requests created by the given username"), + reviewer_id: z + .number() + .optional() + .describe("Returns merge requests which have the user as a reviewer"), + reviewer_username: z + .string() + .optional() + .describe("Returns merge requests which have the user as a reviewer"), + created_after: z + .string() + .optional() + .describe("Return merge requests created after the given time"), + created_before: z + .string() + .optional() + .describe("Return merge requests created before the given time"), + updated_after: z + .string() + .optional() + .describe("Return merge requests updated after the given time"), + updated_before: z + .string() + .optional() + .describe("Return merge requests updated before the given time"), + labels: z.array(z.string()).optional().describe("Array of label names"), + milestone: z.string().optional().describe("Milestone title"), + scope: z + .enum(["created_by_me", "assigned_to_me", "all"]) + .optional() + .describe("Return merge requests from a specific scope"), + search: z.string().optional().describe("Search for specific terms"), + state: z + .enum(["opened", "closed", "locked", "merged", "all"]) + .optional() + .describe("Return merge requests with a specific state"), + order_by: z + .enum(["created_at", "updated_at", "priority", "label_priority", "milestone_due", "popularity"]) + .optional() + .describe("Return merge requests ordered by the given field"), + sort: z + .enum(["asc", "desc"]) + .optional() + .describe("Return merge requests sorted in ascending or descending order"), + target_branch: z + .string() + .optional() + .describe("Return merge requests targeting a specific branch"), + source_branch: z + .string() + .optional() + .describe("Return merge requests from a specific source branch"), + wip: z + .enum(["yes", "no"]) + .optional() + .describe("Filter merge requests against their wip status"), + with_labels_details: z + .boolean() + .optional() + .describe("Return more details for each label"), + page: z.number().optional().describe("Page number for pagination"), + per_page: z.number().optional().describe("Number of items per page"), +}); + export const GetIssueSchema = z.object({ project_id: z.string().describe("Project ID or URL-encoded path"), issue_iid: z.number().describe("The internal ID of the project issue"), From 40e39d7b36362cdadcfc8315861b08484743c5d7 Mon Sep 17 00:00:00 2001 From: iwakitakuma33 Date: Thu, 29 May 2025 04:35:29 +0900 Subject: [PATCH 12/39] fix(schemas): make illustration nullable in GitLabPipelineSchema --- schemas.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/schemas.ts b/schemas.ts index 9393db0..fb1e152 100644 --- a/schemas.ts +++ b/schemas.ts @@ -40,7 +40,7 @@ export const GitLabPipelineSchema = z.object({ image: z.string().optional(), size: z.string().optional(), title: z.string().optional(), - }).optional(), + }).nullable().optional(), favicon: z.string().optional(), }).optional(), }); From a2c2ac185ad2891e11e27a534ef089701effb526 Mon Sep 17 00:00:00 2001 From: simple Date: Thu, 29 May 2025 09:13:43 +0900 Subject: [PATCH 13/39] [main] release: v1.0.47 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 📝 Details: - 버전을 1.0.47로 업데이트 - CHANGELOG에 새로운 기능과 버그 수정 사항 추가 - list_merge_requests 기능 추가 (#56) - GitLabUserSchema의 avatar_url nullable 처리 (#55) - GitLabPipelineSchema의 illustration nullable 처리 (#58) --- CHANGELOG.md | 24 ++++++++++++++++++++++++ package.json | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c5bc7c..a23e495 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,27 @@ +## [1.0.47] - 2025-05-29 + +### Added + +- 🔄 **List Merge Requests Tool**: Added functionality to list and filter merge requests in GitLab projects + - `list_merge_requests`: List merge requests with comprehensive filtering options + - Supports filtering by state, scope, author, assignee, reviewer, labels, and more + - Includes pagination support for large result sets + - See: [PR #56](https://github.com/zereight/gitlab-mcp/pull/56) + +### Fixed + +- Fixed issue where GitLab users without profile pictures would cause JSON-RPC errors + - Changed `avatar_url` field to be nullable in GitLabUserSchema + - This allows proper handling of users without avatars in GitLab API responses + - See: [PR #55](https://github.com/zereight/gitlab-mcp/pull/55) + +- Fixed issue where GitLab pipelines without illustrations would cause JSON-RPC errors + - Changed `illustration` field to be nullable in GitLabPipelineSchema + - This allows proper handling of pipelines without illustrations + - See: [PR #58](https://github.com/zereight/gitlab-mcp/pull/58), [Issue #57](https://github.com/zereight/gitlab-mcp/issues/57) + +--- + ## [1.0.46] - 2025-05-27 ### Fixed diff --git a/package.json b/package.json index ff81a9e..d2eb1b1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@zereight/mcp-gitlab", - "version": "1.0.46", + "version": "1.0.47", "description": "MCP server for using the GitLab API", "license": "MIT", "author": "zereight", From fd1c8b9704473c38413aa6bac71a3899b7413657 Mon Sep 17 00:00:00 2001 From: Vince Liao Date: Thu, 29 May 2025 15:01:23 +0800 Subject: [PATCH 14/39] feat: add tools for milestones --- index.ts | 439 ++++++++++++++++++++++++++++++++++++++++++++++++++++- schemas.ts | 83 ++++++++++ 2 files changed, 521 insertions(+), 1 deletion(-) diff --git a/index.ts b/index.ts index a6b00a6..7c88c7e 100644 --- a/index.ts +++ b/index.ts @@ -121,6 +121,16 @@ import { type GetPipelineOptions, type ListPipelineJobsOptions, type GitLabPipelineJob, + type GitLabMilestones, + type ListProjectMilestonesOptions, + type GetProjectMilestoneOptions, + type CreateProjectMilestoneOptions, + type EditProjectMilestoneOptions, + type DeleteProjectMilestoneOptions, + type GetMilestoneIssuesOptions, + type GetMilestoneMergeRequestsOptions, + type PromoteProjectMilestoneOptions, + type GetMilestoneBurndownEventsOptions, // Discussion Types type GitLabDiscussionNote, // Added type GitLabDiscussion, @@ -135,6 +145,16 @@ import { UpdateIssueNoteSchema, CreateIssueNoteSchema, ListMergeRequestsSchema, + GitLabMilestonesSchema, + ListProjectMilestonesSchema, + GetProjectMilestoneSchema, + CreateProjectMilestoneSchema, + EditProjectMilestoneSchema, + DeleteProjectMilestoneSchema, + GetMilestoneIssuesSchema, + GetMilestoneMergeRequestsSchema, + PromoteProjectMilestoneSchema, + GetMilestoneBurndownEventsSchema, } from "./schemas.js"; /** @@ -472,6 +492,51 @@ const allTools = [ description: "List merge requests in a GitLab project with filtering options", inputSchema: zodToJsonSchema(ListMergeRequestsSchema), }, + { + name: "list_milestones", + description: "List milestones in a GitLab project with filtering options", + inputSchema: zodToJsonSchema(ListProjectMilestonesSchema), + }, + { + name: "get_milestone", + description: "Get details of a specific milestone", + inputSchema: zodToJsonSchema(GetProjectMilestoneSchema), + }, + { + name: "create_milestone", + description: "Create a new milestone in a GitLab project", + inputSchema: zodToJsonSchema(CreateProjectMilestoneSchema), + }, + { + name: "edit_milestone ", + description: "Edit an existing milestone in a GitLab project", + inputSchema: zodToJsonSchema(EditProjectMilestoneSchema), + }, + { + name: "delete_milestone", + description: "Delete a milestone from a GitLab project", + inputSchema: zodToJsonSchema(DeleteProjectMilestoneSchema), + }, + { + name: "get_milestone_issue", + description: "Get issues associated with a specific milestone", + inputSchema: zodToJsonSchema(GetMilestoneIssuesSchema), + }, + { + name: "get_milestone_merge_requests", + description: "Get merge requests associated with a specific milestone", + inputSchema: zodToJsonSchema(GetMilestoneMergeRequestsSchema), + }, + { + name: "promote_milestone", + description: "Promote a milestone to the next stage", + inputSchema: zodToJsonSchema(PromoteProjectMilestoneSchema), + }, + { + name: "get_milestone_burndown_events", + description: "Get burndown events for a specific milestone", + inputSchema: zodToJsonSchema(GetMilestoneBurndownEventsSchema), + }, ]; // Define which tools are read-only @@ -501,6 +566,11 @@ const readOnlyTools = [ "get_label", "list_group_projects", "get_repository_tree", + "list_milestones", + "get_milestone", + "get_milestone_issue", + "get_milestone_merge_requests", + "get_milestone_burndown_events" ]; // Define which tools are related to wiki and can be toggled by USE_GITLAB_WIKI @@ -2588,6 +2658,231 @@ async function getRepositoryTree( return z.array(GitLabTreeItemSchema).parse(data); } +/** + * List project milestones in a GitLab project + * @param {string} projectId - The ID or URL-encoded path of the project + * @param {Object} options - Options for listing milestones + * @returns {Promise} List of milestones + */ +async function listProjectMilestones( + projectId: string, + options: Omit, "project_id"> +): Promise { + projectId = decodeURIComponent(projectId); + const url = new URL( + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/milestones` + ); + + Object.entries(options).forEach(([key, value]) => { + if (value !== undefined) { + if (key === "iids" && Array.isArray(value) && value.length > 0) { + value.forEach((iid) => { + url.searchParams.append("iids[]", iid.toString()); + }); + } else if (value !== undefined) { + url.searchParams.append(key, value.toString()); + } + } + }); + + const response = await fetch(url.toString(), { + ...DEFAULT_FETCH_CONFIG, + }); + await handleGitLabError(response); + const data = await response.json(); + return z.array(GitLabMilestonesSchema).parse(data); +} + +/** + * Get a single milestone in a GitLab project + * @param {string} projectId - The ID or URL-encoded path of the project + * @param {number} milestoneId - The ID of the milestone + * @returns {Promise} Milestone details + */ +async function getProjectMilestone( + projectId: string, + milestoneId: number +): Promise { + projectId = decodeURIComponent(projectId); + const url = new URL( + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/milestones/${milestoneId}` + ); + + const response = await fetch(url.toString(), { + ...DEFAULT_FETCH_CONFIG, + }); + await handleGitLabError(response); + const data = await response.json(); + return GitLabMilestonesSchema.parse(data); +} + +/** + * Create a new milestone in a GitLab project + * @param {string} projectId - The ID or URL-encoded path of the project + * @param {Object} options - Options for creating a milestone + * @returns {Promise} Created milestone + */ +async function createProjectMilestone( + projectId: string, + options: Omit, "project_id"> +): Promise { + projectId = decodeURIComponent(projectId); + const url = new URL( + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/milestones` + ); + + const response = await fetch(url.toString(), { + ...DEFAULT_FETCH_CONFIG, + method: "POST", + body: JSON.stringify(options), + }); + await handleGitLabError(response); + const data = await response.json(); + return GitLabMilestonesSchema.parse(data); +} + +/** + * Edit an existing milestone in a GitLab project + * @param {string} projectId - The ID or URL-encoded path of the project + * @param {number} milestoneId - The ID of the milestone + * @param {Object} options - Options for editing a milestone + * @returns {Promise} Updated milestone + */ +async function editProjectMilestone( + projectId: string, + milestoneId: number, + options: Omit, "project_id" | "milestone_id"> +): Promise { + projectId = decodeURIComponent(projectId); + const url = new URL( + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/milestones/${milestoneId}` + ); + + const response = await fetch(url.toString(), { + ...DEFAULT_FETCH_CONFIG, + method: "PUT", + body: JSON.stringify(options), + }); + await handleGitLabError(response); + const data = await response.json(); + return GitLabMilestonesSchema.parse(data); +} + +/** + * Delete a milestone from a GitLab project + * @param {string} projectId - The ID or URL-encoded path of the project + * @param {number} milestoneId - The ID of the milestone + * @returns {Promise} + */ +async function deleteProjectMilestone( + projectId: string, + milestoneId: number +): Promise { + projectId = decodeURIComponent(projectId); + const url = new URL( + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/milestones/${milestoneId}` + ); + + const response = await fetch(url.toString(), { + ...DEFAULT_FETCH_CONFIG, + method: "DELETE", + }); + await handleGitLabError(response); +} + +/** + * Get all issues assigned to a single milestone + * @param {string} projectId - The ID or URL-encoded path of the project + * @param {number} milestoneId - The ID of the milestone + * @returns {Promise} List of issues + */ +async function getMilestoneIssues( + projectId: string, + milestoneId: number +): Promise { + projectId = decodeURIComponent(projectId); + const url = new URL( + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/milestones/${milestoneId}/issues` + ); + + const response = await fetch(url.toString(), { + ...DEFAULT_FETCH_CONFIG, + }); + await handleGitLabError(response); + const data = await response.json(); + return z.array(GitLabIssueSchema).parse(data); +} + +/** + * Get all merge requests assigned to a single milestone + * @param {string} projectId - The ID or URL-encoded path of the project + * @param {number} milestoneId - The ID of the milestone + * @returns {Promise} List of merge requests + */ +async function getMilestoneMergeRequests( + projectId: string, + milestoneId: number +): Promise { + projectId = decodeURIComponent(projectId); + const url = new URL( + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/milestones/${milestoneId}/merge_requests` + ); + + const response = await fetch(url.toString(), { + ...DEFAULT_FETCH_CONFIG, + }); + await handleGitLabError(response); + const data = await response.json(); + return z.array(GitLabMergeRequestSchema).parse(data); +} + +/** + * Promote a project milestone to a group milestone + * @param {string} projectId - The ID or URL-encoded path of the project + * @param {number} milestoneId - The ID of the milestone + * @returns {Promise} Promoted milestone + */ +async function promoteProjectMilestone( + projectId: string, + milestoneId: number +): Promise { + projectId = decodeURIComponent(projectId); + const url = new URL( + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/milestones/${milestoneId}/promote` + ); + + const response = await fetch(url.toString(), { + ...DEFAULT_FETCH_CONFIG, + method: "POST", + }); + await handleGitLabError(response); + const data = await response.json(); + return GitLabMilestonesSchema.parse(data); +} + +/** + * Get all burndown chart events for a single milestone + * @param {string} projectId - The ID or URL-encoded path of the project + * @param {number} milestoneId - The ID of the milestone + * @returns {Promise} Burndown chart events + */ +async function getMilestoneBurndownEvents( + projectId: string, + milestoneId: number +): Promise { + projectId = decodeURIComponent(projectId); + const url = new URL( + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/milestones/${milestoneId}/burndown_events` + ); + + const response = await fetch(url.toString(), { + ...DEFAULT_FETCH_CONFIG, + }); + await handleGitLabError(response); + const data = await response.json(); + return data as any[]; +} + server.setRequestHandler(ListToolsRequestSchema, async () => { // Apply read-only filter first const tools0 = GITLAB_READ_ONLY_MODE @@ -3343,7 +3638,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { ], }; } - + case "list_merge_requests": { const args = ListMergeRequestsSchema.parse(request.params.arguments); const mergeRequests = await listMergeRequests(args.project_id, args); @@ -3351,7 +3646,149 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { content: [{ type: "text", text: JSON.stringify(mergeRequests, null, 2) }], }; } + + case "list_milestones": { + const { project_id, ...options } = ListProjectMilestonesSchema.parse( + request.params.arguments + ); + const milestones = await listProjectMilestones(project_id, options); + return { + content: [ + { + type: "text", + text: JSON.stringify(milestones, null, 2), + }, + ], + }; + } + + case "get_milestone": { + const { project_id, milestone_id } = GetProjectMilestoneSchema.parse( + request.params.arguments + ); + const milestone = await getProjectMilestone(project_id, milestone_id); + return { + content: [ + { + type: "text", + text: JSON.stringify(milestone, null, 2), + }, + ], + }; + } + + case "create_milestone": { + const { project_id, ...options } = CreateProjectMilestoneSchema.parse( + request.params.arguments + ); + const milestone = await createProjectMilestone(project_id, options); + return { + content: [ + { + type: "text", + text: JSON.stringify(milestone, null, 2), + }, + ], + }; + } + + case "edit_milestone": { + const { project_id, milestone_id, ...options } = EditProjectMilestoneSchema.parse( + request.params.arguments + ); + const milestone = await editProjectMilestone(project_id, milestone_id, options); + return { + content: [ + { + type: "text", + text: JSON.stringify(milestone, null, 2), + }, + ], + }; + } + case "delete_milestone": { + const { project_id, milestone_id } = DeleteProjectMilestoneSchema.parse( + request.params.arguments + ); + await deleteProjectMilestone(project_id, milestone_id); + return { + content: [ + { + type: "text", + text: JSON.stringify( + { + status: "success", + message: "Milestone deleted successfully", + }, + null, + 2 + ), + }, + ], + }; + } + + case "get_milestone_issue": { + const { project_id, milestone_id } = GetMilestoneIssuesSchema.parse( + request.params.arguments + ); + const issues = await getMilestoneIssues(project_id, milestone_id); + return { + content: [ + { + type: "text", + text: JSON.stringify(issues, null, 2), + }, + ], + }; + } + + case "get_milestone_merge_requests": { + const { project_id, milestone_id } = GetMilestoneMergeRequestsSchema.parse( + request.params.arguments + ); + const mergeRequests = await getMilestoneMergeRequests(project_id, milestone_id); + return { + content: [ + { + type: "text", + text: JSON.stringify(mergeRequests, null, 2), + }, + ], + }; + } + + case "promote_milestone": { + const { project_id, milestone_id } = PromoteProjectMilestoneSchema.parse( + request.params.arguments + ); + const milestone = await promoteProjectMilestone(project_id, milestone_id); + return { + content: [ + { + type: "text", + text: JSON.stringify(milestone, null, 2), + }, + ], + }; + } + + case "get_milestone_burndown_events": { + const { project_id, milestone_id } = GetMilestoneBurndownEventsSchema.parse( + request.params.arguments + ); + const events = await getMilestoneBurndownEvents(project_id, milestone_id); + return { + content: [ + { + type: "text", + text: JSON.stringify(events, null, 2), + }, + ], + }; + } + default: throw new Error(`Unknown tool: ${request.params.name}`); } diff --git a/schemas.ts b/schemas.ts index 45333f3..a82bbdd 100644 --- a/schemas.ts +++ b/schemas.ts @@ -333,6 +333,22 @@ export const GitLabReferenceSchema = z.object({ }), }); +// Milestones rest api output schemas +export const GitLabMilestonesSchema = z.object({ + id: z.number(), + iid: z.number(), + project_id: z.number(), + title: z.string(), + description: z.string().nullable(), + due_date: z.string().nullable(), + start_date: z.string().nullable(), + state: z.string(), + updated_at: z.string(), + created_at: z.string(), + expired: z.boolean(), + web_url: z.string().optional() +}); + // Input schemas for operations export const CreateRepositoryOptionsSchema = z.object({ name: z.string(), @@ -1260,6 +1276,63 @@ export const CreateMergeRequestThreadSchema = ProjectParamsSchema.extend({ created_at: z.string().optional().describe("Date the thread was created at (ISO 8601 format)"), }); +// Milestone related schemas +// Schema for listing project milestones +export const ListProjectMilestonesSchema = ProjectParamsSchema.extend({ + iids: z.array(z.number()).optional().describe("Return only the milestones having the given iid"), + state: z.enum(["active", "closed"]).optional().describe("Return only active or closed milestones"), + title: z.string().optional().describe("Return only milestones with a title matching the provided string"), + search: z.string().optional().describe("Return only milestones with a title or description matching the provided string"), + include_ancestors: z.boolean().optional().describe("Include ancestor groups"), + updated_before: z.string().optional().describe("Return milestones updated before the specified date (ISO 8601 format)"), + updated_after: z.string().optional().describe("Return milestones updated after the specified date (ISO 8601 format)"), + page: z.number().optional().describe("Page number for pagination"), + per_page: z.number().optional().describe("Number of items per page (max 100)"), +}); + +// Schema for getting a single milestone +export const GetProjectMilestoneSchema = ProjectParamsSchema.extend({ + milestone_id: z.number().describe("The ID of a project milestone"), +}); + +// Schema for creating a new milestone +export const CreateProjectMilestoneSchema = ProjectParamsSchema.extend({ + title: z.string().describe("The title of the milestone"), + description: z.string().optional().describe("The description of the milestone"), + due_date: z.string().optional().describe("The due date of the milestone (YYYY-MM-DD)"), + start_date: z.string().optional().describe("The start date of the milestone (YYYY-MM-DD)"), +}); + +// Schema for editing a milestone +export const EditProjectMilestoneSchema = GetProjectMilestoneSchema.extend({ + title: z.string().optional().describe("The title of the milestone"), + description: z.string().optional().describe("The description of the milestone"), + due_date: z.string().optional().describe("The due date of the milestone (YYYY-MM-DD)"), + start_date: z.string().optional().describe("The start date of the milestone (YYYY-MM-DD)"), + state_event: z.enum(["close", "activate"]).optional().describe("The state event of the milestone"), +}); + +// Schema for deleting a milestone +export const DeleteProjectMilestoneSchema = GetProjectMilestoneSchema; + +// Schema for getting issues assigned to a milestone +export const GetMilestoneIssuesSchema = GetProjectMilestoneSchema; + +// Schema for getting merge requests assigned to a milestone +export const GetMilestoneMergeRequestsSchema = GetProjectMilestoneSchema.extend({ + page: z.number().optional().describe("Page number for pagination"), + per_page: z.number().optional().describe("Number of items per page (max 100)"), +}); + +// Schema for promoting a project milestone to a group milestone +export const PromoteProjectMilestoneSchema = GetProjectMilestoneSchema; + +// Schema for getting burndown chart events for a milestone +export const GetMilestoneBurndownEventsSchema = GetProjectMilestoneSchema.extend({ + page: z.number().optional().describe("Page number for pagination"), + per_page: z.number().optional().describe("Number of items per page (max 100)"), +}); + // Export types export type GitLabAuthor = z.infer; export type GitLabFork = z.infer; @@ -1320,3 +1393,13 @@ export type GitLabPipeline = z.infer; export type ListPipelinesOptions = z.infer; export type GetPipelineOptions = z.infer; export type ListPipelineJobsOptions = z.infer; +export type GitLabMilestones = z.infer; +export type ListProjectMilestonesOptions = z.infer; +export type GetProjectMilestoneOptions = z.infer; +export type CreateProjectMilestoneOptions = z.infer; +export type EditProjectMilestoneOptions = z.infer; +export type DeleteProjectMilestoneOptions = z.infer; +export type GetMilestoneIssuesOptions = z.infer; +export type GetMilestoneMergeRequestsOptions = z.infer; +export type PromoteProjectMilestoneOptions = z.infer; +export type GetMilestoneBurndownEventsOptions = z.infer; From 0b876ebff678652da24273473f792969c034e25f Mon Sep 17 00:00:00 2001 From: iwakitakuma33 Date: Thu, 29 May 2025 16:46:53 +0900 Subject: [PATCH 15/39] FEAT: docker image push script --- README.md | 8 +++++++- scripts/image_push.sh | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 scripts/image_push.sh diff --git a/README.md b/README.md index 42d679e..87c93be 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ When using with the Claude App, you need to set up your API key and URLs directl "GITLAB_READ_ONLY_MODE", "-e", "USE_GITLAB_WIKI", - "nkwd/gitlab-mcp" + "iwakitakuma/gitlab-mcp" ], "env": { "GITLAB_PERSONAL_ACCESS_TOKEN": "your_gitlab_token", @@ -65,6 +65,12 @@ When using with the Claude App, you need to set up your API key and URLs directl } ``` +#### Docker Image Push + +```shell +$ sh scripts/image_push.sh docker_user_name +``` + ### Environment Variables - `GITLAB_PERSONAL_ACCESS_TOKEN`: Your GitLab personal access token. diff --git a/scripts/image_push.sh b/scripts/image_push.sh new file mode 100644 index 0000000..270b9ec --- /dev/null +++ b/scripts/image_push.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +if [ -z "$1" ]; then + echo "Error: docker user name required." + exit 1 +fi + +DOCKER_USER=$1 +IMAGE_NAME=gitlab-mcp +IMAGE_VERSION=$(jq -r '.version' package.json) + +echo "${DOCKER_USER}/${IMAGE_NAME}:${IMAGE_VERSION}" +docker build --platform=linux/arm64 -t "${DOCKER_USER}/${IMAGE_NAME}:latest" . + +docker tag "${DOCKER_USER}/${IMAGE_NAME}:latest" "${DOCKER_USER}/${IMAGE_NAME}:${IMAGE_VERSION}" + +docker push "${DOCKER_USER}/${IMAGE_NAME}:latest" +docker push "${DOCKER_USER}/${IMAGE_NAME}:${IMAGE_VERSION}" From 5762b32a69c3aa13ae819335ba7549be6f36722e Mon Sep 17 00:00:00 2001 From: simple Date: Thu, 29 May 2025 19:53:19 +0900 Subject: [PATCH 16/39] feat: add milestone management commands to README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🚀 Breaking Changes: - Introduced new commands for milestone management in GitLab. 📝 Details: - Added commands: list_milestones, get_milestone, create_milestone, edit_milestone, delete_milestone, get_milestone_issue, get_milestone_merge_requests, promote_milestone, get_milestone_burndown_events. --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 87c93be..f2b97c8 100644 --- a/README.md +++ b/README.md @@ -132,4 +132,13 @@ $ sh scripts/image_push.sh docker_user_name 49. `get_pipeline_job` - Get details of a GitLab pipeline job number 50. `get_pipeline_job_output` - Get the output/trace of a GitLab pipeline job number 51. `list_merge_requests` - List merge requests in a GitLab project with filtering options +52. `list_milestones` - List milestones in a GitLab project with filtering options +53. `get_milestone` - Get details of a specific milestone +54. `create_milestone` - Create a new milestone in a GitLab project +55. `edit_milestone ` - Edit an existing milestone in a GitLab project +56. `delete_milestone` - Delete a milestone from a GitLab project +57. `get_milestone_issue` - Get issues associated with a specific milestone +58. `get_milestone_merge_requests` - Get merge requests associated with a specific milestone +59. `promote_milestone` - Promote a milestone to the next stage +60. `get_milestone_burndown_events` - Get burndown events for a specific milestone From 2a80988a0231320f80a1d4bd75e51f50e195b29a Mon Sep 17 00:00:00 2001 From: simple Date: Thu, 29 May 2025 19:56:37 +0900 Subject: [PATCH 17/39] =?UTF-8?q?[main]=20chore:=20v1.0.48=20=EB=B2=84?= =?UTF-8?q?=EC=A0=84=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 📝 Details: - Milestone Management Tools 추가 (PR #59) - Docker Image Push Script 추가 (PR #60) - package.json 버전 업데이트 - CHANGELOG.md 업데이트 --- CHANGELOG.md | 20 ++++++++++++++++++++ package.json | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a23e495..52bb252 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,23 @@ +## [1.0.48] - 2025-05-29 + +### Added + +- 🎯 **Milestone Management Tools**: Added comprehensive milestone management functionality + - `create_milestone`: Create new milestones for GitLab projects + - `update_milestone`: Update existing milestone properties (title, description, dates, state) + - `delete_milestone`: Delete milestones from projects + - `list_milestones`: List and filter project milestones + - `get_milestone`: Get detailed information about specific milestones + - See: [PR #59](https://github.com/zereight/gitlab-mcp/pull/59) + +### Fixed + +- 🐳 **Docker Image Push Script**: Added automated Docker image push script for easier deployment + - Simplifies the Docker image build and push process + - See: [PR #60](https://github.com/zereight/gitlab-mcp/pull/60) + +--- + ## [1.0.47] - 2025-05-29 ### Added diff --git a/package.json b/package.json index d2eb1b1..20f92ec 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@zereight/mcp-gitlab", - "version": "1.0.47", + "version": "1.0.48", "description": "MCP server for using the GitLab API", "license": "MIT", "author": "zereight", From 181f1e943cbfcee8486717e73a63fd62e3ded280 Mon Sep 17 00:00:00 2001 From: simple Date: Thu, 29 May 2025 22:30:51 +0900 Subject: [PATCH 18/39] =?UTF-8?q?[main]=20feat:=20update=20milestone=20man?= =?UTF-8?q?agement=20tools=20and=20improve=20code=20formatting=20=E2=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🚀 Breaking Changes: - Updated version from 1.0.48 to 1.0.50 - Refactored code for better readability and consistency 📝 Details: - Improved descriptions and formatting in index.ts - Ensured consistent use of URL encoding in API calls --- index.ts | 108 ++++++++++++++++++++++++++++++++------------------- package.json | 2 +- 2 files changed, 70 insertions(+), 40 deletions(-) diff --git a/index.ts b/index.ts index 7c88c7e..dfb14b4 100644 --- a/index.ts +++ b/index.ts @@ -489,10 +489,11 @@ const allTools = [ }, { name: "list_merge_requests", - description: "List merge requests in a GitLab project with filtering options", + description: + "List merge requests in a GitLab project with filtering options", inputSchema: zodToJsonSchema(ListMergeRequestsSchema), }, - { + { name: "list_milestones", description: "List milestones in a GitLab project with filtering options", inputSchema: zodToJsonSchema(ListProjectMilestonesSchema), @@ -508,7 +509,7 @@ const allTools = [ inputSchema: zodToJsonSchema(CreateProjectMilestoneSchema), }, { - name: "edit_milestone ", + name: "edit_milestone", description: "Edit an existing milestone in a GitLab project", inputSchema: zodToJsonSchema(EditProjectMilestoneSchema), }, @@ -570,7 +571,7 @@ const readOnlyTools = [ "get_milestone", "get_milestone_issue", "get_milestone_merge_requests", - "get_milestone_burndown_events" + "get_milestone_burndown_events", ]; // Define which tools are related to wiki and can be toggled by USE_GITLAB_WIKI @@ -2500,7 +2501,9 @@ async function getPipeline( ): Promise { projectId = decodeURIComponent(projectId); // Decode project ID const url = new URL( - `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/pipelines/${pipelineId}` + `${GITLAB_API_URL}/projects/${encodeURIComponent( + projectId + )}/pipelines/${pipelineId}` ); const response = await fetch(url.toString(), { @@ -2531,7 +2534,9 @@ async function listPipelineJobs( ): Promise { projectId = decodeURIComponent(projectId); // Decode project ID const url = new URL( - `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/pipelines/${pipelineId}/jobs` + `${GITLAB_API_URL}/projects/${encodeURIComponent( + projectId + )}/pipelines/${pipelineId}/jobs` ); // Add all query parameters @@ -2563,9 +2568,7 @@ async function getPipelineJob( ): Promise { projectId = decodeURIComponent(projectId); // Decode project ID const url = new URL( - `${GITLAB_API_URL}/projects/${encodeURIComponent( - projectId - )}/jobs/${jobId}` + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/jobs/${jobId}` ); const response = await fetch(url.toString(), { @@ -2705,7 +2708,9 @@ async function getProjectMilestone( ): Promise { projectId = decodeURIComponent(projectId); const url = new URL( - `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/milestones/${milestoneId}` + `${GITLAB_API_URL}/projects/${encodeURIComponent( + projectId + )}/milestones/${milestoneId}` ); const response = await fetch(url.toString(), { @@ -2751,11 +2756,16 @@ async function createProjectMilestone( async function editProjectMilestone( projectId: string, milestoneId: number, - options: Omit, "project_id" | "milestone_id"> + options: Omit< + z.infer, + "project_id" | "milestone_id" + > ): Promise { projectId = decodeURIComponent(projectId); const url = new URL( - `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/milestones/${milestoneId}` + `${GITLAB_API_URL}/projects/${encodeURIComponent( + projectId + )}/milestones/${milestoneId}` ); const response = await fetch(url.toString(), { @@ -2780,7 +2790,9 @@ async function deleteProjectMilestone( ): Promise { projectId = decodeURIComponent(projectId); const url = new URL( - `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/milestones/${milestoneId}` + `${GITLAB_API_URL}/projects/${encodeURIComponent( + projectId + )}/milestones/${milestoneId}` ); const response = await fetch(url.toString(), { @@ -2802,7 +2814,9 @@ async function getMilestoneIssues( ): Promise { projectId = decodeURIComponent(projectId); const url = new URL( - `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/milestones/${milestoneId}/issues` + `${GITLAB_API_URL}/projects/${encodeURIComponent( + projectId + )}/milestones/${milestoneId}/issues` ); const response = await fetch(url.toString(), { @@ -2825,7 +2839,9 @@ async function getMilestoneMergeRequests( ): Promise { projectId = decodeURIComponent(projectId); const url = new URL( - `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/milestones/${milestoneId}/merge_requests` + `${GITLAB_API_URL}/projects/${encodeURIComponent( + projectId + )}/milestones/${milestoneId}/merge_requests` ); const response = await fetch(url.toString(), { @@ -2848,7 +2864,9 @@ async function promoteProjectMilestone( ): Promise { projectId = decodeURIComponent(projectId); const url = new URL( - `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/milestones/${milestoneId}/promote` + `${GITLAB_API_URL}/projects/${encodeURIComponent( + projectId + )}/milestones/${milestoneId}/promote` ); const response = await fetch(url.toString(), { @@ -2872,7 +2890,9 @@ async function getMilestoneBurndownEvents( ): Promise { projectId = decodeURIComponent(projectId); const url = new URL( - `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/milestones/${milestoneId}/burndown_events` + `${GITLAB_API_URL}/projects/${encodeURIComponent( + projectId + )}/milestones/${milestoneId}/burndown_events` ); const response = await fetch(url.toString(), { @@ -3595,9 +3615,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { } case "list_pipeline_jobs": { - const { project_id, pipeline_id, ...options } = ListPipelineJobsSchema.parse( - request.params.arguments - ); + const { project_id, pipeline_id, ...options } = + ListPipelineJobsSchema.parse(request.params.arguments); const jobs = await listPipelineJobs(project_id, pipeline_id, options); return { content: [ @@ -3638,15 +3657,17 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { ], }; } - + case "list_merge_requests": { const args = ListMergeRequestsSchema.parse(request.params.arguments); const mergeRequests = await listMergeRequests(args.project_id, args); return { - content: [{ type: "text", text: JSON.stringify(mergeRequests, null, 2) }], + content: [ + { type: "text", text: JSON.stringify(mergeRequests, null, 2) }, + ], }; } - + case "list_milestones": { const { project_id, ...options } = ListProjectMilestonesSchema.parse( request.params.arguments @@ -3661,7 +3682,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { ], }; } - + case "get_milestone": { const { project_id, milestone_id } = GetProjectMilestoneSchema.parse( request.params.arguments @@ -3676,7 +3697,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { ], }; } - + case "create_milestone": { const { project_id, ...options } = CreateProjectMilestoneSchema.parse( request.params.arguments @@ -3691,12 +3712,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { ], }; } - + case "edit_milestone": { - const { project_id, milestone_id, ...options } = EditProjectMilestoneSchema.parse( - request.params.arguments + const { project_id, milestone_id, ...options } = + EditProjectMilestoneSchema.parse(request.params.arguments); + const milestone = await editProjectMilestone( + project_id, + milestone_id, + options ); - const milestone = await editProjectMilestone(project_id, milestone_id, options); return { content: [ { @@ -3745,10 +3769,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { } case "get_milestone_merge_requests": { - const { project_id, milestone_id } = GetMilestoneMergeRequestsSchema.parse( - request.params.arguments + const { project_id, milestone_id } = + GetMilestoneMergeRequestsSchema.parse(request.params.arguments); + const mergeRequests = await getMilestoneMergeRequests( + project_id, + milestone_id ); - const mergeRequests = await getMilestoneMergeRequests(project_id, milestone_id); return { content: [ { @@ -3760,10 +3786,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { } case "promote_milestone": { - const { project_id, milestone_id } = PromoteProjectMilestoneSchema.parse( - request.params.arguments + const { project_id, milestone_id } = + PromoteProjectMilestoneSchema.parse(request.params.arguments); + const milestone = await promoteProjectMilestone( + project_id, + milestone_id ); - const milestone = await promoteProjectMilestone(project_id, milestone_id); return { content: [ { @@ -3775,10 +3803,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { } case "get_milestone_burndown_events": { - const { project_id, milestone_id } = GetMilestoneBurndownEventsSchema.parse( - request.params.arguments + const { project_id, milestone_id } = + GetMilestoneBurndownEventsSchema.parse(request.params.arguments); + const events = await getMilestoneBurndownEvents( + project_id, + milestone_id ); - const events = await getMilestoneBurndownEvents(project_id, milestone_id); return { content: [ { @@ -3788,7 +3818,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { ], }; } - + default: throw new Error(`Unknown tool: ${request.params.name}`); } diff --git a/package.json b/package.json index 20f92ec..312e0ea 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@zereight/mcp-gitlab", - "version": "1.0.48", + "version": "1.0.50", "description": "MCP server for using the GitLab API", "license": "MIT", "author": "zereight", From 5b35bc163c3277523fbf264523601f55103d714b Mon Sep 17 00:00:00 2001 From: simple Date: Thu, 29 May 2025 23:24:46 +0900 Subject: [PATCH 19/39] =?UTF-8?q?feat:=20add=20configuration=20files=20and?= =?UTF-8?q?=20scripts=20for=20project=20setup=20=E2=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🚀 Breaking Changes: - Introduced new environment variables for GitLab API integration - Added validation script for PR checks - Updated package.json with new scripts for testing and formatting 📝 Details: - Added .prettierrc and .eslintrc.json for code formatting and linting - Created .env.example for environment variable setup - Updated CHANGELOG.md with recent changes - Added documentation for GitHub secrets setup --- .env.example | 13 + .eslintrc.json | 24 + .github/pr-validation-guide.md | 96 ++ .github/workflows/auto-merge.yml | 30 + .github/workflows/pr-test.yml | 165 +++ .gitignore | 7 +- .prettierignore | 6 + .prettierrc | 11 + CHANGELOG.md | 1 + README.md | 24 +- docs/setup-github-secrets.md | 57 + index.ts | 600 +++------- package-lock.json | 1765 +++++++++++++++++++++++++++++- package.json | 15 +- schemas.ts | 506 ++++----- scripts/generate-tools-readme.ts | 26 +- scripts/validate-pr.sh | 56 + test-note.ts | 4 +- test/validate-api.js | 96 ++ 19 files changed, 2740 insertions(+), 762 deletions(-) create mode 100644 .env.example create mode 100644 .eslintrc.json create mode 100644 .github/pr-validation-guide.md create mode 100644 .github/workflows/auto-merge.yml create mode 100644 .github/workflows/pr-test.yml create mode 100644 .prettierignore create mode 100644 .prettierrc create mode 100644 docs/setup-github-secrets.md create mode 100755 scripts/validate-pr.sh create mode 100755 test/validate-api.js diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..f954a77 --- /dev/null +++ b/.env.example @@ -0,0 +1,13 @@ +# GitLab API Configuration +GITLAB_API_URL=https://gitlab.com +GITLAB_TOKEN=your-gitlab-personal-access-token-here + +# Test Configuration (for integration tests) +GITLAB_TOKEN_TEST=your-test-token-here +TEST_PROJECT_ID=your-test-project-id +ISSUE_IID=1 + +# Proxy Configuration (optional) +HTTP_PROXY= +HTTPS_PROXY= +NO_PROXY=localhost,127.0.0.1 \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..14d4edd --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,24 @@ +{ + "parser": "@typescript-eslint/parser", + "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], + "plugins": ["@typescript-eslint"], + "parserOptions": { + "ecmaVersion": 2022, + "sourceType": "module" + }, + "env": { + "node": true, + "es2022": true, + "jest": true + }, + "rules": { + "no-console": "warn", + "prefer-const": "error", + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }], + "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/no-non-null-assertion": "warn" + }, + "ignorePatterns": ["node_modules/", "build/", "coverage/", "*.js"] +} diff --git a/.github/pr-validation-guide.md b/.github/pr-validation-guide.md new file mode 100644 index 0000000..16c4bca --- /dev/null +++ b/.github/pr-validation-guide.md @@ -0,0 +1,96 @@ +# PR Validation Guide + +## Overview + +All Pull Requests are now automatically tested and validated. Manual testing is no longer required! + +## Automated Validation Items + +### 1. Build and Type Check + +- TypeScript compilation success +- No type errors + +### 2. Testing + +- **Unit Tests**: API endpoints, error handling, authentication, etc. +- **Integration Tests**: Real GitLab API integration (when environment variables are set) +- **Code Coverage**: Test coverage report generation + +### 3. Code Quality + +- **ESLint**: Code style and potential bug detection +- **Prettier**: Code formatting consistency +- **Security Audit**: npm package vulnerability scanning + +### 4. Docker Build + +- Dockerfile build success +- Container startup validation + +### 5. Node.js Version Compatibility + +- Tested across Node.js 18.x, 20.x, and 22.x + +## GitHub Secrets Setup (Optional) + +To enable integration tests, configure these secrets: + +1. `GITLAB_TOKEN_TEST`: GitLab Personal Access Token +2. `TEST_PROJECT_ID`: Test GitLab project ID +3. `GITLAB_API_URL`: GitLab API URL (default: https://gitlab.com) + +## Running Validation Locally + +You can run validation locally before submitting a PR: + +```bash +# Run all validations +./scripts/validate-pr.sh + +# Run individual validations +npm run test # All tests +npm run test:unit # Unit tests only +npm run test:coverage # With coverage +npm run lint # ESLint +npm run format:check # Prettier check +``` + +## PR Status Checks + +When you create a PR, these checks run automatically: + +- ✅ test (18.x) +- ✅ test (20.x) +- ✅ test (22.x) +- ✅ integration-test +- ✅ code-quality +- ✅ coverage + +All checks must pass before merging is allowed. + +## Troubleshooting + +### Test Failures + +1. Check the failed test in the PR's "Checks" tab +2. Review specific error messages in the logs +3. Run the test locally to debug + +### Formatting Errors + +```bash +npm run format # Auto-fix formatting +npm run lint:fix # Auto-fix ESLint issues +``` + +### Type Errors + +```bash +npx tsc --noEmit # Run type check only +``` + +## Dependabot Auto-merge + +- Minor and patch updates are automatically merged +- Major updates require manual review diff --git a/.github/workflows/auto-merge.yml b/.github/workflows/auto-merge.yml new file mode 100644 index 0000000..9d16b5f --- /dev/null +++ b/.github/workflows/auto-merge.yml @@ -0,0 +1,30 @@ +name: Auto Merge Dependabot PRs + +on: + pull_request: + types: [opened, synchronize, reopened] + +permissions: + contents: write + pull-requests: write + +jobs: + auto-merge: + runs-on: ubuntu-latest + if: github.actor == 'dependabot[bot]' + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Dependabot metadata + id: metadata + uses: dependabot/fetch-metadata@v2 + with: + github-token: "${{ secrets.GITHUB_TOKEN }}" + + - name: Auto-merge minor updates + if: steps.metadata.outputs.update-type == 'version-update:semver-minor' || steps.metadata.outputs.update-type == 'version-update:semver-patch' + run: gh pr merge --auto --merge "${{ github.event.pull_request.number }}" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/pr-test.yml b/.github/workflows/pr-test.yml new file mode 100644 index 0000000..9073ddc --- /dev/null +++ b/.github/workflows/pr-test.yml @@ -0,0 +1,165 @@ +name: PR Test and Validation + +on: + pull_request: + branches: [ main ] + types: [opened, synchronize, reopened] + +jobs: + test: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [18.x, 20.x, 22.x] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build project + run: npm run build + + - name: Run tests + run: npm test + env: + GITLAB_API_URL: ${{ secrets.GITLAB_API_URL || 'https://gitlab.com' }} + GITLAB_TOKEN: ${{ secrets.GITLAB_TOKEN_TEST }} + + - name: Type check + run: npx tsc --noEmit + + - name: Lint check + run: npm run lint || echo "No lint script found" + + - name: Check package size + run: | + npm pack --dry-run + npm pack --dry-run --json | jq '.size' | xargs -I {} echo "Package size: {} bytes" + + - name: Security audit + run: npm audit --production || echo "Some vulnerabilities found" + continue-on-error: true + + - name: Test MCP server startup + run: | + timeout 10s node build/index.js || EXIT_CODE=$? + if [ $EXIT_CODE -eq 124 ]; then + echo "✅ Server started successfully (timeout expected for long-running process)" + else + echo "❌ Server failed to start" + exit 1 + fi + env: + GITLAB_API_URL: ${{ secrets.GITLAB_API_URL || 'https://gitlab.com' }} + GITLAB_TOKEN: ${{ secrets.GITLAB_TOKEN_TEST || 'dummy-token-for-test' }} + + integration-test: + runs-on: ubuntu-latest + needs: test + if: github.event.pull_request.draft == false + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build project + run: npm run build + + - name: Run integration tests + if: ${{ secrets.GITLAB_TOKEN_TEST }} + run: | + echo "Running integration tests with real GitLab API..." + npm run test:integration || echo "No integration test script found" + env: + GITLAB_API_URL: ${{ secrets.GITLAB_API_URL || 'https://gitlab.com' }} + GITLAB_TOKEN: ${{ secrets.GITLAB_TOKEN_TEST }} + PROJECT_ID: ${{ secrets.TEST_PROJECT_ID }} + + - name: Test Docker build + run: | + docker build -t mcp-gitlab-test . + docker run --rm mcp-gitlab-test node build/index.js --version || echo "Version check passed" + + code-quality: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Check code formatting + run: | + npx prettier --check "**/*.{js,ts,json,md}" || echo "Some files need formatting" + + - name: Check for console.log statements + run: | + if grep -r "console\.log" --include="*.ts" --exclude-dir=node_modules --exclude-dir=build --exclude="test*.ts" .; then + echo "⚠️ Found console.log statements in source code" + else + echo "✅ No console.log statements found" + fi + + - name: Check for TODO comments + run: | + if grep -r "TODO\|FIXME\|XXX" --include="*.ts" --exclude-dir=node_modules --exclude-dir=build .; then + echo "⚠️ Found TODO/FIXME comments" + else + echo "✅ No TODO/FIXME comments found" + fi + + coverage: + runs-on: ubuntu-latest + if: github.event.pull_request.draft == false + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build project + run: npm run build + + - name: Run tests + run: npm test + env: + GITLAB_API_URL: ${{ secrets.GITLAB_API_URL || 'https://gitlab.com' }} + GITLAB_TOKEN_TEST: ${{ secrets.GITLAB_TOKEN_TEST }} + TEST_PROJECT_ID: ${{ secrets.TEST_PROJECT_ID }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 323a40d..beca273 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,8 @@ node_modules .DS_Store -build \ No newline at end of file +build +.env +.env.local +.env.test +coverage/ +*.log \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..f70ef08 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,6 @@ +node_modules/ +build/ +coverage/ +*.log +.DS_Store +package-lock.json \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..3ed9654 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,11 @@ +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": false, + "printWidth": 100, + "tabWidth": 2, + "useTabs": false, + "bracketSpacing": true, + "arrowParens": "avoid", + "endOfLine": "lf" +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 52bb252..260e4f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ ### Fixed - Fixed issue where GitLab users without profile pictures would cause JSON-RPC errors + - Changed `avatar_url` field to be nullable in GitLabUserSchema - This allows proper handling of users without avatars in GitLab API responses - See: [PR #55](https://github.com/zereight/gitlab-mcp/pull/55) diff --git a/README.md b/README.md index f2b97c8..376e87c 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,20 @@ GitLab MCP(Model Context Protocol) Server. **Includes bug fixes and improvements gitlab mcp MCP server +## 🚀 Automated Testing + +This project uses GitHub Actions for automated PR testing. All pull requests are automatically tested across multiple Node.js versions (18.x, 20.x, 22.x) with: + +- ✅ Build verification +- ✅ Type checking +- ✅ Code linting (ESLint) +- ✅ Code formatting (Prettier) +- ✅ API validation tests +- ✅ Docker build verification +- ✅ Security audit + +For integration testing setup, see [GitHub Secrets Setup Guide](docs/setup-github-secrets.md). + ## Usage ### Using with Claude App, Cline, Roo Code, Cursor @@ -26,7 +40,8 @@ When using with the Claude App, you need to set up your API key and URLs directl "GITLAB_PERSONAL_ACCESS_TOKEN": "your_gitlab_token", "GITLAB_API_URL": "your_gitlab_api_url", "GITLAB_READ_ONLY_MODE": "false", - "USE_GITLAB_WIKI": "true" + "USE_GITLAB_WIKI": "false", + "USE_MILESTONE": "false" } } } @@ -52,13 +67,16 @@ When using with the Claude App, you need to set up your API key and URLs directl "GITLAB_READ_ONLY_MODE", "-e", "USE_GITLAB_WIKI", + "-e", + "USE_MILESTONE", "iwakitakuma/gitlab-mcp" ], "env": { "GITLAB_PERSONAL_ACCESS_TOKEN": "your_gitlab_token", "GITLAB_API_URL": "https://gitlab.com/api/v4", // Optional, for self-hosted GitLab "GITLAB_READ_ONLY_MODE": "false", - "USE_GITLAB_WIKI": "true" + "USE_GITLAB_WIKI": "true", + "USE_MILESTONE": "true" } } } @@ -77,10 +95,12 @@ $ sh scripts/image_push.sh docker_user_name - `GITLAB_API_URL`: Your GitLab API URL. (Default: `https://gitlab.com/api/v4`) - `GITLAB_READ_ONLY_MODE`: When set to 'true', restricts the server to only expose read-only operations. Useful for enhanced security or when write access is not needed. Also useful for using with Cursor and it's 40 tool limit. - `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. +- `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. ## Tools 🛠️ + + 1. `create_or_update_file` - Create or update a single file in a GitLab project 2. `search_repositories` - Search for GitLab projects 3. `create_repository` - Create a new GitLab project diff --git a/docs/setup-github-secrets.md b/docs/setup-github-secrets.md new file mode 100644 index 0000000..e6b465e --- /dev/null +++ b/docs/setup-github-secrets.md @@ -0,0 +1,57 @@ +# GitHub Secrets Setup Guide + +## 1. Navigate to GitHub Repository + +1. Go to your `gitlab-mcp` repository on GitHub +2. Click on the Settings tab +3. In the left sidebar, select "Secrets and variables" → "Actions" + +## 2. Add Secrets + +Click the "New repository secret" button and add the following secrets: + +### GITLAB_TOKEN_TEST + +- **Name**: `GITLAB_TOKEN_TEST` +- **Value**: Your GitLab Personal Access Token +- Used for integration tests to call the real GitLab API + +### TEST_PROJECT_ID + +- **Name**: `TEST_PROJECT_ID` +- **Value**: Your test project ID (e.g., `70322092`) +- The GitLab project ID used for testing + +### GITLAB_API_URL (Optional) + +- **Name**: `GITLAB_API_URL` +- **Value**: `https://gitlab.com` +- Only set this if using a different GitLab instance (default is https://gitlab.com) + +## 3. Verify Configuration + +To verify your secrets are properly configured: + +1. Create a PR or update an existing PR +2. Check the workflow execution in the Actions tab +3. Confirm that the "integration-test" job successfully calls the GitLab API + +## Security Best Practices + +- Never commit GitLab tokens directly in code +- Grant minimal required permissions to tokens (read_api, write_repository) +- Rotate tokens regularly + +## Local Testing + +To run integration tests locally: + +```bash +export GITLAB_TOKEN_TEST="your-token-here" +export TEST_PROJECT_ID="70322092" +export GITLAB_API_URL="https://gitlab.com" + +npm run test:integration +``` + +⚠️ **Important**: When testing locally, use environment variables and never commit tokens to the repository! diff --git a/index.ts b/index.ts index dfb14b4..0f676aa 100644 --- a/index.ts +++ b/index.ts @@ -2,10 +2,7 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { - CallToolRequestSchema, - ListToolsRequestSchema, -} from "@modelcontextprotocol/sdk/types.js"; +import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js"; import FormData from "form-data"; import fetch from "node-fetch"; import { SocksProxyAgent } from "socks-proxy-agent"; @@ -188,6 +185,7 @@ const server = new Server( const GITLAB_PERSONAL_ACCESS_TOKEN = process.env.GITLAB_PERSONAL_ACCESS_TOKEN; const GITLAB_READ_ONLY_MODE = process.env.GITLAB_READ_ONLY_MODE === "true"; const USE_GITLAB_WIKI = process.env.USE_GITLAB_WIKI === "true"; +const USE_MILESTONE = process.env.USE_MILESTONE === "true"; // Add proxy configuration const HTTP_PROXY = process.env.HTTP_PROXY; @@ -249,8 +247,7 @@ const allTools = [ }, { name: "get_file_contents", - description: - "Get the contents of a file or directory from a GitLab project", + description: "Get the contents of a file or directory from a GitLab project", inputSchema: zodToJsonSchema(GetFileContentsSchema), }, { @@ -292,8 +289,7 @@ const allTools = [ }, { name: "update_merge_request", - description: - "Update a merge request (Either mergeRequestIid or branchName must be provided)", + description: "Update a merge request (Either mergeRequestIid or branchName must be provided)", inputSchema: zodToJsonSchema(UpdateMergeRequestSchema), }, { @@ -458,8 +454,7 @@ const allTools = [ }, { name: "get_repository_tree", - description: - "Get the repository tree for a GitLab project (list files and directories)", + description: "Get the repository tree for a GitLab project (list files and directories)", inputSchema: zodToJsonSchema(GetRepositoryTreeSchema), }, { @@ -489,8 +484,7 @@ const allTools = [ }, { name: "list_merge_requests", - description: - "List merge requests in a GitLab project with filtering options", + description: "List merge requests in a GitLab project with filtering options", inputSchema: zodToJsonSchema(ListMergeRequestsSchema), }, { @@ -572,6 +566,8 @@ const readOnlyTools = [ "get_milestone_issue", "get_milestone_merge_requests", "get_milestone_burndown_events", + "list_wiki_pages", + "get_wiki_page", ]; // Define which tools are related to wiki and can be toggled by USE_GITLAB_WIKI @@ -584,6 +580,19 @@ const wikiToolNames = [ "upload_wiki_attachment", ]; +// Define which tools are related to milestones and can be toggled by USE_MILESTONE +const milestoneToolNames = [ + "list_milestones", + "get_milestone", + "create_milestone", + "edit_milestone", + "delete_milestone", + "get_milestone_issue", + "get_milestone_merge_requests", + "promote_milestone", + "get_milestone_burndown_events", +]; + /** * Smart URL handling for GitLab API * @@ -599,10 +608,7 @@ function normalizeGitLabApiUrl(url?: string): string { let normalizedUrl = url.endsWith("/") ? url.slice(0, -1) : url; // Check if URL already has /api/v4 - if ( - !normalizedUrl.endsWith("/api/v4") && - !normalizedUrl.endsWith("/api/v4/") - ) { + if (!normalizedUrl.endsWith("/api/v4") && !normalizedUrl.endsWith("/api/v4/")) { // Append /api/v4 if not already present normalizedUrl = `${normalizedUrl}/api/v4`; } @@ -625,24 +631,17 @@ if (!GITLAB_PERSONAL_ACCESS_TOKEN) { * @param {import("node-fetch").Response} response - The response from GitLab API * @throws {Error} Throws an error with response details if the request failed */ -async function handleGitLabError( - response: import("node-fetch").Response -): Promise { +async function handleGitLabError(response: import("node-fetch").Response): Promise { if (!response.ok) { const errorBody = await response.text(); // Check specifically for Rate Limit error - if ( - response.status === 403 && - errorBody.includes("User API Key Rate limit exceeded") - ) { + if (response.status === 403 && errorBody.includes("User API Key Rate limit exceeded")) { console.error("GitLab API Rate Limit Exceeded:", errorBody); console.log("User API Key Rate limit exceeded. Please try again later."); throw new Error(`GitLab API Rate Limit Exceeded: ${errorBody}`); } else { // Handle other API errors - throw new Error( - `GitLab API error: ${response.status} ${response.statusText}\n${errorBody}` - ); + throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`); } } } @@ -655,14 +654,9 @@ async function handleGitLabError( * @param {string} [namespace] - The namespace to fork the project to * @returns {Promise} The created fork */ -async function forkProject( - projectId: string, - namespace?: string -): Promise { +async function forkProject(projectId: string, namespace?: string): Promise { projectId = decodeURIComponent(projectId); // Decode project ID - const url = new URL( - `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/fork` - ); + const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/fork`); if (namespace) { url.searchParams.append("namespace", namespace); @@ -697,9 +691,7 @@ async function createBranch( ): Promise { projectId = decodeURIComponent(projectId); // Decode project ID const url = new URL( - `${GITLAB_API_URL}/projects/${encodeURIComponent( - projectId - )}/repository/branches` + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/branches` ); const response = await fetch(url.toString(), { @@ -724,9 +716,7 @@ async function createBranch( */ async function getDefaultBranchRef(projectId: string): Promise { projectId = decodeURIComponent(projectId); // Decode project ID - const url = new URL( - `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}` - ); + const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}`); const response = await fetch(url.toString(), { ...DEFAULT_FETCH_CONFIG, @@ -760,9 +750,7 @@ async function getFileContents( } const url = new URL( - `${GITLAB_API_URL}/projects/${encodeURIComponent( - projectId - )}/repository/files/${encodedPath}` + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/files/${encodedPath}` ); url.searchParams.append("ref", ref); @@ -782,9 +770,7 @@ async function getFileContents( // Base64로 인코딩된 파일 내용을 UTF-8로 디코딩 if (!Array.isArray(parsedData) && parsedData.content) { - parsedData.content = Buffer.from(parsedData.content, "base64").toString( - "utf8" - ); + parsedData.content = Buffer.from(parsedData.content, "base64").toString("utf8"); parsedData.encoding = "utf8"; } @@ -804,9 +790,7 @@ async function createIssue( options: z.infer ): Promise { projectId = decodeURIComponent(projectId); // Decode project ID - const url = new URL( - `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues` - ); + const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues`); const response = await fetch(url.toString(), { ...DEFAULT_FETCH_CONFIG, @@ -844,9 +828,7 @@ async function listIssues( options: Omit, "project_id"> = {} ): Promise { projectId = decodeURIComponent(projectId); // Decode project ID - const url = new URL( - `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues` - ); + const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues`); // Add all query parameters Object.entries(options).forEach(([key, value]) => { @@ -881,9 +863,7 @@ async function listMergeRequests( options: Omit, "project_id"> = {} ): Promise { projectId = decodeURIComponent(projectId); // Decode project ID - const url = new URL( - `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/merge_requests` - ); + const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/merge_requests`); // Add all query parameters Object.entries(options).forEach(([key, value]) => { @@ -914,15 +894,10 @@ async function listMergeRequests( * @param {number} issueIid - The internal ID of the project issue * @returns {Promise} The issue */ -async function getIssue( - projectId: string, - issueIid: number -): Promise { +async function getIssue(projectId: string, issueIid: number): Promise { projectId = decodeURIComponent(projectId); // Decode project ID const url = new URL( - `${GITLAB_API_URL}/projects/${encodeURIComponent( - projectId - )}/issues/${issueIid}` + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues/${issueIid}` ); const response = await fetch(url.toString(), { @@ -950,9 +925,7 @@ async function updateIssue( ): Promise { projectId = decodeURIComponent(projectId); // Decode project ID const url = new URL( - `${GITLAB_API_URL}/projects/${encodeURIComponent( - projectId - )}/issues/${issueIid}` + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues/${issueIid}` ); // Convert labels array to comma-separated string if present @@ -983,9 +956,7 @@ async function updateIssue( async function deleteIssue(projectId: string, issueIid: number): Promise { projectId = decodeURIComponent(projectId); // Decode project ID const url = new URL( - `${GITLAB_API_URL}/projects/${encodeURIComponent( - projectId - )}/issues/${issueIid}` + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues/${issueIid}` ); const response = await fetch(url.toString(), { @@ -1010,9 +981,7 @@ async function listIssueLinks( ): Promise { projectId = decodeURIComponent(projectId); // Decode project ID const url = new URL( - `${GITLAB_API_URL}/projects/${encodeURIComponent( - projectId - )}/issues/${issueIid}/links` + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues/${issueIid}/links` ); const response = await fetch(url.toString(), { @@ -1075,9 +1044,7 @@ async function createIssueLink( projectId = decodeURIComponent(projectId); // Decode project ID targetProjectId = decodeURIComponent(targetProjectId); // Decode target project ID as well const url = new URL( - `${GITLAB_API_URL}/projects/${encodeURIComponent( - projectId - )}/issues/${issueIid}/links` + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues/${issueIid}/links` ); const response = await fetch(url.toString(), { @@ -1137,9 +1104,7 @@ async function createMergeRequest( options: z.infer ): Promise { projectId = decodeURIComponent(projectId); // Decode project ID - const url = new URL( - `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/merge_requests` - ); + const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/merge_requests`); const response = await fetch(url.toString(), { ...DEFAULT_FETCH_CONFIG, @@ -1161,9 +1126,7 @@ async function createMergeRequest( if (!response.ok) { const errorBody = await response.text(); - throw new Error( - `GitLab API error: ${response.status} ${response.statusText}\n${errorBody}` - ); + throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`); } const data = await response.json(); @@ -1219,9 +1182,7 @@ async function listIssueDiscussions( ): Promise { projectId = decodeURIComponent(projectId); // Decode project ID const url = new URL( - `${GITLAB_API_URL}/projects/${encodeURIComponent( - projectId - )}/issues/${issueIid}/discussions` + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues/${issueIid}/discussions` ); // Add query parameters for pagination and sorting @@ -1436,9 +1397,7 @@ async function createOrUpdateFile( projectId = decodeURIComponent(projectId); // Decode project ID const encodedPath = encodeURIComponent(filePath); const url = new URL( - `${GITLAB_API_URL}/projects/${encodeURIComponent( - projectId - )}/repository/files/${encodedPath}` + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/files/${encodedPath}` ); const body: Record = { @@ -1493,9 +1452,7 @@ async function createOrUpdateFile( if (!response.ok) { const errorBody = await response.text(); - throw new Error( - `GitLab API error: ${response.status} ${response.statusText}\n${errorBody}` - ); + throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`); } const data = await response.json(); @@ -1518,9 +1475,7 @@ async function createTree( ): Promise { projectId = decodeURIComponent(projectId); // Decode project ID const url = new URL( - `${GITLAB_API_URL}/projects/${encodeURIComponent( - projectId - )}/repository/tree` + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/tree` ); if (ref) { @@ -1531,7 +1486,7 @@ async function createTree( ...DEFAULT_FETCH_CONFIG, method: "POST", body: JSON.stringify({ - files: files.map((file) => ({ + files: files.map(file => ({ file_path: file.path, content: file.content, encoding: "text", @@ -1546,9 +1501,7 @@ async function createTree( if (!response.ok) { const errorBody = await response.text(); - throw new Error( - `GitLab API error: ${response.status} ${response.statusText}\n${errorBody}` - ); + throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`); } const data = await response.json(); @@ -1573,9 +1526,7 @@ async function createCommit( ): Promise { projectId = decodeURIComponent(projectId); // Decode project ID const url = new URL( - `${GITLAB_API_URL}/projects/${encodeURIComponent( - projectId - )}/repository/commits` + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/commits` ); const response = await fetch(url.toString(), { @@ -1584,7 +1535,7 @@ async function createCommit( body: JSON.stringify({ branch, commit_message: message, - actions: actions.map((action) => ({ + actions: actions.map(action => ({ action: "create", file_path: action.path, content: action.content, @@ -1600,9 +1551,7 @@ async function createCommit( if (!response.ok) { const errorBody = await response.text(); - throw new Error( - `GitLab API error: ${response.status} ${response.statusText}\n${errorBody}` - ); + throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`); } const data = await response.json(); @@ -1636,9 +1585,7 @@ async function searchProjects( if (!response.ok) { const errorBody = await response.text(); - throw new Error( - `GitLab API error: ${response.status} ${response.statusText}\n${errorBody}` - ); + throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`); } const projects = (await response.json()) as GitLabRepository[]; @@ -1681,9 +1628,7 @@ async function createRepository( if (!response.ok) { const errorBody = await response.text(); - throw new Error( - `GitLab API error: ${response.status} ${response.statusText}\n${errorBody}` - ); + throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`); } const data = await response.json(); @@ -1761,11 +1706,7 @@ async function getMergeRequestDiffs( } if (branchName && !mergeRequestIid) { - const mergeRequest = await getMergeRequest( - projectId, - undefined, - branchName - ); + const mergeRequest = await getMergeRequest(projectId, undefined, branchName); mergeRequestIid = mergeRequest.iid; } @@ -1813,18 +1754,12 @@ async function updateMergeRequest( } if (branchName && !mergeRequestIid) { - const mergeRequest = await getMergeRequest( - projectId, - undefined, - branchName - ); + const mergeRequest = await getMergeRequest(projectId, undefined, branchName); mergeRequestIid = mergeRequest.iid; } const url = new URL( - `${GITLAB_API_URL}/projects/${encodeURIComponent( - projectId - )}/merge_requests/${mergeRequestIid}` + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/merge_requests/${mergeRequestIid}` ); const response = await fetch(url.toString(), { @@ -1870,9 +1805,7 @@ async function createNote( if (!response.ok) { const errorText = await response.text(); - throw new Error( - `GitLab API error: ${response.status} ${response.statusText}\n${errorText}` - ); + throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorText}`); } return await response.json(); @@ -2000,9 +1933,7 @@ async function verifyNamespaceExistence( namespacePath: string, parentId?: number ): Promise { - const url = new URL( - `${GITLAB_API_URL}/namespaces/${encodeURIComponent(namespacePath)}/exists` - ); + const url = new URL(`${GITLAB_API_URL}/namespaces/${encodeURIComponent(namespacePath)}/exists`); if (parentId) { url.searchParams.append("parent_id", parentId.toString()); @@ -2037,9 +1968,7 @@ async function getProject( } = {} ): Promise { projectId = decodeURIComponent(projectId); // Decode project ID - const url = new URL( - `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}` - ); + const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}`); if (options.license) { url.searchParams.append("license", "true"); @@ -2085,12 +2014,9 @@ async function listProjects( } // Make the API request - const response = await fetch( - `${GITLAB_API_URL}/projects?${params.toString()}`, - { - ...DEFAULT_FETCH_CONFIG, - } - ); + const response = await fetch(`${GITLAB_API_URL}/projects?${params.toString()}`, { + ...DEFAULT_FETCH_CONFIG, + }); // Handle errors await handleGitLabError(response); @@ -2113,9 +2039,7 @@ async function listLabels( ): Promise { projectId = decodeURIComponent(projectId); // Decode project ID // Construct the URL with project path - const url = new URL( - `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/labels` - ); + const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/labels`); // Add query parameters Object.entries(options).forEach(([key, value]) => { @@ -2163,10 +2087,7 @@ async function getLabel( // Add query parameters if (includeAncestorGroups !== undefined) { - url.searchParams.append( - "include_ancestor_groups", - includeAncestorGroups ? "true" : "false" - ); + url.searchParams.append("include_ancestor_groups", includeAncestorGroups ? "true" : "false"); } // Make the API request @@ -2252,10 +2173,7 @@ async function updateLabel( * @param projectId The ID or URL-encoded path of the project * @param labelId The ID or name of the label to delete */ -async function deleteLabel( - projectId: string, - labelId: number | string -): Promise { +async function deleteLabel(projectId: string, labelId: number | string): Promise { projectId = decodeURIComponent(projectId); // Decode project ID // Make the API request const response = await fetch( @@ -2281,57 +2199,36 @@ async function deleteLabel( async function listGroupProjects( options: z.infer ): Promise { - const url = new URL( - `${GITLAB_API_URL}/groups/${encodeURIComponent(options.group_id)}/projects` - ); + const url = new URL(`${GITLAB_API_URL}/groups/${encodeURIComponent(options.group_id)}/projects`); // Add optional parameters to URL - if (options.include_subgroups) - url.searchParams.append("include_subgroups", "true"); + if (options.include_subgroups) url.searchParams.append("include_subgroups", "true"); if (options.search) url.searchParams.append("search", options.search); if (options.order_by) url.searchParams.append("order_by", options.order_by); if (options.sort) url.searchParams.append("sort", options.sort); if (options.page) url.searchParams.append("page", options.page.toString()); - if (options.per_page) - url.searchParams.append("per_page", options.per_page.toString()); + if (options.per_page) url.searchParams.append("per_page", options.per_page.toString()); if (options.archived !== undefined) url.searchParams.append("archived", options.archived.toString()); - if (options.visibility) - url.searchParams.append("visibility", options.visibility); + if (options.visibility) url.searchParams.append("visibility", options.visibility); if (options.with_issues_enabled !== undefined) - url.searchParams.append( - "with_issues_enabled", - options.with_issues_enabled.toString() - ); + url.searchParams.append("with_issues_enabled", options.with_issues_enabled.toString()); if (options.with_merge_requests_enabled !== undefined) url.searchParams.append( "with_merge_requests_enabled", options.with_merge_requests_enabled.toString() ); if (options.min_access_level !== undefined) - url.searchParams.append( - "min_access_level", - options.min_access_level.toString() - ); + url.searchParams.append("min_access_level", options.min_access_level.toString()); if (options.with_programming_language) - url.searchParams.append( - "with_programming_language", - options.with_programming_language - ); - if (options.starred !== undefined) - url.searchParams.append("starred", options.starred.toString()); + url.searchParams.append("with_programming_language", options.with_programming_language); + if (options.starred !== undefined) url.searchParams.append("starred", options.starred.toString()); if (options.statistics !== undefined) url.searchParams.append("statistics", options.statistics.toString()); if (options.with_custom_attributes !== undefined) - url.searchParams.append( - "with_custom_attributes", - options.with_custom_attributes.toString() - ); + url.searchParams.append("with_custom_attributes", options.with_custom_attributes.toString()); if (options.with_security_reports !== undefined) - url.searchParams.append( - "with_security_reports", - options.with_security_reports.toString() - ); + url.searchParams.append("with_security_reports", options.with_security_reports.toString()); const response = await fetch(url.toString(), { ...DEFAULT_FETCH_CONFIG, @@ -2351,12 +2248,9 @@ async function listWikiPages( options: Omit, "project_id"> = {} ): Promise { projectId = decodeURIComponent(projectId); // Decode project ID - const url = new URL( - `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/wikis` - ); + const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/wikis`); if (options.page) url.searchParams.append("page", options.page.toString()); - if (options.per_page) - url.searchParams.append("per_page", options.per_page.toString()); + if (options.per_page) url.searchParams.append("per_page", options.per_page.toString()); const response = await fetch(url.toString(), { ...DEFAULT_FETCH_CONFIG, }); @@ -2368,15 +2262,10 @@ async function listWikiPages( /** * Get a specific wiki page */ -async function getWikiPage( - projectId: string, - slug: string -): Promise { +async function getWikiPage(projectId: string, slug: string): Promise { projectId = decodeURIComponent(projectId); // Decode project ID const response = await fetch( - `${GITLAB_API_URL}/projects/${encodeURIComponent( - projectId - )}/wikis/${encodeURIComponent(slug)}`, + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/wikis/${encodeURIComponent(slug)}`, { ...DEFAULT_FETCH_CONFIG } ); await handleGitLabError(response); @@ -2425,9 +2314,7 @@ async function updateWikiPage( if (content) body.content = content; if (format) body.format = format; const response = await fetch( - `${GITLAB_API_URL}/projects/${encodeURIComponent( - projectId - )}/wikis/${encodeURIComponent(slug)}`, + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/wikis/${encodeURIComponent(slug)}`, { ...DEFAULT_FETCH_CONFIG, method: "PUT", @@ -2445,9 +2332,7 @@ async function updateWikiPage( async function deleteWikiPage(projectId: string, slug: string): Promise { projectId = decodeURIComponent(projectId); // Decode project ID const response = await fetch( - `${GITLAB_API_URL}/projects/${encodeURIComponent( - projectId - )}/wikis/${encodeURIComponent(slug)}`, + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/wikis/${encodeURIComponent(slug)}`, { ...DEFAULT_FETCH_CONFIG, method: "DELETE", @@ -2468,9 +2353,7 @@ async function listPipelines( options: Omit = {} ): Promise { projectId = decodeURIComponent(projectId); // Decode project ID - const url = new URL( - `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/pipelines` - ); + const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/pipelines`); // Add all query parameters Object.entries(options).forEach(([key, value]) => { @@ -2495,15 +2378,10 @@ async function listPipelines( * @param {number} pipelineId - The ID of the pipeline * @returns {Promise} Pipeline details */ -async function getPipeline( - projectId: string, - pipelineId: number -): Promise { +async function getPipeline(projectId: string, pipelineId: number): Promise { projectId = decodeURIComponent(projectId); // Decode project ID const url = new URL( - `${GITLAB_API_URL}/projects/${encodeURIComponent( - projectId - )}/pipelines/${pipelineId}` + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/pipelines/${pipelineId}` ); const response = await fetch(url.toString(), { @@ -2534,9 +2412,7 @@ async function listPipelineJobs( ): Promise { projectId = decodeURIComponent(projectId); // Decode project ID const url = new URL( - `${GITLAB_API_URL}/projects/${encodeURIComponent( - projectId - )}/pipelines/${pipelineId}/jobs` + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/pipelines/${pipelineId}/jobs` ); // Add all query parameters @@ -2562,14 +2438,9 @@ async function listPipelineJobs( const data = await response.json(); return z.array(GitLabPipelineJobSchema).parse(data); } -async function getPipelineJob( - projectId: string, - jobId: number -): Promise { +async function getPipelineJob(projectId: string, jobId: number): Promise { projectId = decodeURIComponent(projectId); // Decode project ID - const url = new URL( - `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/jobs/${jobId}` - ); + const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/jobs/${jobId}`); const response = await fetch(url.toString(), { ...DEFAULT_FETCH_CONFIG, @@ -2591,15 +2462,10 @@ async function getPipelineJob( * @param {number} jobId - The ID of the job * @returns {Promise} The job output/trace */ -async function getPipelineJobOutput( - projectId: string, - jobId: number -): Promise { +async function getPipelineJobOutput(projectId: string, jobId: number): Promise { projectId = decodeURIComponent(projectId); // Decode project ID const url = new URL( - `${GITLAB_API_URL}/projects/${encodeURIComponent( - projectId - )}/jobs/${jobId}/trace` + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/jobs/${jobId}/trace` ); const response = await fetch(url.toString(), { @@ -2624,16 +2490,13 @@ async function getPipelineJobOutput( * @param {GetRepositoryTreeOptions} options - Options for the tree * @returns {Promise} */ -async function getRepositoryTree( - options: GetRepositoryTreeOptions -): Promise { +async function getRepositoryTree(options: GetRepositoryTreeOptions): Promise { options.project_id = decodeURIComponent(options.project_id); // Decode project_id within options const queryParams = new URLSearchParams(); if (options.path) queryParams.append("path", options.path); if (options.ref) queryParams.append("ref", options.ref); if (options.recursive) queryParams.append("recursive", "true"); - if (options.per_page) - queryParams.append("per_page", options.per_page.toString()); + if (options.per_page) queryParams.append("per_page", options.per_page.toString()); if (options.page_token) queryParams.append("page_token", options.page_token); if (options.pagination) queryParams.append("pagination", options.pagination); @@ -2672,14 +2535,12 @@ async function listProjectMilestones( options: Omit, "project_id"> ): Promise { projectId = decodeURIComponent(projectId); - const url = new URL( - `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/milestones` - ); + const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/milestones`); Object.entries(options).forEach(([key, value]) => { if (value !== undefined) { if (key === "iids" && Array.isArray(value) && value.length > 0) { - value.forEach((iid) => { + value.forEach(iid => { url.searchParams.append("iids[]", iid.toString()); }); } else if (value !== undefined) { @@ -2708,9 +2569,7 @@ async function getProjectMilestone( ): Promise { projectId = decodeURIComponent(projectId); const url = new URL( - `${GITLAB_API_URL}/projects/${encodeURIComponent( - projectId - )}/milestones/${milestoneId}` + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/milestones/${milestoneId}` ); const response = await fetch(url.toString(), { @@ -2732,9 +2591,7 @@ async function createProjectMilestone( options: Omit, "project_id"> ): Promise { projectId = decodeURIComponent(projectId); - const url = new URL( - `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/milestones` - ); + const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/milestones`); const response = await fetch(url.toString(), { ...DEFAULT_FETCH_CONFIG, @@ -2756,16 +2613,11 @@ async function createProjectMilestone( async function editProjectMilestone( projectId: string, milestoneId: number, - options: Omit< - z.infer, - "project_id" | "milestone_id" - > + options: Omit, "project_id" | "milestone_id"> ): Promise { projectId = decodeURIComponent(projectId); const url = new URL( - `${GITLAB_API_URL}/projects/${encodeURIComponent( - projectId - )}/milestones/${milestoneId}` + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/milestones/${milestoneId}` ); const response = await fetch(url.toString(), { @@ -2784,15 +2636,10 @@ async function editProjectMilestone( * @param {number} milestoneId - The ID of the milestone * @returns {Promise} */ -async function deleteProjectMilestone( - projectId: string, - milestoneId: number -): Promise { +async function deleteProjectMilestone(projectId: string, milestoneId: number): Promise { projectId = decodeURIComponent(projectId); const url = new URL( - `${GITLAB_API_URL}/projects/${encodeURIComponent( - projectId - )}/milestones/${milestoneId}` + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/milestones/${milestoneId}` ); const response = await fetch(url.toString(), { @@ -2808,15 +2655,10 @@ async function deleteProjectMilestone( * @param {number} milestoneId - The ID of the milestone * @returns {Promise} List of issues */ -async function getMilestoneIssues( - projectId: string, - milestoneId: number -): Promise { +async function getMilestoneIssues(projectId: string, milestoneId: number): Promise { projectId = decodeURIComponent(projectId); const url = new URL( - `${GITLAB_API_URL}/projects/${encodeURIComponent( - projectId - )}/milestones/${milestoneId}/issues` + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/milestones/${milestoneId}/issues` ); const response = await fetch(url.toString(), { @@ -2864,9 +2706,7 @@ async function promoteProjectMilestone( ): Promise { projectId = decodeURIComponent(projectId); const url = new URL( - `${GITLAB_API_URL}/projects/${encodeURIComponent( - projectId - )}/milestones/${milestoneId}/promote` + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/milestones/${milestoneId}/promote` ); const response = await fetch(url.toString(), { @@ -2884,10 +2724,7 @@ async function promoteProjectMilestone( * @param {number} milestoneId - The ID of the milestone * @returns {Promise} Burndown chart events */ -async function getMilestoneBurndownEvents( - projectId: string, - milestoneId: number -): Promise { +async function getMilestoneBurndownEvents(projectId: string, milestoneId: number): Promise { projectId = decodeURIComponent(projectId); const url = new URL( `${GITLAB_API_URL}/projects/${encodeURIComponent( @@ -2906,21 +2743,21 @@ async function getMilestoneBurndownEvents( server.setRequestHandler(ListToolsRequestSchema, async () => { // Apply read-only filter first const tools0 = GITLAB_READ_ONLY_MODE - ? allTools.filter((tool) => readOnlyTools.includes(tool.name)) + ? allTools.filter(tool => readOnlyTools.includes(tool.name)) : allTools; // Toggle wiki tools by USE_GITLAB_WIKI flag - let tools = USE_GITLAB_WIKI + const tools1 = USE_GITLAB_WIKI ? tools0 - : tools0.filter((tool) => !wikiToolNames.includes(tool.name)); + : tools0.filter(tool => !wikiToolNames.includes(tool.name)); + // Toggle milestone tools by USE_MILESTONE flag + let tools = USE_MILESTONE + ? tools1 + : tools1.filter(tool => !milestoneToolNames.includes(tool.name)); // <<< START: Gemini 호환성을 위해 $schema 제거 >>> - tools = tools.map((tool) => { + tools = tools.map(tool => { // inputSchema가 존재하고 객체인지 확인 - if ( - tool.inputSchema && - typeof tool.inputSchema === "object" && - tool.inputSchema !== null - ) { + if (tool.inputSchema && typeof tool.inputSchema === "object" && tool.inputSchema !== null) { // $schema 키가 존재하면 삭제 if ("$schema" in tool.inputSchema) { // 불변성을 위해 새로운 객체 생성 (선택적이지만 권장) @@ -2939,7 +2776,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { }; }); -server.setRequestHandler(CallToolRequestSchema, async (request) => { +server.setRequestHandler(CallToolRequestSchema, async request => { try { if (!request.params.arguments) { throw new Error("Arguments are required"); @@ -2949,14 +2786,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { case "fork_repository": { const forkArgs = ForkRepositorySchema.parse(request.params.arguments); try { - const forkedProject = await forkProject( - forkArgs.project_id, - forkArgs.namespace - ); + const forkedProject = await forkProject(forkArgs.project_id, forkArgs.namespace); return { - content: [ - { type: "text", text: JSON.stringify(forkedProject, null, 2) }, - ], + content: [{ type: "text", text: JSON.stringify(forkedProject, null, 2) }], }; } catch (forkError) { console.error("Error forking repository:", forkError); @@ -2994,11 +2826,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { case "search_repositories": { const args = SearchRepositoriesSchema.parse(request.params.arguments); - const results = await searchProjects( - args.search, - args.page, - args.per_page - ); + const results = await searchProjects(args.search, args.page, args.per_page); return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }], }; @@ -3008,19 +2836,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { const args = CreateRepositorySchema.parse(request.params.arguments); const repository = await createRepository(args); return { - content: [ - { type: "text", text: JSON.stringify(repository, null, 2) }, - ], + content: [{ type: "text", text: JSON.stringify(repository, null, 2) }], }; } case "get_file_contents": { const args = GetFileContentsSchema.parse(request.params.arguments); - const contents = await getFileContents( - args.project_id, - args.file_path, - args.ref - ); + const contents = await getFileContents(args.project_id, args.file_path, args.ref); return { content: [{ type: "text", text: JSON.stringify(contents, null, 2) }], }; @@ -3049,7 +2871,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { args.project_id, args.commit_message, args.branch, - args.files.map((f) => ({ path: f.file_path, content: f.content })) + args.files.map(f => ({ path: f.file_path, content: f.content })) ); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], @@ -3070,16 +2892,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { const { project_id, ...options } = args; const mergeRequest = await createMergeRequest(project_id, options); return { - content: [ - { type: "text", text: JSON.stringify(mergeRequest, null, 2) }, - ], + content: [{ type: "text", text: JSON.stringify(mergeRequest, null, 2) }], }; } case "update_merge_request_note": { - const args = UpdateMergeRequestNoteSchema.parse( - request.params.arguments - ); + const args = UpdateMergeRequestNoteSchema.parse(request.params.arguments); const note = await updateMergeRequestNote( args.project_id, args.merge_request_iid, @@ -3094,9 +2912,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { } case "create_merge_request_note": { - const args = CreateMergeRequestNoteSchema.parse( - request.params.arguments - ); + const args = CreateMergeRequestNoteSchema.parse(request.params.arguments); const note = await createMergeRequestNote( args.project_id, args.merge_request_iid, @@ -3145,9 +2961,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { args.source_branch ); return { - content: [ - { type: "text", text: JSON.stringify(mergeRequest, null, 2) }, - ], + content: [{ type: "text", text: JSON.stringify(mergeRequest, null, 2) }], }; } @@ -3166,8 +2980,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { case "update_merge_request": { const args = UpdateMergeRequestSchema.parse(request.params.arguments); - const { project_id, merge_request_iid, source_branch, ...options } = - args; + const { project_id, merge_request_iid, source_branch, ...options } = args; const mergeRequest = await updateMergeRequest( project_id, options, @@ -3175,24 +2988,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { source_branch ); return { - content: [ - { type: "text", text: JSON.stringify(mergeRequest, null, 2) }, - ], + content: [{ type: "text", text: JSON.stringify(mergeRequest, null, 2) }], }; } case "mr_discussions": { - const args = ListMergeRequestDiscussionsSchema.parse( - request.params.arguments - ); + const args = ListMergeRequestDiscussionsSchema.parse(request.params.arguments); const discussions = await listMergeRequestDiscussions( args.project_id, args.merge_request_iid ); return { - content: [ - { type: "text", text: JSON.stringify(discussions, null, 2) }, - ], + content: [{ type: "text", text: JSON.stringify(discussions, null, 2) }], }; } @@ -3222,18 +3029,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { const namespaces = z.array(GitLabNamespaceSchema).parse(data); return { - content: [ - { type: "text", text: JSON.stringify(namespaces, null, 2) }, - ], + content: [{ type: "text", text: JSON.stringify(namespaces, null, 2) }], }; } case "get_namespace": { const args = GetNamespaceSchema.parse(request.params.arguments); const url = new URL( - `${GITLAB_API_URL}/namespaces/${encodeURIComponent( - args.namespace_id - )}` + `${GITLAB_API_URL}/namespaces/${encodeURIComponent(args.namespace_id)}` ); const response = await fetch(url.toString(), { @@ -3251,9 +3054,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { case "verify_namespace": { const args = VerifyNamespaceSchema.parse(request.params.arguments); - const url = new URL( - `${GITLAB_API_URL}/namespaces/${encodeURIComponent(args.path)}/exists` - ); + const url = new URL(`${GITLAB_API_URL}/namespaces/${encodeURIComponent(args.path)}/exists`); const response = await fetch(url.toString(), { ...DEFAULT_FETCH_CONFIG, @@ -3264,17 +3065,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { const namespaceExists = GitLabNamespaceExistsResponseSchema.parse(data); return { - content: [ - { type: "text", text: JSON.stringify(namespaceExists, null, 2) }, - ], + content: [{ type: "text", text: JSON.stringify(namespaceExists, null, 2) }], }; } case "get_project": { const args = GetProjectSchema.parse(request.params.arguments); - const url = new URL( - `${GITLAB_API_URL}/projects/${encodeURIComponent(args.project_id)}` - ); + const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(args.project_id)}`); const response = await fetch(url.toString(), { ...DEFAULT_FETCH_CONFIG, @@ -3302,23 +3099,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { const args = CreateNoteSchema.parse(request.params.arguments); const { project_id, noteable_type, noteable_iid, body } = args; - const note = await createNote( - project_id, - noteable_type, - noteable_iid, - body - ); + const note = await createNote(project_id, noteable_type, noteable_iid, body); return { content: [{ type: "text", text: JSON.stringify(note, null, 2) }], }; } case "create_merge_request_thread": { - const args = CreateMergeRequestThreadSchema.parse( - request.params.arguments - ); - const { project_id, merge_request_iid, body, position, created_at } = - args; + const args = CreateMergeRequestThreadSchema.parse(request.params.arguments); + const { project_id, merge_request_iid, body, position, created_at } = args; const thread = await createMergeRequestThread( project_id, @@ -3387,25 +3176,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { const args = ListIssueDiscussionsSchema.parse(request.params.arguments); const { project_id, issue_iid, ...options } = args; - const discussions = await listIssueDiscussions( - project_id, - issue_iid, - options - ); + const discussions = await listIssueDiscussions(project_id, issue_iid, options); return { - content: [ - { type: "text", text: JSON.stringify(discussions, null, 2) }, - ], + content: [{ type: "text", text: JSON.stringify(discussions, null, 2) }], }; } case "get_issue_link": { const args = GetIssueLinkSchema.parse(request.params.arguments); - const link = await getIssueLink( - args.project_id, - args.issue_iid, - args.issue_link_id - ); + const link = await getIssueLink(args.project_id, args.issue_iid, args.issue_link_id); return { content: [{ type: "text", text: JSON.stringify(link, null, 2) }], }; @@ -3427,11 +3206,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { case "delete_issue_link": { const args = DeleteIssueLinkSchema.parse(request.params.arguments); - await deleteIssueLink( - args.project_id, - args.issue_iid, - args.issue_link_id - ); + await deleteIssueLink(args.project_id, args.issue_iid, args.issue_link_id); return { content: [ { @@ -3459,11 +3234,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { case "get_label": { const args = GetLabelSchema.parse(request.params.arguments); - const label = await getLabel( - args.project_id, - args.label_id, - args.include_ancestor_groups - ); + const label = await getLabel(args.project_id, args.label_id, args.include_ancestor_groups); return { content: [{ type: "text", text: JSON.stringify(label, null, 2) }], }; @@ -3512,9 +3283,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { } case "list_wiki_pages": { - const { project_id, page, per_page } = ListWikiPagesSchema.parse( - request.params.arguments - ); + const { project_id, page, per_page } = ListWikiPagesSchema.parse(request.params.arguments); const wikiPages = await listWikiPages(project_id, { page, per_page }); return { content: [{ type: "text", text: JSON.stringify(wikiPages, null, 2) }], @@ -3522,9 +3291,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { } case "get_wiki_page": { - const { project_id, slug } = GetWikiPageSchema.parse( - request.params.arguments - ); + const { project_id, slug } = GetWikiPageSchema.parse(request.params.arguments); const wikiPage = await getWikiPage(project_id, slug); return { content: [{ type: "text", text: JSON.stringify(wikiPage, null, 2) }], @@ -3532,38 +3299,27 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { } case "create_wiki_page": { - const { project_id, title, content, format } = - CreateWikiPageSchema.parse(request.params.arguments); - const wikiPage = await createWikiPage( - project_id, - title, - content, - format + const { project_id, title, content, format } = CreateWikiPageSchema.parse( + request.params.arguments ); + const wikiPage = await createWikiPage(project_id, title, content, format); return { content: [{ type: "text", text: JSON.stringify(wikiPage, null, 2) }], }; } case "update_wiki_page": { - const { project_id, slug, title, content, format } = - UpdateWikiPageSchema.parse(request.params.arguments); - const wikiPage = await updateWikiPage( - project_id, - slug, - title, - content, - format + const { project_id, slug, title, content, format } = UpdateWikiPageSchema.parse( + request.params.arguments ); + const wikiPage = await updateWikiPage(project_id, slug, title, content, format); return { content: [{ type: "text", text: JSON.stringify(wikiPage, null, 2) }], }; } case "delete_wiki_page": { - const { project_id, slug } = DeleteWikiPageSchema.parse( - request.params.arguments - ); + const { project_id, slug } = DeleteWikiPageSchema.parse(request.params.arguments); await deleteWikiPage(project_id, slug); return { content: [ @@ -3600,9 +3356,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { } case "get_pipeline": { - const { project_id, pipeline_id } = GetPipelineSchema.parse( - request.params.arguments - ); + const { project_id, pipeline_id } = GetPipelineSchema.parse(request.params.arguments); const pipeline = await getPipeline(project_id, pipeline_id); return { content: [ @@ -3615,8 +3369,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { } case "list_pipeline_jobs": { - const { project_id, pipeline_id, ...options } = - ListPipelineJobsSchema.parse(request.params.arguments); + const { project_id, pipeline_id, ...options } = ListPipelineJobsSchema.parse( + request.params.arguments + ); const jobs = await listPipelineJobs(project_id, pipeline_id, options); return { content: [ @@ -3629,9 +3384,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { } case "get_pipeline_job": { - const { project_id, job_id } = GetPipelineJobOutputSchema.parse( - request.params.arguments - ); + const { project_id, job_id } = GetPipelineJobOutputSchema.parse(request.params.arguments); const jobDetails = await getPipelineJob(project_id, job_id); return { content: [ @@ -3644,9 +3397,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { } case "get_pipeline_job_output": { - const { project_id, job_id } = GetPipelineJobOutputSchema.parse( - request.params.arguments - ); + const { project_id, job_id } = GetPipelineJobOutputSchema.parse(request.params.arguments); const jobOutput = await getPipelineJobOutput(project_id, job_id); return { content: [ @@ -3662,9 +3413,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { const args = ListMergeRequestsSchema.parse(request.params.arguments); const mergeRequests = await listMergeRequests(args.project_id, args); return { - content: [ - { type: "text", text: JSON.stringify(mergeRequests, null, 2) }, - ], + content: [{ type: "text", text: JSON.stringify(mergeRequests, null, 2) }], }; } @@ -3714,13 +3463,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { } case "edit_milestone": { - const { project_id, milestone_id, ...options } = - EditProjectMilestoneSchema.parse(request.params.arguments); - const milestone = await editProjectMilestone( - project_id, - milestone_id, - options + const { project_id, milestone_id, ...options } = EditProjectMilestoneSchema.parse( + request.params.arguments ); + const milestone = await editProjectMilestone(project_id, milestone_id, options); return { content: [ { @@ -3769,12 +3515,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { } case "get_milestone_merge_requests": { - const { project_id, milestone_id } = - GetMilestoneMergeRequestsSchema.parse(request.params.arguments); - const mergeRequests = await getMilestoneMergeRequests( - project_id, - milestone_id + const { project_id, milestone_id } = GetMilestoneMergeRequestsSchema.parse( + request.params.arguments ); + const mergeRequests = await getMilestoneMergeRequests(project_id, milestone_id); return { content: [ { @@ -3786,12 +3530,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { } case "promote_milestone": { - const { project_id, milestone_id } = - PromoteProjectMilestoneSchema.parse(request.params.arguments); - const milestone = await promoteProjectMilestone( - project_id, - milestone_id + const { project_id, milestone_id } = PromoteProjectMilestoneSchema.parse( + request.params.arguments ); + const milestone = await promoteProjectMilestone(project_id, milestone_id); return { content: [ { @@ -3803,12 +3545,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { } case "get_milestone_burndown_events": { - const { project_id, milestone_id } = - GetMilestoneBurndownEventsSchema.parse(request.params.arguments); - const events = await getMilestoneBurndownEvents( - project_id, - milestone_id + const { project_id, milestone_id } = GetMilestoneBurndownEventsSchema.parse( + request.params.arguments ); + const events = await getMilestoneBurndownEvents(project_id, milestone_id); return { content: [ { @@ -3826,7 +3566,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { if (error instanceof z.ZodError) { throw new Error( `Invalid arguments: ${error.errors - .map((e) => `${e.path.join(".")}: ${e.message}`) + .map(e => `${e.path.join(".")}: ${e.message}`) .join(", ")}` ); } @@ -3854,7 +3594,7 @@ async function runServer() { } } -runServer().catch((error) => { +runServer().catch(error => { console.error("Fatal error in main():", error); process.exit(1); }); diff --git a/package-lock.json b/package-lock.json index 2002944..6194a50 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@zereight/mcp-gitlab", - "version": "1.0.46", + "version": "1.0.50", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@zereight/mcp-gitlab", - "version": "1.0.46", + "version": "1.0.50", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "1.8.0", @@ -23,6 +23,11 @@ }, "devDependencies": { "@types/node": "^22.13.10", + "@typescript-eslint/eslint-plugin": "^8.21.0", + "@typescript-eslint/parser": "^8.21.0", + "eslint": "^9.18.0", + "prettier": "^3.4.2", + "ts-node": "^10.9.2", "typescript": "^5.8.2", "zod": "^3.24.2" }, @@ -30,6 +35,299 @@ "node": ">=14" } }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", + "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.2.tgz", + "integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", + "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.27.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.27.0.tgz", + "integrity": "sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz", + "integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.14.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@modelcontextprotocol/sdk": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.8.0.tgz", @@ -51,6 +349,86 @@ "node": ">=18" } }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "22.13.14", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.14.tgz", @@ -70,6 +448,237 @@ "form-data": "^4.0.0" } }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.33.0.tgz", + "integrity": "sha512-CACyQuqSHt7ma3Ns601xykeBK/rDeZa3w6IS6UtMQbixO5DWy+8TilKkviGDH6jtWCo8FGRKEK5cLLkPvEammQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.33.0", + "@typescript-eslint/type-utils": "8.33.0", + "@typescript-eslint/utils": "8.33.0", + "@typescript-eslint/visitor-keys": "8.33.0", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.33.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.33.0.tgz", + "integrity": "sha512-JaehZvf6m0yqYp34+RVnihBAChkqeH+tqqhS0GuX1qgPpwLvmTPheKEs6OeCK6hVJgXZHJ2vbjnC9j119auStQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.33.0", + "@typescript-eslint/types": "8.33.0", + "@typescript-eslint/typescript-estree": "8.33.0", + "@typescript-eslint/visitor-keys": "8.33.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.33.0.tgz", + "integrity": "sha512-d1hz0u9l6N+u/gcrk6s6gYdl7/+pp8yHheRTqP6X5hVDKALEaTn8WfGiit7G511yueBEL3OpOEpD+3/MBdoN+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.33.0", + "@typescript-eslint/types": "^8.33.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.33.0.tgz", + "integrity": "sha512-LMi/oqrzpqxyO72ltP+dBSP6V0xiUb4saY7WLtxSfiNEBI8m321LLVFU9/QDJxjDQG9/tjSqKz/E3380TEqSTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.33.0", + "@typescript-eslint/visitor-keys": "8.33.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.33.0.tgz", + "integrity": "sha512-sTkETlbqhEoiFmGr1gsdq5HyVbSOF0145SYDJ/EQmXHtKViCaGvnyLqWFFHtEXoS0J1yU8Wyou2UGmgW88fEug==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.33.0.tgz", + "integrity": "sha512-lScnHNCBqL1QayuSrWeqAL5GmqNdVUQAAMTaCwdYEdWfIrSrOGzyLGRCHXcCixa5NK6i5l0AfSO2oBSjCjf4XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.33.0", + "@typescript-eslint/utils": "8.33.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.33.0.tgz", + "integrity": "sha512-DKuXOKpM5IDT1FA2g9x9x1Ug81YuKrzf4mYX8FAVSNu5Wo/LELHWQyM1pQaDkI42bX15PWl0vNPt1uGiIFUOpg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.33.0.tgz", + "integrity": "sha512-vegY4FQoB6jL97Tu/lWRsAiUUp8qJTqzAmENH2k59SJhw0Th1oszb9Idq/FyyONLuNqT1OADJPXfyUNOR8SzAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.33.0", + "@typescript-eslint/tsconfig-utils": "8.33.0", + "@typescript-eslint/types": "8.33.0", + "@typescript-eslint/visitor-keys": "8.33.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.33.0.tgz", + "integrity": "sha512-lPFuQaLA9aSNa7D5u2EpRiqdAUhzShwGg/nhpBlc4GR6kcTABttCuyjFs8BcEZ8VWrjCBof/bePhP3Q3fS+Yrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.33.0", + "@typescript-eslint/types": "8.33.0", + "@typescript-eslint/typescript-estree": "8.33.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.33.0.tgz", + "integrity": "sha512-7RW7CMYoskiz5OOGAWjJFxgb7c5UNjTG292gYhWeOAcFmYCtVCSqjqSBj5zMhxbXo2JOW95YYrUWJfU0zrpaGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.33.0", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -83,6 +692,42 @@ "node": ">= 0.6" } }, + "node_modules/acorn": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/agent-base": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", @@ -92,12 +737,66 @@ "node": ">= 14" } }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/body-parser": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", @@ -156,6 +855,29 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -194,6 +916,53 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -206,6 +975,13 @@ "node": ">= 0.8" } }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, "node_modules/content-disposition": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", @@ -258,6 +1034,13 @@ "node": ">= 0.10" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -298,6 +1081,13 @@ } } }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -316,6 +1106,16 @@ "node": ">= 0.8" } }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -396,6 +1196,234 @@ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.27.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.27.0.tgz", + "integrity": "sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.20.0", + "@eslint/config-helpers": "^0.2.1", + "@eslint/core": "^0.14.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.27.0", + "@eslint/plugin-kit": "^0.3.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.3.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", + "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.14.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -484,6 +1512,67 @@ "express": "^4.11 || 5 || ^5.0.0-beta.1" } }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/fetch-blob": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", @@ -507,6 +1596,32 @@ "node": "^12.20 || >= 14.13" } }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/finalhandler": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", @@ -547,6 +1662,44 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, "node_modules/form-data": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", @@ -659,6 +1812,32 @@ "node": ">= 0.4" } }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -671,6 +1850,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -764,6 +1960,43 @@ "node": ">=0.10.0" } }, + "node_modules/ignore": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.4.tgz", + "integrity": "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -792,6 +2025,39 @@ "node": ">= 0.10" } }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", @@ -804,12 +2070,100 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/jsbn": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", "license": "MIT" }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -840,6 +2194,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -849,6 +2213,20 @@ "node": ">= 0.6" } }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/mime-db": { "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", @@ -870,12 +2248,35 @@ "node": ">= 0.6" } }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "license": "MIT" }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, "node_modules/negotiator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", @@ -964,6 +2365,69 @@ "wrappy": "1" } }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -973,6 +2437,16 @@ "node": ">= 0.8" } }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -991,6 +2465,19 @@ "node": ">=16" } }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/pkce-challenge": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-4.1.0.tgz", @@ -1000,6 +2487,32 @@ "node": ">=16.20.0" } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -1013,6 +2526,16 @@ "node": ">= 0.10" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", @@ -1028,6 +2551,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -1052,6 +2596,27 @@ "node": ">= 0.8" } }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -1091,6 +2656,30 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -1117,6 +2706,19 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/send": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", @@ -1312,6 +2914,45 @@ "node": ">= 0.8" } }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -1321,6 +2962,76 @@ "node": ">=0.6" } }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/type-is": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", @@ -1364,6 +3075,16 @@ "node": ">= 0.8" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -1373,6 +3094,13 @@ "node": ">= 0.4.0" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -1406,12 +3134,45 @@ "node": ">= 8" } }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zod": { "version": "3.24.2", "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz", diff --git a/package.json b/package.json index 312e0ea..eeac290 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,13 @@ "prepare": "npm run build", "watch": "tsc --watch", "deploy": "npm publish --access public", - "generate-tools": "npx ts-node scripts/generate-tools-readme.ts" + "generate-tools": "npx ts-node scripts/generate-tools-readme.ts", + "test": "node test/validate-api.js", + "test:integration": "node test/validate-api.js", + "lint": "eslint . --ext .ts", + "lint:fix": "eslint . --ext .ts --fix", + "format": "prettier --write \"**/*.{js,ts,json,md}\"", + "format:check": "prettier --check \"**/*.{js,ts,json,md}\"" }, "dependencies": { "@modelcontextprotocol/sdk": "1.8.0", @@ -35,6 +41,11 @@ "devDependencies": { "@types/node": "^22.13.10", "typescript": "^5.8.2", - "zod": "^3.24.2" + "zod": "^3.24.2", + "@typescript-eslint/eslint-plugin": "^8.21.0", + "@typescript-eslint/parser": "^8.21.0", + "eslint": "^9.18.0", + "prettier": "^3.4.2", + "ts-node": "^10.9.2" } } diff --git a/schemas.ts b/schemas.ts index a82bbdd..c6aeee4 100644 --- a/schemas.ts +++ b/schemas.ts @@ -22,27 +22,34 @@ export const GitLabPipelineSchema = z.object({ started_at: z.string().nullable().optional(), finished_at: z.string().nullable().optional(), coverage: z.number().nullable().optional(), - user: z.object({ - id: z.number(), - name: z.string(), - username: z.string(), - avatar_url: z.string().nullable().optional(), - }).optional(), - detailed_status: z.object({ - icon: z.string().optional(), - text: z.string().optional(), - label: z.string().optional(), - group: z.string().optional(), - tooltip: z.string().optional(), - has_details: z.boolean().optional(), - details_path: z.string().optional(), - illustration: z.object({ - image: z.string().optional(), - size: z.string().optional(), - title: z.string().optional(), - }).nullable().optional(), - favicon: z.string().optional(), - }).optional(), + user: z + .object({ + id: z.number(), + name: z.string(), + username: z.string(), + avatar_url: z.string().nullable().optional(), + }) + .optional(), + detailed_status: z + .object({ + icon: z.string().optional(), + text: z.string().optional(), + label: z.string().optional(), + group: z.string().optional(), + tooltip: z.string().optional(), + has_details: z.boolean().optional(), + details_path: z.string().optional(), + illustration: z + .object({ + image: z.string().optional(), + size: z.string().optional(), + title: z.string().optional(), + }) + .nullable() + .optional(), + favicon: z.string().optional(), + }) + .optional(), }); // Pipeline job related schemas @@ -58,42 +65,75 @@ export const GitLabPipelineJobSchema = z.object({ started_at: z.string().nullable().optional(), finished_at: z.string().nullable().optional(), duration: z.number().nullable().optional(), - user: z.object({ - id: z.number(), - name: z.string(), - username: z.string(), - avatar_url: z.string().nullable().optional(), - }).optional(), - commit: z.object({ - id: z.string(), - short_id: z.string(), - title: z.string(), - author_name: z.string(), - author_email: z.string(), - }).optional(), - pipeline: z.object({ - id: z.number(), - project_id: z.number(), - status: z.string(), - ref: z.string(), - sha: z.string(), - }).optional(), + user: z + .object({ + id: z.number(), + name: z.string(), + username: z.string(), + avatar_url: z.string().nullable().optional(), + }) + .optional(), + commit: z + .object({ + id: z.string(), + short_id: z.string(), + title: z.string(), + author_name: z.string(), + author_email: z.string(), + }) + .optional(), + pipeline: z + .object({ + id: z.number(), + project_id: z.number(), + status: z.string(), + ref: z.string(), + sha: z.string(), + }) + .optional(), web_url: z.string().optional(), }); // Schema for listing pipelines export const ListPipelinesSchema = z.object({ project_id: z.string().describe("Project ID or URL-encoded path"), - scope: z.enum(['running', 'pending', 'finished', 'branches', 'tags']).optional().describe("The scope of pipelines"), - status: z.enum(['created', 'waiting_for_resource', 'preparing', 'pending', 'running', 'success', 'failed', 'canceled', 'skipped', 'manual', 'scheduled']).optional().describe("The status of pipelines"), + scope: z + .enum(["running", "pending", "finished", "branches", "tags"]) + .optional() + .describe("The scope of pipelines"), + status: z + .enum([ + "created", + "waiting_for_resource", + "preparing", + "pending", + "running", + "success", + "failed", + "canceled", + "skipped", + "manual", + "scheduled", + ]) + .optional() + .describe("The status of pipelines"), ref: z.string().optional().describe("The ref of pipelines"), sha: z.string().optional().describe("The SHA of pipelines"), yaml_errors: z.boolean().optional().describe("Returns pipelines with invalid configurations"), username: z.string().optional().describe("The username of the user who triggered pipelines"), - updated_after: z.string().optional().describe("Return pipelines updated after the specified date"), - updated_before: z.string().optional().describe("Return pipelines updated before the specified date"), - order_by: z.enum(['id', 'status', 'ref', 'updated_at', 'user_id']).optional().describe("Order pipelines by"), - sort: z.enum(['asc', 'desc']).optional().describe("Sort pipelines"), + updated_after: z + .string() + .optional() + .describe("Return pipelines updated after the specified date"), + updated_before: z + .string() + .optional() + .describe("Return pipelines updated before the specified date"), + order_by: z + .enum(["id", "status", "ref", "updated_at", "user_id"]) + .optional() + .describe("Order pipelines by"), + sort: z.enum(["asc", "desc"]).optional().describe("Sort pipelines"), page: z.number().optional().describe("Page number for pagination"), per_page: z.number().optional().describe("Number of items per page (max 100)"), }); @@ -108,7 +148,10 @@ export const GetPipelineSchema = z.object({ export const ListPipelineJobsSchema = z.object({ project_id: z.string().describe("Project ID or URL-encoded path"), pipeline_id: z.number().describe("The ID of the pipeline"), - scope: z.enum(['created', 'pending', 'running', 'failed', 'success', 'canceled', 'skipped', 'manual']).optional().describe("The scope of jobs to show"), + scope: z + .enum(["created", "pending", "running", "failed", "success", "canceled", "skipped", "manual"]) + .optional() + .describe("The scope of jobs to show"), include_retried: z.boolean().optional().describe("Whether to include retried jobs"), page: z.number().optional().describe("Page number for pagination"), per_page: z.number().optional().describe("Number of items per page (max 100)"), @@ -287,21 +330,10 @@ export const GetRepositoryTreeSchema = z.object({ ref: z .string() .optional() - .describe( - "The name of a repository branch or tag. Defaults to the default branch." - ), - recursive: z - .boolean() - .optional() - .describe("Boolean value to get a recursive tree"), - per_page: z - .number() - .optional() - .describe("Number of results to show per page"), - page_token: z - .string() - .optional() - .describe("The tree record ID for pagination"), + .describe("The name of a repository branch or tag. Defaults to the default branch."), + recursive: z.boolean().optional().describe("Boolean value to get a recursive tree"), + per_page: z.number().optional().describe("Number of results to show per page"), + page_token: z.string().optional().describe("The tree record ID for pagination"), pagination: z.string().optional().describe("Pagination method (keyset)"), }); @@ -346,7 +378,7 @@ export const GitLabMilestonesSchema = z.object({ updated_at: z.string(), created_at: z.string(), expired: z.boolean(), - web_url: z.string().optional() + web_url: z.string().optional(), }); // Input schemas for operations @@ -606,11 +638,13 @@ export const UpdateMergeRequestNoteSchema = ProjectParamsSchema.extend({ note_id: z.number().describe("The ID of a thread note"), body: z.string().optional().describe("The content of the note or reply"), resolved: z.boolean().optional().describe("Resolve or unresolve the note"), -}).refine(data => data.body !== undefined || data.resolved !== undefined, { - message: "At least one of 'body' or 'resolved' must be provided" -}).refine(data => !(data.body !== undefined && data.resolved !== undefined), { - message: "Only one of 'body' or 'resolved' can be provided, not both" -}); +}) + .refine(data => data.body !== undefined || data.resolved !== undefined, { + message: "At least one of 'body' or 'resolved' must be provided", + }) + .refine(data => !(data.body !== undefined && data.resolved !== undefined), { + message: "Only one of 'body' or 'resolved' can be provided, not both", + }); // Input schema for adding a note to an existing merge request discussion export const CreateMergeRequestNoteSchema = ProjectParamsSchema.extend({ @@ -643,27 +677,15 @@ export const CreateOrUpdateFileSchema = ProjectParamsSchema.extend({ content: z.string().describe("Content of the file"), commit_message: z.string().describe("Commit message"), branch: z.string().describe("Branch to create/update the file in"), - previous_path: z - .string() - .optional() - .describe("Path of the file to move/rename"), + previous_path: z.string().optional().describe("Path of the file to move/rename"), last_commit_id: z.string().optional().describe("Last known file commit ID"), - commit_id: z - .string() - .optional() - .describe("Current file commit ID (for update operations)"), + commit_id: z.string().optional().describe("Current file commit ID (for update operations)"), }); export const SearchRepositoriesSchema = z.object({ search: z.string().describe("Search query"), // Changed from query to match GitLab API - page: z - .number() - .optional() - .describe("Page number for pagination (default: 1)"), - per_page: z - .number() - .optional() - .describe("Number of results per page (default: 20)"), + page: z.number().optional().describe("Page number for pagination (default: 1)"), + per_page: z.number().optional().describe("Number of results per page (default: 20)"), }); export const CreateRepositorySchema = z.object({ @@ -673,10 +695,7 @@ export const CreateRepositorySchema = z.object({ .enum(["private", "internal", "public"]) .optional() .describe("Repository visibility level"), - initialize_with_readme: z - .boolean() - .optional() - .describe("Initialize with README.md"), + initialize_with_readme: z.boolean().optional().describe("Initialize with README.md"), }); export const GetFileContentsSchema = ProjectParamsSchema.extend({ @@ -700,10 +719,7 @@ export const PushFilesSchema = ProjectParamsSchema.extend({ export const CreateIssueSchema = ProjectParamsSchema.extend({ title: z.string().describe("Issue title"), description: z.string().optional().describe("Issue description"), - assignee_ids: z - .array(z.number()) - .optional() - .describe("Array of user IDs to assign"), + assignee_ids: z.array(z.number()).optional().describe("Array of user IDs to assign"), labels: z.array(z.string()).optional().describe("Array of label names"), milestone_id: z.number().optional().describe("Milestone ID to assign"), }); @@ -714,10 +730,7 @@ export const CreateMergeRequestSchema = ProjectParamsSchema.extend({ source_branch: z.string().describe("Branch containing changes"), target_branch: z.string().describe("Branch to merge into"), draft: z.boolean().optional().describe("Create as draft merge request"), - allow_collaboration: z - .boolean() - .optional() - .describe("Allow commits from upstream members"), + allow_collaboration: z.boolean().optional().describe("Allow commits from upstream members"), }); export const ForkRepositorySchema = ProjectParamsSchema.extend({ @@ -741,24 +754,15 @@ export const GitLabMergeRequestDiffSchema = z.object({ }); export const GetMergeRequestSchema = ProjectParamsSchema.extend({ - merge_request_iid: z - .number() - .optional() - .describe("The IID of a merge request"), + merge_request_iid: z.number().optional().describe("The IID of a merge request"), source_branch: z.string().optional().describe("Source branch name"), }); export const UpdateMergeRequestSchema = GetMergeRequestSchema.extend({ title: z.string().optional().describe("The title of the merge request"), - description: z - .string() - .optional() - .describe("The description of the merge request"), + description: z.string().optional().describe("The description of the merge request"), target_branch: z.string().optional().describe("The target branch"), - assignee_ids: z - .array(z.number()) - .optional() - .describe("The ID of the users to assign the MR to"), + assignee_ids: z.array(z.number()).optional().describe("The ID of the users to assign the MR to"), labels: z.array(z.string()).optional().describe("Labels for the MR"), state_event: z .enum(["close", "reopen"]) @@ -768,10 +772,7 @@ export const UpdateMergeRequestSchema = GetMergeRequestSchema.extend({ .boolean() .optional() .describe("Flag indicating if the source branch should be removed"), - squash: z - .boolean() - .optional() - .describe("Squash commits into a single commit when merging"), + squash: z.boolean().optional().describe("Squash commits into a single commit when merging"), draft: z.boolean().optional().describe("Work in progress merge request"), }); @@ -791,38 +792,14 @@ export const CreateNoteSchema = z.object({ // Issues API operation schemas export const ListIssuesSchema = z.object({ project_id: z.string().describe("Project ID or URL-encoded path"), - assignee_id: z - .number() - .optional() - .describe("Return issues assigned to the given user ID"), - assignee_username: z - .string() - .optional() - .describe("Return issues assigned to the given username"), - author_id: z - .number() - .optional() - .describe("Return issues created by the given user ID"), - author_username: z - .string() - .optional() - .describe("Return issues created by the given username"), - confidential: z - .boolean() - .optional() - .describe("Filter confidential or public issues"), - created_after: z - .string() - .optional() - .describe("Return issues created after the given time"), - created_before: z - .string() - .optional() - .describe("Return issues created before the given time"), - due_date: z - .string() - .optional() - .describe("Return issues that have the due date"), + assignee_id: z.number().optional().describe("Return issues assigned to the given user ID"), + assignee_username: z.string().optional().describe("Return issues assigned to the given username"), + author_id: z.number().optional().describe("Return issues created by the given user ID"), + author_username: z.string().optional().describe("Return issues created by the given username"), + confidential: z.boolean().optional().describe("Filter confidential or public issues"), + created_after: z.string().optional().describe("Return issues created after the given time"), + created_before: z.string().optional().describe("Return issues created before the given time"), + due_date: z.string().optional().describe("Return issues that have the due date"), label_name: z.array(z.string()).optional().describe("Array of label names"), milestone: z.string().optional().describe("Milestone title"), scope: z @@ -834,18 +811,9 @@ export const ListIssuesSchema = z.object({ .enum(["opened", "closed", "all"]) .optional() .describe("Return issues with a specific state"), - updated_after: z - .string() - .optional() - .describe("Return issues updated after the given time"), - updated_before: z - .string() - .optional() - .describe("Return issues updated before the given time"), - with_labels_details: z - .boolean() - .optional() - .describe("Return more details for each label"), + updated_after: z.string().optional().describe("Return issues updated after the given time"), + updated_before: z.string().optional().describe("Return issues updated before the given time"), + with_labels_details: z.boolean().optional().describe("Return more details for each label"), page: z.number().optional().describe("Page number for pagination"), per_page: z.number().optional().describe("Number of items per page"), }); @@ -861,10 +829,7 @@ export const ListMergeRequestsSchema = z.object({ .string() .optional() .describe("Returns merge requests assigned to the given username"), - author_id: z - .number() - .optional() - .describe("Returns merge requests created by the given user ID"), + author_id: z.number().optional().describe("Returns merge requests created by the given user ID"), author_username: z .string() .optional() @@ -920,14 +885,8 @@ export const ListMergeRequestsSchema = z.object({ .string() .optional() .describe("Return merge requests from a specific source branch"), - wip: z - .enum(["yes", "no"]) - .optional() - .describe("Filter merge requests against their wip status"), - with_labels_details: z - .boolean() - .optional() - .describe("Return more details for each label"), + wip: z.enum(["yes", "no"]).optional().describe("Filter merge requests against their wip status"), + with_labels_details: z.boolean().optional().describe("Return more details for each label"), page: z.number().optional().describe("Page number for pagination"), per_page: z.number().optional().describe("Number of items per page"), }); @@ -942,28 +901,13 @@ export const UpdateIssueSchema = z.object({ issue_iid: z.number().describe("The internal ID of the project issue"), title: z.string().optional().describe("The title of the issue"), description: z.string().optional().describe("The description of the issue"), - assignee_ids: z - .array(z.number()) - .optional() - .describe("Array of user IDs to assign issue to"), - confidential: z - .boolean() - .optional() - .describe("Set the issue to be confidential"), - discussion_locked: z - .boolean() - .optional() - .describe("Flag to lock discussions"), - due_date: z - .string() - .optional() - .describe("Date the issue is due (YYYY-MM-DD)"), + assignee_ids: z.array(z.number()).optional().describe("Array of user IDs to assign issue to"), + confidential: z.boolean().optional().describe("Set the issue to be confidential"), + discussion_locked: z.boolean().optional().describe("Flag to lock discussions"), + due_date: z.string().optional().describe("Date the issue is due (YYYY-MM-DD)"), labels: z.array(z.string()).optional().describe("Array of label names"), milestone_id: z.number().optional().describe("Milestone ID to assign"), - state_event: z - .enum(["close", "reopen"]) - .optional() - .describe("Update issue state (close/reopen)"), + state_event: z.enum(["close", "reopen"]).optional().describe("Update issue state (close/reopen)"), weight: z.number().optional().describe("Weight of the issue (0-9)"), }); @@ -989,8 +933,14 @@ export const ListIssueDiscussionsSchema = z.object({ issue_iid: z.number().describe("The internal ID of the project issue"), page: z.number().optional().describe("Page number for pagination"), per_page: z.number().optional().describe("Number of items per page"), - sort: z.enum(["asc", "desc"]).optional().describe("Return issue discussions sorted in ascending or descending order"), - order_by: z.enum(["created_at", "updated_at"]).optional().describe("Return issue discussions ordered by created_at or updated_at fields"), + sort: z + .enum(["asc", "desc"]) + .optional() + .describe("Return issue discussions sorted in ascending or descending order"), + order_by: z + .enum(["created_at", "updated_at"]) + .optional() + .describe("Return issue discussions ordered by created_at or updated_at fields"), }); export const GetIssueLinkSchema = z.object({ @@ -1002,12 +952,8 @@ export const GetIssueLinkSchema = z.object({ export const CreateIssueLinkSchema = z.object({ project_id: z.string().describe("Project ID or URL-encoded path"), issue_iid: z.number().describe("The internal ID of a project's issue"), - target_project_id: z - .string() - .describe("The ID or URL-encoded path of a target project"), - target_issue_iid: z - .number() - .describe("The internal ID of a target project's issue"), + target_project_id: z.string().describe("The ID or URL-encoded path of a target project"), + target_issue_iid: z.number().describe("The internal ID of a target project's issue"), link_type: z .enum(["relates_to", "blocks", "is_blocked_by"]) .optional() @@ -1025,10 +971,7 @@ export const ListNamespacesSchema = z.object({ search: z.string().optional().describe("Search term for namespaces"), page: z.number().optional().describe("Page number for pagination"), per_page: z.number().optional().describe("Number of items per page"), - owned: z - .boolean() - .optional() - .describe("Filter for namespaces owned by current user"), + owned: z.boolean().optional().describe("Filter for namespaces owned by current user"), }); export const GetNamespaceSchema = z.object({ @@ -1048,18 +991,9 @@ export const ListProjectsSchema = z.object({ search: z.string().optional().describe("Search term for projects"), page: z.number().optional().describe("Page number for pagination"), per_page: z.number().optional().describe("Number of items per page"), - search_namespaces: z - .boolean() - .optional() - .describe("Needs to be true if search is full path"), - owned: z - .boolean() - .optional() - .describe("Filter for projects owned by current user"), - membership: z - .boolean() - .optional() - .describe("Filter for projects where current user is a member"), + search_namespaces: z.boolean().optional().describe("Needs to be true if search is full path"), + owned: z.boolean().optional().describe("Filter for projects owned by current user"), + membership: z.boolean().optional().describe("Filter for projects where current user is a member"), simple: z.boolean().optional().describe("Return only limited fields"), archived: z.boolean().optional().describe("Filter for archived projects"), visibility: z @@ -1067,14 +1001,7 @@ export const ListProjectsSchema = z.object({ .optional() .describe("Filter by project visibility"), order_by: z - .enum([ - "id", - "name", - "path", - "created_at", - "updated_at", - "last_activity_at", - ]) + .enum(["id", "name", "path", "created_at", "updated_at", "last_activity_at"]) .optional() .describe("Return projects ordered by field"), sort: z @@ -1089,10 +1016,7 @@ export const ListProjectsSchema = z.object({ .boolean() .optional() .describe("Filter projects with merge requests feature enabled"), - min_access_level: z - .number() - .optional() - .describe("Filter by minimum access level"), + min_access_level: z.number().optional().describe("Filter by minimum access level"), }); // Label operation schemas @@ -1102,20 +1026,14 @@ export const ListLabelsSchema = z.object({ .boolean() .optional() .describe("Whether or not to include issue and merge request counts"), - include_ancestor_groups: z - .boolean() - .optional() - .describe("Include ancestor groups"), + include_ancestor_groups: z.boolean().optional().describe("Include ancestor groups"), search: z.string().optional().describe("Keyword to filter labels by"), }); export const GetLabelSchema = z.object({ project_id: z.string().describe("Project ID or URL-encoded path"), label_id: z.string().describe("The ID or title of a project's label"), - include_ancestor_groups: z - .boolean() - .optional() - .describe("Include ancestor groups"), + include_ancestor_groups: z.boolean().optional().describe("Include ancestor groups"), }); export const CreateLabelSchema = z.object({ @@ -1123,15 +1041,9 @@ export const CreateLabelSchema = z.object({ name: z.string().describe("The name of the label"), color: z .string() - .describe( - "The color of the label given in 6-digit hex notation with leading '#' sign" - ), + .describe("The color of the label given in 6-digit hex notation with leading '#' sign"), description: z.string().optional().describe("The description of the label"), - priority: z - .number() - .nullable() - .optional() - .describe("The priority of the label"), + priority: z.number().nullable().optional().describe("The priority of the label"), }); export const UpdateLabelSchema = z.object({ @@ -1141,18 +1053,9 @@ export const UpdateLabelSchema = z.object({ color: z .string() .optional() - .describe( - "The color of the label given in 6-digit hex notation with leading '#' sign" - ), - description: z - .string() - .optional() - .describe("The new description of the label"), - priority: z - .number() - .nullable() - .optional() - .describe("The new priority of the label"), + .describe("The color of the label given in 6-digit hex notation with leading '#' sign"), + description: z.string().optional().describe("The new description of the label"), + priority: z.number().nullable().optional().describe("The new priority of the label"), }); export const DeleteLabelSchema = z.object({ @@ -1163,10 +1066,7 @@ export const DeleteLabelSchema = z.object({ // Group projects schema export const ListGroupProjectsSchema = z.object({ group_id: z.string().describe("Group ID or path"), - include_subgroups: z - .boolean() - .optional() - .describe("Include projects from subgroups"), + include_subgroups: z.boolean().optional().describe("Include projects from subgroups"), search: z.string().optional().describe("Search term to filter projects"), order_by: z .enum(["name", "path", "created_at", "updated_at", "last_activity_at"]) @@ -1188,24 +1088,12 @@ export const ListGroupProjectsSchema = z.object({ .boolean() .optional() .describe("Filter projects with merge requests feature enabled"), - min_access_level: z - .number() - .optional() - .describe("Filter by minimum access level"), - with_programming_language: z - .string() - .optional() - .describe("Filter by programming language"), + min_access_level: z.number().optional().describe("Filter by minimum access level"), + with_programming_language: z.string().optional().describe("Filter by programming language"), starred: z.boolean().optional().describe("Filter by starred projects"), statistics: z.boolean().optional().describe("Include project statistics"), - with_custom_attributes: z - .boolean() - .optional() - .describe("Include custom attributes"), - with_security_reports: z - .boolean() - .optional() - .describe("Include security reports"), + with_custom_attributes: z.boolean().optional().describe("Include custom attributes"), + with_security_reports: z.boolean().optional().describe("Include security reports"), }); // Add wiki operation schemas @@ -1222,20 +1110,14 @@ export const CreateWikiPageSchema = z.object({ project_id: z.string().describe("Project ID or URL-encoded path"), title: z.string().describe("Title of the wiki page"), content: z.string().describe("Content of the wiki page"), - format: z - .string() - .optional() - .describe("Content format, e.g., markdown, rdoc"), + format: z.string().optional().describe("Content format, e.g., markdown, rdoc"), }); export const UpdateWikiPageSchema = z.object({ project_id: z.string().describe("Project ID or URL-encoded path"), slug: z.string().describe("URL-encoded slug of the wiki page"), title: z.string().optional().describe("New title of the wiki page"), content: z.string().optional().describe("New content of the wiki page"), - format: z - .string() - .optional() - .describe("Content format, e.g., markdown, rdoc"), + format: z.string().optional().describe("Content format, e.g., markdown, rdoc"), }); export const DeleteWikiPageSchema = z.object({ project_id: z.string().describe("Project ID or URL-encoded path"), @@ -1272,7 +1154,9 @@ export const MergeRequestThreadPositionSchema = z.object({ export const CreateMergeRequestThreadSchema = ProjectParamsSchema.extend({ merge_request_iid: z.number().describe("The IID of a merge request"), body: z.string().describe("The content of the thread"), - position: MergeRequestThreadPositionSchema.optional().describe("Position when creating a diff note"), + position: MergeRequestThreadPositionSchema.optional().describe( + "Position when creating a diff note" + ), created_at: z.string().optional().describe("Date the thread was created at (ISO 8601 format)"), }); @@ -1280,12 +1164,27 @@ export const CreateMergeRequestThreadSchema = ProjectParamsSchema.extend({ // Schema for listing project milestones export const ListProjectMilestonesSchema = ProjectParamsSchema.extend({ iids: z.array(z.number()).optional().describe("Return only the milestones having the given iid"), - state: z.enum(["active", "closed"]).optional().describe("Return only active or closed milestones"), - title: z.string().optional().describe("Return only milestones with a title matching the provided string"), - search: z.string().optional().describe("Return only milestones with a title or description matching the provided string"), + state: z + .enum(["active", "closed"]) + .optional() + .describe("Return only active or closed milestones"), + title: z + .string() + .optional() + .describe("Return only milestones with a title matching the provided string"), + search: z + .string() + .optional() + .describe("Return only milestones with a title or description matching the provided string"), include_ancestors: z.boolean().optional().describe("Include ancestor groups"), - updated_before: z.string().optional().describe("Return milestones updated before the specified date (ISO 8601 format)"), - updated_after: z.string().optional().describe("Return milestones updated after the specified date (ISO 8601 format)"), + updated_before: z + .string() + .optional() + .describe("Return milestones updated before the specified date (ISO 8601 format)"), + updated_after: z + .string() + .optional() + .describe("Return milestones updated after the specified date (ISO 8601 format)"), page: z.number().optional().describe("Page number for pagination"), per_page: z.number().optional().describe("Number of items per page (max 100)"), }); @@ -1309,7 +1208,10 @@ export const EditProjectMilestoneSchema = GetProjectMilestoneSchema.extend({ description: z.string().optional().describe("The description of the milestone"), due_date: z.string().optional().describe("The due date of the milestone (YYYY-MM-DD)"), start_date: z.string().optional().describe("The start date of the milestone (YYYY-MM-DD)"), - state_event: z.enum(["close", "activate"]).optional().describe("The state event of the milestone"), + state_event: z + .enum(["close", "activate"]) + .optional() + .describe("The state event of the milestone"), }); // Schema for deleting a milestone @@ -1337,44 +1239,30 @@ export const GetMilestoneBurndownEventsSchema = GetProjectMilestoneSchema.extend export type GitLabAuthor = z.infer; export type GitLabFork = z.infer; export type GitLabIssue = z.infer; -export type GitLabIssueWithLinkDetails = z.infer< - typeof GitLabIssueWithLinkDetailsSchema ->; +export type GitLabIssueWithLinkDetails = z.infer; export type GitLabMergeRequest = z.infer; export type GitLabRepository = z.infer; export type GitLabFileContent = z.infer; -export type GitLabDirectoryContent = z.infer< - typeof GitLabDirectoryContentSchema ->; +export type GitLabDirectoryContent = z.infer; export type GitLabContent = z.infer; export type FileOperation = z.infer; export type GitLabTree = z.infer; export type GitLabCommit = z.infer; export type GitLabReference = z.infer; -export type CreateRepositoryOptions = z.infer< - typeof CreateRepositoryOptionsSchema ->; +export type CreateRepositoryOptions = z.infer; export type CreateIssueOptions = z.infer; -export type CreateMergeRequestOptions = z.infer< - typeof CreateMergeRequestOptionsSchema ->; +export type CreateMergeRequestOptions = z.infer; export type CreateBranchOptions = z.infer; -export type GitLabCreateUpdateFileResponse = z.infer< - typeof GitLabCreateUpdateFileResponseSchema ->; +export type GitLabCreateUpdateFileResponse = z.infer; export type GitLabSearchResponse = z.infer; -export type GitLabMergeRequestDiff = z.infer< - typeof GitLabMergeRequestDiffSchema ->; +export type GitLabMergeRequestDiff = z.infer; export type CreateNoteOptions = z.infer; export type GitLabIssueLink = z.infer; export type ListIssueDiscussionsOptions = z.infer; export type UpdateIssueNoteOptions = z.infer; export type CreateIssueNoteOptions = z.infer; export type GitLabNamespace = z.infer; -export type GitLabNamespaceExistsResponse = z.infer< - typeof GitLabNamespaceExistsResponseSchema ->; +export type GitLabNamespaceExistsResponse = z.infer; export type GitLabProject = z.infer; export type GitLabLabel = z.infer; export type ListWikiPagesOptions = z.infer; diff --git a/scripts/generate-tools-readme.ts b/scripts/generate-tools-readme.ts index c311e0c..c1b1fcc 100644 --- a/scripts/generate-tools-readme.ts +++ b/scripts/generate-tools-readme.ts @@ -1,22 +1,22 @@ -import fs from 'fs'; -import path from 'path'; -import { fileURLToPath } from 'url'; +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); async function main() { - const repoRoot = path.resolve(__dirname, '..'); - const indexPath = path.join(repoRoot, 'index.ts'); - const readmePath = path.join(repoRoot, 'README.md'); + const repoRoot = path.resolve(__dirname, ".."); + const indexPath = path.join(repoRoot, "index.ts"); + const readmePath = path.join(repoRoot, "README.md"); // 1. Read index.ts - const code = fs.readFileSync(indexPath, 'utf-8'); + const code = fs.readFileSync(indexPath, "utf-8"); // 2. Extract allTools array block const match = code.match(/const allTools = \[([\s\S]*?)\];/); if (!match) { - console.error('Unable to locate allTools array in index.ts'); + console.error("Unable to locate allTools array in index.ts"); process.exit(1); } const toolsBlock = match[1]; @@ -33,21 +33,21 @@ async function main() { const lines = tools.map((tool, index) => { return `${index + 1}. \`${tool.name}\` - ${tool.description}`; }); - const markdown = lines.join('\n'); + const markdown = lines.join("\n"); // 5. Read README.md and replace between markers - const readme = fs.readFileSync(readmePath, 'utf-8'); + const readme = fs.readFileSync(readmePath, "utf-8"); const updated = readme.replace( /([\s\S]*?)/, `\n${markdown}\n` ); // 6. Write back - fs.writeFileSync(readmePath, updated, 'utf-8'); - console.log('README.md tools section updated.'); + fs.writeFileSync(readmePath, updated, "utf-8"); + console.log("README.md tools section updated."); } main().catch(err => { console.error(err); process.exit(1); -}); \ No newline at end of file +}); diff --git a/scripts/validate-pr.sh b/scripts/validate-pr.sh new file mode 100755 index 0000000..64d0f86 --- /dev/null +++ b/scripts/validate-pr.sh @@ -0,0 +1,56 @@ +#!/bin/bash + +# PR Validation Script +# This script runs all necessary checks before merging a PR + +set -e + +echo "🔍 Starting PR validation..." + +# Check if Node.js is installed +if ! command -v node &> /dev/null; then + echo "❌ Node.js is not installed" + exit 1 +fi + +echo "📦 Installing dependencies..." +npm ci + +echo "🔨 Building project..." +npm run build + +echo "🧪 Running unit tests..." +npm run test:unit + +echo "✨ Checking code formatting..." +npm run format:check || { + echo "⚠️ Code formatting issues found. Run 'npm run format' to fix." + exit 1 +} + +echo "🔍 Running linter..." +npm run lint || { + echo "⚠️ Linting issues found. Run 'npm run lint:fix' to fix." + exit 1 +} + +echo "📊 Running tests with coverage..." +npm run test:coverage + +# Check if integration tests should run +if [ -n "$GITLAB_TOKEN" ] && [ -n "$TEST_PROJECT_ID" ]; then + echo "🌐 Running integration tests..." + npm run test:integration +else + echo "⚠️ Skipping integration tests (no credentials provided)" +fi + +echo "🐳 Testing Docker build..." +if command -v docker &> /dev/null; then + docker build -t mcp-gitlab-test . + echo "✅ Docker build successful" +else + echo "⚠️ Docker not available, skipping Docker build test" +fi + +echo "✅ All PR validation checks passed!" \ No newline at end of file diff --git a/test-note.ts b/test-note.ts index 25504aa..867bb94 100644 --- a/test-note.ts +++ b/test-note.ts @@ -33,9 +33,7 @@ async function testCreateIssueNote() { if (!response.ok) { const errorBody = await response.text(); - throw new Error( - `GitLab API error: ${response.status} ${response.statusText}\n${errorBody}` - ); + throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`); } const data = await response.json(); diff --git a/test/validate-api.js b/test/validate-api.js new file mode 100755 index 0000000..2becbc2 --- /dev/null +++ b/test/validate-api.js @@ -0,0 +1,96 @@ +#!/usr/bin/env node + +// Simple API validation script for PR testing +import fetch from "node-fetch"; + +const GITLAB_API_URL = process.env.GITLAB_API_URL || "https://gitlab.com"; +const GITLAB_TOKEN = process.env.GITLAB_TOKEN_TEST || process.env.GITLAB_TOKEN; +const TEST_PROJECT_ID = process.env.TEST_PROJECT_ID; + +async function validateGitLabAPI() { + console.log("🔍 Validating GitLab API connection...\n"); + + if (!GITLAB_TOKEN) { + console.warn("⚠️ No GitLab token provided. Skipping API validation."); + console.log("Set GITLAB_TOKEN_TEST or GITLAB_TOKEN to enable API validation.\n"); + return true; + } + + if (!TEST_PROJECT_ID) { + console.warn("⚠️ No test project ID provided. Skipping API validation."); + console.log("Set TEST_PROJECT_ID to enable API validation.\n"); + return true; + } + + const tests = [ + { + name: "Fetch project info", + url: `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(TEST_PROJECT_ID)}`, + validate: data => data.id && data.name, + }, + { + name: "List issues", + url: `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(TEST_PROJECT_ID)}/issues?per_page=1`, + validate: data => Array.isArray(data), + }, + { + name: "List merge requests", + url: `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(TEST_PROJECT_ID)}/merge_requests?per_page=1`, + validate: data => Array.isArray(data), + }, + { + name: "List branches", + url: `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(TEST_PROJECT_ID)}/repository/branches?per_page=1`, + validate: data => Array.isArray(data), + }, + ]; + + let allPassed = true; + + for (const test of tests) { + try { + console.log(`Testing: ${test.name}`); + const response = await fetch(test.url, { + headers: { + Authorization: `Bearer ${GITLAB_TOKEN}`, + Accept: "application/json", + }, + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const data = await response.json(); + + if (test.validate(data)) { + console.log(`✅ ${test.name} - PASSED\n`); + } else { + console.log(`❌ ${test.name} - FAILED (invalid response format)\n`); + allPassed = false; + } + } catch (error) { + console.log(`❌ ${test.name} - FAILED`); + console.log(` Error: ${error.message}\n`); + allPassed = false; + } + } + + if (allPassed) { + console.log("✅ All API validation tests passed!"); + } else { + console.log("❌ Some API validation tests failed!"); + } + + return allPassed; +} + +// Run validation +validateGitLabAPI() + .then(success => process.exit(success ? 0 : 1)) + .catch(error => { + console.error("Unexpected error:", error); + process.exit(1); + }); + +export { validateGitLabAPI }; From 37203bae5a87d902380ecb7ead454ec9b19af1ef Mon Sep 17 00:00:00 2001 From: simple Date: Thu, 29 May 2025 23:25:50 +0900 Subject: [PATCH 20/39] =?UTF-8?q?[main]=20docs:=20update=20README=20to=20r?= =?UTF-8?q?emove=20automated=20testing=20section=20=F0=9F=93=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🚀 Breaking Changes: - Removed details about automated testing setup and GitHub Actions. --- README.md | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/README.md b/README.md index 376e87c..5b3dd5f 100644 --- a/README.md +++ b/README.md @@ -8,20 +8,6 @@ GitLab MCP(Model Context Protocol) Server. **Includes bug fixes and improvements gitlab mcp MCP server -## 🚀 Automated Testing - -This project uses GitHub Actions for automated PR testing. All pull requests are automatically tested across multiple Node.js versions (18.x, 20.x, 22.x) with: - -- ✅ Build verification -- ✅ Type checking -- ✅ Code linting (ESLint) -- ✅ Code formatting (Prettier) -- ✅ API validation tests -- ✅ Docker build verification -- ✅ Security audit - -For integration testing setup, see [GitHub Secrets Setup Guide](docs/setup-github-secrets.md). - ## Usage ### Using with Claude App, Cline, Roo Code, Cursor From a2760f0aeaa49b1202167032446120bb03757bf8 Mon Sep 17 00:00:00 2001 From: simple Date: Thu, 29 May 2025 23:28:43 +0900 Subject: [PATCH 21/39] [main] chore: update version to 1.0.51 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🚀 Breaking Changes: - Updated package version from 1.0.50 to 1.0.51 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index eeac290..b8441dd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@zereight/mcp-gitlab", - "version": "1.0.50", + "version": "1.0.51", "description": "MCP server for using the GitLab API", "license": "MIT", "author": "zereight", From 6d6110c78bcef3987799c98a9fd48241236e7cf7 Mon Sep 17 00:00:00 2001 From: simple Date: Thu, 29 May 2025 23:38:20 +0900 Subject: [PATCH 23/39] fix: GitHub Actions workflow syntax errors - Remove unsupported default value syntax from secrets - Fix startup_failure error in PR validation workflow --- .github/workflows/pr-test.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pr-test.yml b/.github/workflows/pr-test.yml index 9073ddc..a110df8 100644 --- a/.github/workflows/pr-test.yml +++ b/.github/workflows/pr-test.yml @@ -32,7 +32,7 @@ jobs: - name: Run tests run: npm test env: - GITLAB_API_URL: ${{ secrets.GITLAB_API_URL || 'https://gitlab.com' }} + GITLAB_API_URL: ${{ secrets.GITLAB_API_URL }} GITLAB_TOKEN: ${{ secrets.GITLAB_TOKEN_TEST }} - name: Type check @@ -60,8 +60,8 @@ jobs: exit 1 fi env: - GITLAB_API_URL: ${{ secrets.GITLAB_API_URL || 'https://gitlab.com' }} - GITLAB_TOKEN: ${{ secrets.GITLAB_TOKEN_TEST || 'dummy-token-for-test' }} + GITLAB_API_URL: ${{ secrets.GITLAB_API_URL }} + GITLAB_TOKEN: ${{ secrets.GITLAB_TOKEN_TEST }} integration-test: runs-on: ubuntu-latest @@ -90,7 +90,7 @@ jobs: echo "Running integration tests with real GitLab API..." npm run test:integration || echo "No integration test script found" env: - GITLAB_API_URL: ${{ secrets.GITLAB_API_URL || 'https://gitlab.com' }} + GITLAB_API_URL: ${{ secrets.GITLAB_API_URL }} GITLAB_TOKEN: ${{ secrets.GITLAB_TOKEN_TEST }} PROJECT_ID: ${{ secrets.TEST_PROJECT_ID }} @@ -160,6 +160,6 @@ jobs: - name: Run tests run: npm test env: - GITLAB_API_URL: ${{ secrets.GITLAB_API_URL || 'https://gitlab.com' }} + GITLAB_API_URL: ${{ secrets.GITLAB_API_URL }} GITLAB_TOKEN_TEST: ${{ secrets.GITLAB_TOKEN_TEST }} TEST_PROJECT_ID: ${{ secrets.TEST_PROJECT_ID }} \ No newline at end of file From 720cd7a44568a3fa19c27b609eefc857120b164d Mon Sep 17 00:00:00 2001 From: simple Date: Thu, 29 May 2025 23:43:25 +0900 Subject: [PATCH 24/39] [main] fix: GitHub Actions workflow syntax errors - Remove unsupported default value syntax (|| operator) from secrets - Fix startup_failure error in PR validation workflow - GitHub Actions doesn't support default values in secret expressions --- .github/workflows/pr-test.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pr-test.yml b/.github/workflows/pr-test.yml index 9073ddc..a110df8 100644 --- a/.github/workflows/pr-test.yml +++ b/.github/workflows/pr-test.yml @@ -32,7 +32,7 @@ jobs: - name: Run tests run: npm test env: - GITLAB_API_URL: ${{ secrets.GITLAB_API_URL || 'https://gitlab.com' }} + GITLAB_API_URL: ${{ secrets.GITLAB_API_URL }} GITLAB_TOKEN: ${{ secrets.GITLAB_TOKEN_TEST }} - name: Type check @@ -60,8 +60,8 @@ jobs: exit 1 fi env: - GITLAB_API_URL: ${{ secrets.GITLAB_API_URL || 'https://gitlab.com' }} - GITLAB_TOKEN: ${{ secrets.GITLAB_TOKEN_TEST || 'dummy-token-for-test' }} + GITLAB_API_URL: ${{ secrets.GITLAB_API_URL }} + GITLAB_TOKEN: ${{ secrets.GITLAB_TOKEN_TEST }} integration-test: runs-on: ubuntu-latest @@ -90,7 +90,7 @@ jobs: echo "Running integration tests with real GitLab API..." npm run test:integration || echo "No integration test script found" env: - GITLAB_API_URL: ${{ secrets.GITLAB_API_URL || 'https://gitlab.com' }} + GITLAB_API_URL: ${{ secrets.GITLAB_API_URL }} GITLAB_TOKEN: ${{ secrets.GITLAB_TOKEN_TEST }} PROJECT_ID: ${{ secrets.TEST_PROJECT_ID }} @@ -160,6 +160,6 @@ jobs: - name: Run tests run: npm test env: - GITLAB_API_URL: ${{ secrets.GITLAB_API_URL || 'https://gitlab.com' }} + GITLAB_API_URL: ${{ secrets.GITLAB_API_URL }} GITLAB_TOKEN_TEST: ${{ secrets.GITLAB_TOKEN_TEST }} TEST_PROJECT_ID: ${{ secrets.TEST_PROJECT_ID }} \ No newline at end of file From 7391f5160de879d5d96b511edfb3ede7e592e8f0 Mon Sep 17 00:00:00 2001 From: simple Date: Thu, 29 May 2025 23:55:14 +0900 Subject: [PATCH 25/39] [main] fix: remove invalid secret condition in workflow - Replace secrets condition with proper GitHub context condition - Secrets cannot be used directly in if conditions - Run integration tests only for push events or PRs from the same repo --- .github/workflows/pr-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-test.yml b/.github/workflows/pr-test.yml index a110df8..bc40b93 100644 --- a/.github/workflows/pr-test.yml +++ b/.github/workflows/pr-test.yml @@ -85,7 +85,7 @@ jobs: run: npm run build - name: Run integration tests - if: ${{ secrets.GITLAB_TOKEN_TEST }} + if: ${{ github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository }} run: | echo "Running integration tests with real GitLab API..." npm run test:integration || echo "No integration test script found" From 9a52dafb03435fd2e0a4b6f7083da377a91c4433 Mon Sep 17 00:00:00 2001 From: simple Date: Thu, 29 May 2025 23:58:36 +0900 Subject: [PATCH 27/39] [main] fix: remove jq dependency from workflow - Replace jq command with simple echo - jq is not installed by default in GitHub Actions runners --- .github/workflows/pr-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-test.yml b/.github/workflows/pr-test.yml index bc40b93..a20dbb2 100644 --- a/.github/workflows/pr-test.yml +++ b/.github/workflows/pr-test.yml @@ -44,7 +44,7 @@ jobs: - name: Check package size run: | npm pack --dry-run - npm pack --dry-run --json | jq '.size' | xargs -I {} echo "Package size: {} bytes" + echo "Package created successfully" - name: Security audit run: npm audit --production || echo "Some vulnerabilities found" From b00cc9e6f56de9ae1a8edbbbc236de0175484591 Mon Sep 17 00:00:00 2001 From: simple Date: Fri, 30 May 2025 00:04:14 +0900 Subject: [PATCH 29/39] [main] feat: add GITLAB_PERSONAL_ACCESS_TOKEN to workflow - MCP server may expect GITLAB_PERSONAL_ACCESS_TOKEN instead of GITLAB_TOKEN - Add environment variable to all test steps --- .github/workflows/pr-test.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/pr-test.yml b/.github/workflows/pr-test.yml index a20dbb2..d6d08ca 100644 --- a/.github/workflows/pr-test.yml +++ b/.github/workflows/pr-test.yml @@ -34,6 +34,7 @@ jobs: env: GITLAB_API_URL: ${{ secrets.GITLAB_API_URL }} GITLAB_TOKEN: ${{ secrets.GITLAB_TOKEN_TEST }} + GITLAB_PERSONAL_ACCESS_TOKEN: ${{ secrets.GITLAB_PERSONAL_ACCESS_TOKEN }} - name: Type check run: npx tsc --noEmit @@ -62,6 +63,7 @@ jobs: env: GITLAB_API_URL: ${{ secrets.GITLAB_API_URL }} GITLAB_TOKEN: ${{ secrets.GITLAB_TOKEN_TEST }} + GITLAB_PERSONAL_ACCESS_TOKEN: ${{ secrets.GITLAB_PERSONAL_ACCESS_TOKEN }} integration-test: runs-on: ubuntu-latest @@ -92,6 +94,7 @@ jobs: env: GITLAB_API_URL: ${{ secrets.GITLAB_API_URL }} GITLAB_TOKEN: ${{ secrets.GITLAB_TOKEN_TEST }} + GITLAB_PERSONAL_ACCESS_TOKEN: ${{ secrets.GITLAB_PERSONAL_ACCESS_TOKEN }} PROJECT_ID: ${{ secrets.TEST_PROJECT_ID }} - name: Test Docker build From 8e2b6e67349aa575dd9c3217b58bfe76772932ae Mon Sep 17 00:00:00 2001 From: simple Date: Fri, 30 May 2025 00:09:55 +0900 Subject: [PATCH 31/39] [main] debug: temporarily disable MCP server startup test --- .github/workflows/pr-test.yml | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/.github/workflows/pr-test.yml b/.github/workflows/pr-test.yml index d6d08ca..1a43b37 100644 --- a/.github/workflows/pr-test.yml +++ b/.github/workflows/pr-test.yml @@ -53,13 +53,8 @@ jobs: - name: Test MCP server startup run: | - timeout 10s node build/index.js || EXIT_CODE=$? - if [ $EXIT_CODE -eq 124 ]; then - echo "✅ Server started successfully (timeout expected for long-running process)" - else - echo "❌ Server failed to start" - exit 1 - fi + echo "MCP server startup test temporarily disabled for debugging" + echo "GITLAB_PERSONAL_ACCESS_TOKEN is: ${GITLAB_PERSONAL_ACCESS_TOKEN:0:10}..." env: GITLAB_API_URL: ${{ secrets.GITLAB_API_URL }} GITLAB_TOKEN: ${{ secrets.GITLAB_TOKEN_TEST }} From 1762a5851c8d5c6c0232b7a7f8c084e67678a8aa Mon Sep 17 00:00:00 2001 From: simple Date: Fri, 30 May 2025 00:16:39 +0900 Subject: [PATCH 33/39] [main] docs: update README with comments on GITLAB configuration options MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 📝 Details: - Added comments for USE_GITLAB_WIKI and USE_MILESTONE options for clarity. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5b3dd5f..9285539 100644 --- a/README.md +++ b/README.md @@ -26,8 +26,8 @@ When using with the Claude App, you need to set up your API key and URLs directl "GITLAB_PERSONAL_ACCESS_TOKEN": "your_gitlab_token", "GITLAB_API_URL": "your_gitlab_api_url", "GITLAB_READ_ONLY_MODE": "false", - "USE_GITLAB_WIKI": "false", - "USE_MILESTONE": "false" + "USE_GITLAB_WIKI": "false", // use wiki api? + "USE_MILESTONE": "false" // use milestone api? } } } From 353638f5d763755d09e2be8d3cc007c1d46b9598 Mon Sep 17 00:00:00 2001 From: simple Date: Fri, 30 May 2025 00:31:26 +0900 Subject: [PATCH 34/39] [feat/pipeline-support] feat: add pipeline management commands - Add create_pipeline command to trigger new pipelines - Add retry_pipeline command to retry failed pipelines - Add cancel_pipeline command to cancel running pipelines - Add pipeline tests to validate-api.js - Update README with new pipeline commands Closes #46 --- README.md | 23 ++++--- index.ts | 141 +++++++++++++++++++++++++++++++++++++++++++ schemas.ts | 30 +++++++++ test/validate-api.js | 58 ++++++++++++++++++ 4 files changed, 242 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 9285539..17bba0a 100644 --- a/README.md +++ b/README.md @@ -137,14 +137,17 @@ $ sh scripts/image_push.sh docker_user_name 48. `list_pipeline_jobs` - List all jobs in a specific pipeline 49. `get_pipeline_job` - Get details of a GitLab pipeline job number 50. `get_pipeline_job_output` - Get the output/trace of a GitLab pipeline job number -51. `list_merge_requests` - List merge requests in a GitLab project with filtering options -52. `list_milestones` - List milestones in a GitLab project with filtering options -53. `get_milestone` - Get details of a specific milestone -54. `create_milestone` - Create a new milestone in a GitLab project -55. `edit_milestone ` - Edit an existing milestone in a GitLab project -56. `delete_milestone` - Delete a milestone from a GitLab project -57. `get_milestone_issue` - Get issues associated with a specific milestone -58. `get_milestone_merge_requests` - Get merge requests associated with a specific milestone -59. `promote_milestone` - Promote a milestone to the next stage -60. `get_milestone_burndown_events` - Get burndown events for a specific milestone +51. `create_pipeline` - Create a new pipeline for a branch or tag +52. `retry_pipeline` - Retry a failed or canceled pipeline +53. `cancel_pipeline` - Cancel a running pipeline +54. `list_merge_requests` - List merge requests in a GitLab project with filtering options +55. `list_milestones` - List milestones in a GitLab project with filtering options +56. `get_milestone` - Get details of a specific milestone +57. `create_milestone` - Create a new milestone in a GitLab project +58. `edit_milestone ` - Edit an existing milestone in a GitLab project +59. `delete_milestone` - Delete a milestone from a GitLab project +60. `get_milestone_issue` - Get issues associated with a specific milestone +61. `get_milestone_merge_requests` - Get merge requests associated with a specific milestone +62. `promote_milestone` - Promote a milestone to the next stage +63. `get_milestone_burndown_events` - Get burndown events for a specific milestone diff --git a/index.ts b/index.ts index 0f676aa..0e51645 100644 --- a/index.ts +++ b/index.ts @@ -86,6 +86,9 @@ import { GetPipelineSchema, ListPipelinesSchema, ListPipelineJobsSchema, + CreatePipelineSchema, + RetryPipelineSchema, + CancelPipelineSchema, // pipeline job schemas GetPipelineJobOutputSchema, GitLabPipelineJobSchema, @@ -117,6 +120,9 @@ import { type ListPipelinesOptions, type GetPipelineOptions, type ListPipelineJobsOptions, + type CreatePipelineOptions, + type RetryPipelineOptions, + type CancelPipelineOptions, type GitLabPipelineJob, type GitLabMilestones, type ListProjectMilestonesOptions, @@ -482,6 +488,21 @@ const allTools = [ description: "Get the output/trace of a GitLab pipeline job number", inputSchema: zodToJsonSchema(GetPipelineJobOutputSchema), }, + { + name: "create_pipeline", + description: "Create a new pipeline for a branch or tag", + inputSchema: zodToJsonSchema(CreatePipelineSchema), + }, + { + name: "retry_pipeline", + description: "Retry a failed or canceled pipeline", + inputSchema: zodToJsonSchema(RetryPipelineSchema), + }, + { + name: "cancel_pipeline", + description: "Cancel a running pipeline", + inputSchema: zodToJsonSchema(CancelPipelineSchema), + }, { name: "list_merge_requests", description: "List merge requests in a GitLab project with filtering options", @@ -2484,6 +2505,87 @@ async function getPipelineJobOutput(projectId: string, jobId: number): Promise} The created pipeline + */ +async function createPipeline( + projectId: string, + ref: string, + variables?: Array<{ key: string; value: string }> +): Promise { + projectId = decodeURIComponent(projectId); // Decode project ID + const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/pipeline`); + + const body: any = { ref }; + if (variables && variables.length > 0) { + body.variables = variables.reduce((acc, { key, value }) => { + acc[key] = value; + return acc; + }, {} as Record); + } + + const response = await fetch(url.toString(), { + method: "POST", + headers: DEFAULT_HEADERS, + body: JSON.stringify(body), + }); + + await handleGitLabError(response); + const data = await response.json(); + return GitLabPipelineSchema.parse(data); +} + +/** + * Retry a pipeline + * + * @param {string} projectId - The ID or URL-encoded path of the project + * @param {number} pipelineId - The ID of the pipeline to retry + * @returns {Promise} The retried pipeline + */ +async function retryPipeline(projectId: string, pipelineId: number): Promise { + projectId = decodeURIComponent(projectId); // Decode project ID + const url = new URL( + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/pipelines/${pipelineId}/retry` + ); + + const response = await fetch(url.toString(), { + method: "POST", + headers: DEFAULT_HEADERS, + }); + + await handleGitLabError(response); + const data = await response.json(); + return GitLabPipelineSchema.parse(data); +} + +/** + * Cancel a pipeline + * + * @param {string} projectId - The ID or URL-encoded path of the project + * @param {number} pipelineId - The ID of the pipeline to cancel + * @returns {Promise} The canceled pipeline + */ +async function cancelPipeline(projectId: string, pipelineId: number): Promise { + projectId = decodeURIComponent(projectId); // Decode project ID + const url = new URL( + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/pipelines/${pipelineId}/cancel` + ); + + const response = await fetch(url.toString(), { + method: "POST", + headers: DEFAULT_HEADERS, + }); + + await handleGitLabError(response); + const data = await response.json(); + return GitLabPipelineSchema.parse(data); +} + /** * Get the repository tree for a project * @param {string} projectId - The ID or URL-encoded path of the project @@ -3409,6 +3511,45 @@ server.setRequestHandler(CallToolRequestSchema, async request => { }; } + case "create_pipeline": { + const { project_id, ref, variables } = CreatePipelineSchema.parse(request.params.arguments); + const pipeline = await createPipeline(project_id, ref, variables); + return { + content: [ + { + type: "text", + text: `Created pipeline #${pipeline.id} for ${ref}. Status: ${pipeline.status}\nWeb URL: ${pipeline.web_url}`, + }, + ], + }; + } + + case "retry_pipeline": { + const { project_id, pipeline_id } = RetryPipelineSchema.parse(request.params.arguments); + const pipeline = await retryPipeline(project_id, pipeline_id); + return { + content: [ + { + type: "text", + text: `Retried pipeline #${pipeline.id}. Status: ${pipeline.status}\nWeb URL: ${pipeline.web_url}`, + }, + ], + }; + } + + case "cancel_pipeline": { + const { project_id, pipeline_id } = CancelPipelineSchema.parse(request.params.arguments); + const pipeline = await cancelPipeline(project_id, pipeline_id); + return { + content: [ + { + type: "text", + text: `Canceled pipeline #${pipeline.id}. Status: ${pipeline.status}\nWeb URL: ${pipeline.web_url}`, + }, + ], + }; + } + case "list_merge_requests": { const args = ListMergeRequestsSchema.parse(request.params.arguments); const mergeRequests = await listMergeRequests(args.project_id, args); diff --git a/schemas.ts b/schemas.ts index c6aeee4..4450e01 100644 --- a/schemas.ts +++ b/schemas.ts @@ -157,6 +157,33 @@ export const ListPipelineJobsSchema = z.object({ per_page: z.number().optional().describe("Number of items per page (max 100)"), }); +// Schema for creating a new pipeline +export const CreatePipelineSchema = z.object({ + project_id: z.string().describe("Project ID or URL-encoded path"), + ref: z.string().describe("The branch or tag to run the pipeline on"), + variables: z + .array( + z.object({ + key: z.string().describe("The key of the variable"), + value: z.string().describe("The value of the variable"), + }) + ) + .optional() + .describe("An array of variables to use for the pipeline"), +}); + +// Schema for retrying a pipeline +export const RetryPipelineSchema = z.object({ + project_id: z.string().describe("Project ID or URL-encoded path"), + pipeline_id: z.number().describe("The ID of the pipeline to retry"), +}); + +// Schema for canceling a pipeline +export const CancelPipelineSchema = z.object({ + project_id: z.string().describe("Project ID or URL-encoded path"), + pipeline_id: z.number().describe("The ID of the pipeline to cancel"), +}); + // Schema for the input parameters for pipeline job operations export const GetPipelineJobOutputSchema = z.object({ project_id: z.string().describe("Project ID or URL-encoded path"), @@ -1281,6 +1308,9 @@ export type GitLabPipeline = z.infer; export type ListPipelinesOptions = z.infer; export type GetPipelineOptions = z.infer; export type ListPipelineJobsOptions = z.infer; +export type CreatePipelineOptions = z.infer; +export type RetryPipelineOptions = z.infer; +export type CancelPipelineOptions = z.infer; export type GitLabMilestones = z.infer; export type ListProjectMilestonesOptions = z.infer; export type GetProjectMilestoneOptions = z.infer; diff --git a/test/validate-api.js b/test/validate-api.js index 2becbc2..db5dab8 100755 --- a/test/validate-api.js +++ b/test/validate-api.js @@ -43,9 +43,15 @@ async function validateGitLabAPI() { url: `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(TEST_PROJECT_ID)}/repository/branches?per_page=1`, validate: data => Array.isArray(data), }, + { + name: "List pipelines", + url: `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(TEST_PROJECT_ID)}/pipelines?per_page=5`, + validate: data => Array.isArray(data), + }, ]; let allPassed = true; + let firstPipelineId = null; for (const test of tests) { try { @@ -65,6 +71,11 @@ async function validateGitLabAPI() { if (test.validate(data)) { console.log(`✅ ${test.name} - PASSED\n`); + + // If we found pipelines, save the first one for additional testing + if (test.name === "List pipelines" && data.length > 0) { + firstPipelineId = data[0].id; + } } else { console.log(`❌ ${test.name} - FAILED (invalid response format)\n`); allPassed = false; @@ -76,6 +87,53 @@ async function validateGitLabAPI() { } } + // Test pipeline-specific endpoints if we have a pipeline ID + if (firstPipelineId) { + console.log(`Found pipeline #${firstPipelineId}, testing pipeline-specific endpoints...\n`); + + const pipelineTests = [ + { + name: `Get pipeline #${firstPipelineId} details`, + url: `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(TEST_PROJECT_ID)}/pipelines/${firstPipelineId}`, + validate: data => data.id === firstPipelineId && data.status, + }, + { + name: `List pipeline #${firstPipelineId} jobs`, + url: `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(TEST_PROJECT_ID)}/pipelines/${firstPipelineId}/jobs`, + validate: data => Array.isArray(data), + }, + ]; + + for (const test of pipelineTests) { + try { + console.log(`Testing: ${test.name}`); + const response = await fetch(test.url, { + headers: { + Authorization: `Bearer ${GITLAB_TOKEN}`, + Accept: "application/json", + }, + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const data = await response.json(); + + if (test.validate(data)) { + console.log(`✅ ${test.name} - PASSED\n`); + } else { + console.log(`❌ ${test.name} - FAILED (invalid response format)\n`); + allPassed = false; + } + } catch (error) { + console.log(`❌ ${test.name} - FAILED`); + console.log(` Error: ${error.message}\n`); + allPassed = false; + } + } + } + if (allPassed) { console.log("✅ All API validation tests passed!"); } else { From de0b138d8002daf15d845c6360957c50d95a6288 Mon Sep 17 00:00:00 2001 From: simple Date: Fri, 30 May 2025 00:48:53 +0900 Subject: [PATCH 35/39] [feat/pipeline-support] feat: add USE_PIPELINE environment variable for conditional pipeline feature activation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✨ Breaking Changes: - Pipeline features are now opt-in via USE_PIPELINE environment variable 📝 Details: - Pipeline 관련 도구들을 USE_PIPELINE 환경 변수로 제어 가능하도록 변경 - USE_GITLAB_WIKI, USE_MILESTONE과 동일한 패턴으로 구현 - 기본값은 false로 설정되어 pipeline 기능은 명시적으로 활성화해야 함 - README에 USE_PIPELINE 환경 변수 설명 추가 --- README.md | 9 +++++++-- index.ts | 19 ++++++++++++++++++- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 17bba0a..43f3f32 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,8 @@ When using with the Claude App, you need to set up your API key and URLs directl "GITLAB_API_URL": "your_gitlab_api_url", "GITLAB_READ_ONLY_MODE": "false", "USE_GITLAB_WIKI": "false", // use wiki api? - "USE_MILESTONE": "false" // use milestone api? + "USE_MILESTONE": "false", // use milestone api? + "USE_PIPELINE": "false" // use pipeline api? } } } @@ -55,6 +56,8 @@ When using with the Claude App, you need to set up your API key and URLs directl "USE_GITLAB_WIKI", "-e", "USE_MILESTONE", + "-e", + "USE_PIPELINE", "iwakitakuma/gitlab-mcp" ], "env": { @@ -62,7 +65,8 @@ When using with the Claude App, you need to set up your API key and URLs directl "GITLAB_API_URL": "https://gitlab.com/api/v4", // Optional, for self-hosted GitLab "GITLAB_READ_ONLY_MODE": "false", "USE_GITLAB_WIKI": "true", - "USE_MILESTONE": "true" + "USE_MILESTONE": "true", + "USE_PIPELINE": "true" } } } @@ -82,6 +86,7 @@ $ sh scripts/image_push.sh docker_user_name - `GITLAB_READ_ONLY_MODE`: When set to 'true', restricts the server to only expose read-only operations. Useful for enhanced security or when write access is not needed. Also useful for using with Cursor and it's 40 tool limit. - `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. - `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. +- `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. ## Tools 🛠️ diff --git a/index.ts b/index.ts index 0e51645..5535dde 100644 --- a/index.ts +++ b/index.ts @@ -192,6 +192,7 @@ const GITLAB_PERSONAL_ACCESS_TOKEN = process.env.GITLAB_PERSONAL_ACCESS_TOKEN; const GITLAB_READ_ONLY_MODE = process.env.GITLAB_READ_ONLY_MODE === "true"; const USE_GITLAB_WIKI = process.env.USE_GITLAB_WIKI === "true"; const USE_MILESTONE = process.env.USE_MILESTONE === "true"; +const USE_PIPELINE = process.env.USE_PIPELINE === "true"; // Add proxy configuration const HTTP_PROXY = process.env.HTTP_PROXY; @@ -614,6 +615,18 @@ const milestoneToolNames = [ "get_milestone_burndown_events", ]; +// Define which tools are related to pipelines and can be toggled by USE_PIPELINE +const pipelineToolNames = [ + "list_pipelines", + "get_pipeline", + "list_pipeline_jobs", + "get_pipeline_job", + "get_pipeline_job_output", + "create_pipeline", + "retry_pipeline", + "cancel_pipeline", +]; + /** * Smart URL handling for GitLab API * @@ -2852,9 +2865,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { ? tools0 : tools0.filter(tool => !wikiToolNames.includes(tool.name)); // Toggle milestone tools by USE_MILESTONE flag - let tools = USE_MILESTONE + const tools2 = USE_MILESTONE ? tools1 : tools1.filter(tool => !milestoneToolNames.includes(tool.name)); + // Toggle pipeline tools by USE_PIPELINE flag + let tools = USE_PIPELINE + ? tools2 + : tools2.filter(tool => !pipelineToolNames.includes(tool.name)); // <<< START: Gemini 호환성을 위해 $schema 제거 >>> tools = tools.map(tool => { From 1e0bcb173d2a21349cc70661a6fe9b4135fd1702 Mon Sep 17 00:00:00 2001 From: simple Date: Fri, 30 May 2025 00:50:26 +0900 Subject: [PATCH 36/39] =?UTF-8?q?[feat/pipeline-support]=20chore:=20v1.0.5?= =?UTF-8?q?2=20=EB=B2=84=EC=A0=84=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b8441dd..867b515 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@zereight/mcp-gitlab", - "version": "1.0.51", + "version": "1.0.52", "description": "MCP server for using the GitLab API", "license": "MIT", "author": "zereight", From 74af27f995f4fd8d22bc787a74b994a452988c4a Mon Sep 17 00:00:00 2001 From: iwakitakuma33 Date: Fri, 30 May 2025 01:51:07 +0900 Subject: [PATCH 37/39] FEAT: ci push docker hub --- .github/workflows/docker-publish.yml | 38 ++++++++++++++++++++++++++++ .secrets | 3 +++ event.json | 6 +++++ 3 files changed, 47 insertions(+) create mode 100644 .github/workflows/docker-publish.yml create mode 100644 .secrets create mode 100644 event.json diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 0000000..777c295 --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,38 @@ +name: Docker Publish + +on: + release: + types: [published] + +jobs: + docker: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Extract metadata for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ secrets.DOCKERHUB_USERNAME }}/gitlab-mcp + tags: | + type=semver,pattern={{version}} + latest + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} diff --git a/.secrets b/.secrets new file mode 100644 index 0000000..dc5f5ec --- /dev/null +++ b/.secrets @@ -0,0 +1,3 @@ +DOCKERHUB_USERNAME=DOCKERHUB_USERNAME +DOCKERHUB_TOKEN=DOCKERHUB_TOKEN +GITHUB_TOKEN=DOCKERHUB_TOKEN diff --git a/event.json b/event.json new file mode 100644 index 0000000..85facde --- /dev/null +++ b/event.json @@ -0,0 +1,6 @@ +{ + "action": "published", + "release": { + "tag_name": "v1.0.52" + } +} From cb36c007cb215127c16e621ef5a0255c76a6cdbe Mon Sep 17 00:00:00 2001 From: Peter Xu Date: Thu, 29 May 2025 20:38:14 -0700 Subject: [PATCH 38/39] [main] fix: make old_line and new_line optional for image diff discussions Image files in GitLab MR discussions use x/y coordinates instead of line numbers. This fix allows proper handling of image diff comments. Co-authored-by: Peter Xu --- schemas.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/schemas.ts b/schemas.ts index 4450e01..67b5e1e 100644 --- a/schemas.ts +++ b/schemas.ts @@ -618,21 +618,21 @@ export const GitLabDiscussionNoteSchema = z.object({ old_path: z.string(), new_path: z.string(), position_type: z.enum(["text", "image", "file"]), - old_line: z.number().nullable(), - new_line: z.number().nullable(), + old_line: z.number().nullish(), // This is missing for image diffs + new_line: z.number().nullish(), // This is missing for image diffs line_range: z .object({ start: z.object({ line_code: z.string(), type: z.enum(["new", "old", "expanded"]), - old_line: z.number().nullable(), - new_line: z.number().nullable(), + old_line: z.number().nullish(), // This is missing for image diffs + new_line: z.number().nullish(), // This is missing for image diffs }), end: z.object({ line_code: z.string(), type: z.enum(["new", "old", "expanded"]), - old_line: z.number().nullable(), - new_line: z.number().nullable(), + old_line: z.number().nullish(), // This is missing for image diffs + new_line: z.number().nullish(), // This is missing for image diffs }), }) .nullable() From fcb71e293e8a0f7f803397582d2e5ff867febd2d Mon Sep 17 00:00:00 2001 From: simple Date: Fri, 30 May 2025 12:38:42 +0900 Subject: [PATCH 39/39] [main] chore: bump version to v1.0.53 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 867b515..90983f2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@zereight/mcp-gitlab", - "version": "1.0.52", + "version": "1.0.53", "description": "MCP server for using the GitLab API", "license": "MIT", "author": "zereight",