From 1927a23684aa24178b3d35510c2378d98971624e Mon Sep 17 00:00:00 2001 From: Admin Date: Tue, 18 Mar 2025 01:04:11 -0700 Subject: [PATCH] Add GitLab Issues API enhanced support --- index.ts | 179 +++++++++++++++++++++++++++++++++++++++++++++++++++++ schemas.ts | 65 ++++++++++++++++++- 2 files changed, 243 insertions(+), 1 deletion(-) diff --git a/index.ts b/index.ts index 32d3fec..d3b07e6 100644 --- a/index.ts +++ b/index.ts @@ -41,6 +41,10 @@ import { GetMergeRequestSchema, GetMergeRequestDiffsSchema, UpdateMergeRequestSchema, + ListIssuesSchema, + GetIssueSchema, + UpdateIssueSchema, + DeleteIssueSchema, type GitLabFork, type GitLabReference, type GitLabRepository, @@ -332,6 +336,127 @@ async function createIssue( return GitLabIssueSchema.parse(data); } +/** + * List issues in a GitLab project + * 프로젝트의 이슈 목록 조회 + * + * @param {string} projectId - The ID or URL-encoded path of the project + * @param {Object} options - Options for listing issues + * @returns {Promise} List of issues + */ +async function listIssues( + projectId: string, + options: Omit, "project_id"> = {} +): Promise { + const url = new URL( + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues` + ); + + // Add all query parameters + Object.entries(options).forEach(([key, value]) => { + if (value !== undefined) { + if (key === 'label_name' && 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(), { + headers: DEFAULT_HEADERS, + }); + + await handleGitLabError(response); + const data = await response.json(); + return z.array(GitLabIssueSchema).parse(data); +} + +/** + * Get a single issue from a GitLab project + * 단일 이슈 조회 + * + * @param {string} projectId - The ID or URL-encoded path of the project + * @param {number} issueIid - The internal ID of the project issue + * @returns {Promise} The issue + */ +async function getIssue( + projectId: string, + issueIid: number +): Promise { + const url = new URL( + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues/${issueIid}` + ); + + const response = await fetch(url.toString(), { + headers: DEFAULT_HEADERS, + }); + + await handleGitLabError(response); + const data = await response.json(); + return GitLabIssueSchema.parse(data); +} + +/** + * Update an issue in a GitLab project + * 이슈 업데이트 + * + * @param {string} projectId - The ID or URL-encoded path of the project + * @param {number} issueIid - The internal ID of the project issue + * @param {Object} options - Update options for the issue + * @returns {Promise} The updated issue + */ +async function updateIssue( + projectId: string, + issueIid: number, + options: Omit, "project_id" | "issue_iid"> +): Promise { + const url = new URL( + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues/${issueIid}` + ); + + // Convert labels array to comma-separated string if present + const body: Record = { ...options }; + if (body.labels && Array.isArray(body.labels)) { + body.labels = body.labels.join(','); + } + + const response = await fetch(url.toString(), { + method: "PUT", + headers: DEFAULT_HEADERS, + body: JSON.stringify(body), + }); + + await handleGitLabError(response); + const data = await response.json(); + return GitLabIssueSchema.parse(data); +} + +/** + * Delete an issue from a GitLab project + * 이슈 삭제 + * + * @param {string} projectId - The ID or URL-encoded path of the project + * @param {number} issueIid - The internal ID of the project issue + * @returns {Promise} + */ +async function deleteIssue( + projectId: string, + issueIid: number +): Promise { + const url = new URL( + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues/${issueIid}` + ); + + const response = await fetch(url.toString(), { + method: "DELETE", + headers: DEFAULT_HEADERS, + }); + + await handleGitLabError(response); +} + /** * Create a new merge request in a GitLab project * 병합 요청 생성 @@ -882,6 +1007,26 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { description: "Create a new note (comment) to an issue or merge request", inputSchema: zodToJsonSchema(CreateNoteSchema), }, + { + name: "list_issues", + description: "List issues in a GitLab project with filtering options", + inputSchema: zodToJsonSchema(ListIssuesSchema), + }, + { + name: "get_issue", + description: "Get details of a specific issue in a GitLab project", + inputSchema: zodToJsonSchema(GetIssueSchema), + }, + { + name: "update_issue", + description: "Update an issue in a GitLab project", + inputSchema: zodToJsonSchema(UpdateIssueSchema), + }, + { + name: "delete_issue", + description: "Delete an issue from a GitLab project", + inputSchema: zodToJsonSchema(DeleteIssueSchema), + }, ], }; }); @@ -1068,6 +1213,40 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { }; } + case "list_issues": { + const args = ListIssuesSchema.parse(request.params.arguments); + const { project_id, ...options } = args; + const issues = await listIssues(project_id, options); + return { + content: [{ type: "text", text: JSON.stringify(issues, null, 2) }], + }; + } + + case "get_issue": { + const args = GetIssueSchema.parse(request.params.arguments); + const issue = await getIssue(args.project_id, args.issue_iid); + return { + content: [{ type: "text", text: JSON.stringify(issue, null, 2) }], + }; + } + + case "update_issue": { + const args = UpdateIssueSchema.parse(request.params.arguments); + const { project_id, issue_iid, ...options } = args; + const issue = await updateIssue(project_id, issue_iid, options); + return { + content: [{ type: "text", text: JSON.stringify(issue, null, 2) }], + }; + } + + case "delete_issue": { + const args = DeleteIssueSchema.parse(request.params.arguments); + await deleteIssue(args.project_id, args.issue_iid); + return { + content: [{ type: "text", text: JSON.stringify({ status: "success", message: "Issue deleted successfully" }, null, 2) }], + }; + } + default: throw new Error(`Unknown tool: ${request.params.name}`); } diff --git a/schemas.ts b/schemas.ts index 9874b8d..5209e8b 100644 --- a/schemas.ts +++ b/schemas.ts @@ -201,12 +201,75 @@ export const GitLabIssueSchema = z.object({ state: z.string(), author: GitLabUserSchema, assignees: z.array(GitLabUserSchema), - labels: z.array(GitLabLabelSchema), + labels: z.array(GitLabLabelSchema).or(z.array(z.string())), // Support both label objects and strings milestone: GitLabMilestoneSchema.nullable(), created_at: z.string(), updated_at: z.string(), closed_at: z.string().nullable(), web_url: z.string(), // Changed from html_url to match GitLab API + references: z.object({ + short: z.string(), + relative: z.string(), + full: z.string(), + }).optional(), + time_stats: z.object({ + time_estimate: z.number(), + total_time_spent: z.number(), + human_time_estimate: z.string().nullable(), + human_total_time_spent: z.string().nullable(), + }).optional(), + confidential: z.boolean().optional(), + due_date: z.string().nullable().optional(), + discussion_locked: z.boolean().optional(), + weight: z.number().nullable().optional(), +}); + +// 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"), + label_name: 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 issues from a specific scope"), + search: z.string().optional().describe("Search for specific terms"), + state: z.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"), + 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"), +}); + +export const UpdateIssueSchema = 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"), + 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)"), + 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)"), + weight: z.number().optional().describe("Weight of the issue (0-9)"), +}); + +export const DeleteIssueSchema = 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"), }); // Merge Request related schemas (equivalent to Pull Request)