Add GitLab Issues API enhanced support
This commit is contained in:
179
index.ts
179
index.ts
@ -41,6 +41,10 @@ import {
|
|||||||
GetMergeRequestSchema,
|
GetMergeRequestSchema,
|
||||||
GetMergeRequestDiffsSchema,
|
GetMergeRequestDiffsSchema,
|
||||||
UpdateMergeRequestSchema,
|
UpdateMergeRequestSchema,
|
||||||
|
ListIssuesSchema,
|
||||||
|
GetIssueSchema,
|
||||||
|
UpdateIssueSchema,
|
||||||
|
DeleteIssueSchema,
|
||||||
type GitLabFork,
|
type GitLabFork,
|
||||||
type GitLabReference,
|
type GitLabReference,
|
||||||
type GitLabRepository,
|
type GitLabRepository,
|
||||||
@ -332,6 +336,127 @@ async function createIssue(
|
|||||||
return GitLabIssueSchema.parse(data);
|
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
|
* 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",
|
description: "Create a new note (comment) to an issue or merge request",
|
||||||
inputSchema: zodToJsonSchema(CreateNoteSchema),
|
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:
|
default:
|
||||||
throw new Error(`Unknown tool: ${request.params.name}`);
|
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(),
|
state: z.string(),
|
||||||
author: GitLabUserSchema,
|
author: GitLabUserSchema,
|
||||||
assignees: z.array(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(),
|
milestone: GitLabMilestoneSchema.nullable(),
|
||||||
created_at: z.string(),
|
created_at: z.string(),
|
||||||
updated_at: z.string(),
|
updated_at: z.string(),
|
||||||
closed_at: z.string().nullable(),
|
closed_at: z.string().nullable(),
|
||||||
web_url: z.string(), // Changed from html_url to match GitLab API
|
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)
|
// Merge Request related schemas (equivalent to Pull Request)
|
||||||
|
Reference in New Issue
Block a user