Add GitLab Issues API enhanced support

This commit is contained in:
Admin
2025-03-18 01:04:11 -07:00
parent bf867b2fae
commit 1927a23684
2 changed files with 243 additions and 1 deletions

179
index.ts
View File

@ -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<GitLabIssue[]>} List of issues
*/
async function listIssues(
projectId: string,
options: Omit<z.infer<typeof ListIssuesSchema>, "project_id"> = {}
): Promise<GitLabIssue[]> {
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<GitLabIssue>} The issue
*/
async function getIssue(
projectId: string,
issueIid: number
): Promise<GitLabIssue> {
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<GitLabIssue>} The updated issue
*/
async function updateIssue(
projectId: string,
issueIid: number,
options: Omit<z.infer<typeof UpdateIssueSchema>, "project_id" | "issue_iid">
): Promise<GitLabIssue> {
const url = new URL(
`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues/${issueIid}`
);
// Convert labels array to comma-separated string if present
const body: Record<string, any> = { ...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<void>}
*/
async function deleteIssue(
projectId: string,
issueIid: number
): Promise<void> {
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}`);
}

View File

@ -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)