From 48d33ae99011f4701b2e6a186f19c1f2b74ce20f Mon Sep 17 00:00:00 2001 From: Admin Date: Tue, 18 Mar 2025 01:07:12 -0700 Subject: [PATCH] Add GitLab Issue Links API support --- index.ts | 175 ++++++++++++++++++++++++++++++++++++++++++++++++++++- schemas.ts | 36 +++++++++++ 2 files changed, 210 insertions(+), 1 deletion(-) diff --git a/index.ts b/index.ts index d3b07e6..8317ae2 100644 --- a/index.ts +++ b/index.ts @@ -45,6 +45,12 @@ import { GetIssueSchema, UpdateIssueSchema, DeleteIssueSchema, + GitLabIssueLinkSchema, + ListIssueLinksSchema, + GetIssueLinkSchema, + CreateIssueLinkSchema, + DeleteIssueLinkSchema, + CreateNoteSchema, type GitLabFork, type GitLabReference, type GitLabRepository, @@ -57,7 +63,7 @@ import { type GitLabCommit, type FileOperation, type GitLabMergeRequestDiff, - CreateNoteSchema, + type GitLabIssueLink, } from "./schemas.js"; /** @@ -457,6 +463,121 @@ async function deleteIssue( await handleGitLabError(response); } +/** + * List all issue links for a specific issue + * 이슈 관계 목록 조회 + * + * @param {string} projectId - The ID or URL-encoded path of the project + * @param {number} issueIid - The internal ID of the project issue + * @returns {Promise} List of issue links + */ +async function listIssueLinks( + projectId: string, + issueIid: number +): Promise { + const url = new URL( + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues/${issueIid}/links` + ); + + const response = await fetch(url.toString(), { + headers: DEFAULT_HEADERS, + }); + + await handleGitLabError(response); + const data = await response.json(); + return z.array(GitLabIssueLinkSchema).parse(data); +} + +/** + * Get a specific issue link + * 특정 이슈 관계 조회 + * + * @param {string} projectId - The ID or URL-encoded path of the project + * @param {number} issueIid - The internal ID of the project issue + * @param {number} issueLinkId - The ID of the issue link + * @returns {Promise} The issue link + */ +async function getIssueLink( + projectId: string, + issueIid: number, + issueLinkId: number +): Promise { + const url = new URL( + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues/${issueIid}/links/${issueLinkId}` + ); + + const response = await fetch(url.toString(), { + headers: DEFAULT_HEADERS, + }); + + await handleGitLabError(response); + const data = await response.json(); + return GitLabIssueLinkSchema.parse(data); +} + +/** + * Create an issue link between two issues + * 이슈 관계 생성 + * + * @param {string} projectId - The ID or URL-encoded path of the project + * @param {number} issueIid - The internal ID of the project issue + * @param {string} targetProjectId - The ID or URL-encoded path of the target project + * @param {number} targetIssueIid - The internal ID of the target project issue + * @param {string} linkType - The type of the relation (relates_to, blocks, is_blocked_by) + * @returns {Promise} The created issue link + */ +async function createIssueLink( + projectId: string, + issueIid: number, + targetProjectId: string, + targetIssueIid: number, + linkType: 'relates_to' | 'blocks' | 'is_blocked_by' = 'relates_to' +): Promise { + const url = new URL( + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues/${issueIid}/links` + ); + + const response = await fetch(url.toString(), { + method: "POST", + headers: DEFAULT_HEADERS, + body: JSON.stringify({ + target_project_id: targetProjectId, + target_issue_iid: targetIssueIid, + link_type: linkType + }), + }); + + await handleGitLabError(response); + const data = await response.json(); + return GitLabIssueLinkSchema.parse(data); +} + +/** + * Delete an issue link + * 이슈 관계 삭제 + * + * @param {string} projectId - The ID or URL-encoded path of the project + * @param {number} issueIid - The internal ID of the project issue + * @param {number} issueLinkId - The ID of the issue link + * @returns {Promise} + */ +async function deleteIssueLink( + projectId: string, + issueIid: number, + issueLinkId: number +): Promise { + const url = new URL( + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues/${issueIid}/links/${issueLinkId}` + ); + + const response = await fetch(url.toString(), { + method: "DELETE", + headers: DEFAULT_HEADERS, + }); + + await handleGitLabError(response); +} + /** * Create a new merge request in a GitLab project * 병합 요청 생성 @@ -1027,6 +1148,26 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { description: "Delete an issue from a GitLab project", inputSchema: zodToJsonSchema(DeleteIssueSchema), }, + { + name: "list_issue_links", + description: "List all issue links for a specific issue", + inputSchema: zodToJsonSchema(ListIssueLinksSchema), + }, + { + name: "get_issue_link", + description: "Get a specific issue link", + inputSchema: zodToJsonSchema(GetIssueLinkSchema), + }, + { + name: "create_issue_link", + description: "Create an issue link between two issues", + inputSchema: zodToJsonSchema(CreateIssueLinkSchema), + }, + { + name: "delete_issue_link", + description: "Delete an issue link", + inputSchema: zodToJsonSchema(DeleteIssueLinkSchema), + }, ], }; }); @@ -1247,6 +1388,38 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { }; } + case "list_issue_links": { + const args = ListIssueLinksSchema.parse(request.params.arguments); + const links = await listIssueLinks(args.project_id, args.issue_iid); + return { + content: [{ type: "text", text: JSON.stringify(links, 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); + return { + content: [{ type: "text", text: JSON.stringify(link, null, 2) }], + }; + } + + case "create_issue_link": { + const args = CreateIssueLinkSchema.parse(request.params.arguments); + const link = await createIssueLink(args.project_id, args.issue_iid, args.target_project_id, args.target_issue_iid, args.link_type); + return { + content: [{ type: "text", text: JSON.stringify(link, null, 2) }], + }; + } + + case "delete_issue_link": { + const args = DeleteIssueLinkSchema.parse(request.params.arguments); + await deleteIssueLink(args.project_id, args.issue_iid, args.issue_link_id); + return { + content: [{ type: "text", text: JSON.stringify({ status: "success", message: "Issue link deleted successfully" }, null, 2) }], + }; + } + default: throw new Error(`Unknown tool: ${request.params.name}`); } diff --git a/schemas.ts b/schemas.ts index 5209e8b..cf576f4 100644 --- a/schemas.ts +++ b/schemas.ts @@ -470,6 +470,41 @@ export const CreateNoteSchema = z.object({ body: z.string().describe("Note content"), }); +// Issue links related schemas +export const GitLabIssueLinkSchema = z.object({ + id: z.number(), + link_type: z.enum(['relates_to', 'blocks', 'is_blocked_by']), + source_issue: GitLabIssueSchema, + target_issue: GitLabIssueSchema, + link_created_at: z.string().optional(), + link_updated_at: z.string().optional(), +}); + +export const ListIssueLinksSchema = 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"), +}); + +export const GetIssueLinkSchema = 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"), + issue_link_id: z.number().describe("ID of an issue relationship"), +}); + +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"), + link_type: z.enum(['relates_to', 'blocks', 'is_blocked_by']).optional().describe("The type of the relation, defaults to relates_to"), +}); + +export const DeleteIssueLinkSchema = 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"), + issue_link_id: z.number().describe("The ID of an issue relationship"), +}); + // Export types export type GitLabAuthor = z.infer; export type GitLabFork = z.infer; @@ -501,3 +536,4 @@ export type GitLabMergeRequestDiff = z.infer< typeof GitLabMergeRequestDiffSchema >; export type CreateNoteOptions = z.infer; +export type GitLabIssueLink = z.infer;