Add GitLab Issues API enhanced support
This commit is contained in:
179
index.ts
179
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<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}`);
|
||||
}
|
||||
|
65
schemas.ts
65
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)
|
||||
|
Reference in New Issue
Block a user