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;