Skip to main content
Glama
issue.ts17.8 kB
/** * Issue Management Tools * * Issue 管理相关的 MCP 工具实现 */ import type { GiteaClient } from '../gitea-client.js'; import type { ContextManager } from '../context-manager.js'; import type { GiteaIssue, CreateIssueOptions, UpdateIssueOptions, IssueListOptions, GiteaComment, CreateCommentOptions, UpdateCommentOptions, CommentListOptions, } from '../types/gitea.js'; import { createLogger } from '../logger.js'; const logger = createLogger('tools:issue'); export interface IssueToolsContext { client: GiteaClient; contextManager: ContextManager; } /** * 创建 Issue */ export async function createIssue( ctx: IssueToolsContext, args: { owner?: string; repo?: string; title: string; body?: string; assignee?: string; assignees?: string[]; milestone?: number; labels?: number[]; due_date?: string; token?: string; } ) { logger.debug({ args }, 'Creating issue'); const { owner, repo } = ctx.contextManager.resolveOwnerRepo(args.owner, args.repo); const createOptions: CreateIssueOptions = { title: args.title, body: args.body, assignee: args.assignee, assignees: args.assignees, milestone: args.milestone, labels: args.labels, due_date: args.due_date, }; const issue = await ctx.client.post<GiteaIssue>( `/repos/${owner}/${repo}/issues`, createOptions, args.token ); logger.info({ owner, repo, issue: issue.number }, 'Issue created successfully'); return { success: true, issue: { id: issue.id, number: issue.number, title: issue.title, body: issue.body, state: issue.state, user: { id: issue.user.id, login: issue.user.login, }, labels: issue.labels.map((l) => ({ id: l.id, name: l.name, color: l.color })), assignees: issue.assignees?.map((a) => ({ id: a.id, login: a.login })), milestone: issue.milestone ? { id: issue.milestone.id, title: issue.milestone.title } : null, html_url: issue.html_url, created_at: issue.created_at, updated_at: issue.updated_at, }, }; } /** * 获取 Issue 详情 */ export async function getIssue( ctx: IssueToolsContext, args: { owner?: string; repo?: string; index: number; token?: string; } ) { logger.debug({ args }, 'Getting issue'); const { owner, repo } = ctx.contextManager.resolveOwnerRepo(args.owner, args.repo); const issue = await ctx.client.get<GiteaIssue>( `/repos/${owner}/${repo}/issues/${args.index}`, undefined, args.token ); logger.debug({ owner, repo, issue: issue.number }, 'Issue retrieved'); return { success: true, issue: { id: issue.id, number: issue.number, title: issue.title, body: issue.body, state: issue.state, user: { id: issue.user.id, login: issue.user.login, full_name: issue.user.full_name, }, labels: issue.labels.map((l) => ({ id: l.id, name: l.name, color: l.color, description: l.description, })), assignees: issue.assignees?.map((a) => ({ id: a.id, login: a.login, full_name: a.full_name, })), milestone: issue.milestone ? { id: issue.milestone.id, title: issue.milestone.title, state: issue.milestone.state, due_on: issue.milestone.due_on, } : null, comments: issue.comments, html_url: issue.html_url, created_at: issue.created_at, updated_at: issue.updated_at, closed_at: issue.closed_at, due_date: issue.due_date, }, }; } /** * 列出 Issues */ export async function listIssues( ctx: IssueToolsContext, args: { owner?: string; repo?: string; state?: 'open' | 'closed' | 'all'; labels?: string; q?: string; page?: number; limit?: number; milestones?: string; token?: string; } ) { logger.debug({ args }, 'Listing issues'); const { owner, repo } = ctx.contextManager.resolveOwnerRepo(args.owner, args.repo); const listOptions: IssueListOptions = { state: args.state || 'open', labels: args.labels, q: args.q, type: 'issues', page: args.page || 1, limit: args.limit || 30, milestones: args.milestones, }; const issues = await ctx.client.get<GiteaIssue[]>( `/repos/${owner}/${repo}/issues`, listOptions as any, args.token ); logger.debug({ count: issues.length }, 'Issues listed'); return { success: true, issues: issues.map((issue) => ({ id: issue.id, number: issue.number, title: issue.title, state: issue.state, user: { id: issue.user.id, login: issue.user.login, }, labels: issue.labels.map((l) => ({ id: l.id, name: l.name })), assignees: issue.assignees?.map((a) => ({ id: a.id, login: a.login })), milestone: issue.milestone ? { id: issue.milestone.id, title: issue.milestone.title } : null, comments: issue.comments, html_url: issue.html_url, created_at: issue.created_at, updated_at: issue.updated_at, })), pagination: { page: listOptions.page, limit: listOptions.limit, total: issues.length, }, }; } /** * 更新 Issue */ export async function updateIssue( ctx: IssueToolsContext, args: { owner?: string; repo?: string; index: number; title?: string; body?: string; assignee?: string; assignees?: string[]; milestone?: number; state?: 'open' | 'closed'; due_date?: string; unset_due_date?: boolean; token?: string; } ) { logger.debug({ args }, 'Updating issue'); const { owner, repo } = ctx.contextManager.resolveOwnerRepo(args.owner, args.repo); const updateOptions: UpdateIssueOptions = { title: args.title, body: args.body, assignee: args.assignee, assignees: args.assignees, milestone: args.milestone, state: args.state, due_date: args.due_date, unset_due_date: args.unset_due_date, }; const issue = await ctx.client.patch<GiteaIssue>( `/repos/${owner}/${repo}/issues/${args.index}`, updateOptions, args.token ); logger.info({ owner, repo, issue: issue.number }, 'Issue updated successfully'); return { success: true, issue: { id: issue.id, number: issue.number, title: issue.title, body: issue.body, state: issue.state, labels: issue.labels.map((l) => ({ id: l.id, name: l.name })), assignees: issue.assignees?.map((a) => ({ id: a.id, login: a.login })), html_url: issue.html_url, updated_at: issue.updated_at, }, }; } /** * 添加 Issue 评论 */ export async function commentIssue( ctx: IssueToolsContext, args: { owner?: string; repo?: string; index: number; body: string; token?: string; } ) { logger.debug({ args }, 'Adding comment to issue'); const { owner, repo } = ctx.contextManager.resolveOwnerRepo(args.owner, args.repo); const commentOptions: CreateCommentOptions = { body: args.body, }; const comment = await ctx.client.post<GiteaComment>( `/repos/${owner}/${repo}/issues/${args.index}/comments`, commentOptions, args.token ); logger.info({ owner, repo, issue: args.index }, 'Comment added successfully'); return { success: true, comment: { id: comment.id, body: comment.body, user: { id: comment.user.id, login: comment.user.login, }, html_url: comment.html_url, created_at: comment.created_at, updated_at: comment.updated_at, }, }; } /** * 关闭 Issue */ export async function closeIssue( ctx: IssueToolsContext, args: { owner?: string; repo?: string; index: number; token?: string; } ) { logger.debug({ args }, 'Closing issue'); const { owner, repo } = ctx.contextManager.resolveOwnerRepo(args.owner, args.repo); const issue = await ctx.client.patch<GiteaIssue>( `/repos/${owner}/${repo}/issues/${args.index}`, { state: 'closed' }, args.token ); logger.info({ owner, repo, issue: issue.number }, 'Issue closed successfully'); return { success: true, message: `Issue #${issue.number} has been closed`, issue: { id: issue.id, number: issue.number, title: issue.title, state: issue.state, html_url: issue.html_url, closed_at: issue.closed_at, }, }; } /** * 获取 Issue 依赖列表 */ export async function listIssueDependencies( ctx: IssueToolsContext, args: { owner?: string; repo?: string; index: number; page?: number; limit?: number; token?: string; } ) { logger.debug({ args }, 'Listing issue dependencies'); const { owner, repo } = ctx.contextManager.resolveOwnerRepo(args.owner, args.repo); const params: Record<string, any> = {}; if (args.page) params.page = args.page; if (args.limit) params.limit = args.limit; const dependencies = await ctx.client.get<GiteaIssue[]>( `/repos/${owner}/${repo}/issues/${args.index}/dependencies`, Object.keys(params).length > 0 ? params : undefined, args.token ); logger.debug({ count: dependencies.length }, 'Issue dependencies listed'); return { success: true, dependencies: dependencies.map((issue) => ({ id: issue.id, number: issue.number, title: issue.title, state: issue.state, user: { id: issue.user.id, login: issue.user.login, }, html_url: issue.html_url, created_at: issue.created_at, updated_at: issue.updated_at, })), pagination: { page: args.page || 1, limit: args.limit || 30, total: dependencies.length, }, }; } /** * 添加 Issue 依赖关系 * 使当前 Issue 依赖于指定的 Issue */ export async function addIssueDependency( ctx: IssueToolsContext, args: { owner?: string; repo?: string; index: number; dependencyIndex: number; token?: string; } ) { logger.debug({ args }, 'Adding issue dependency'); const { owner, repo } = ctx.contextManager.resolveOwnerRepo(args.owner, args.repo); // 添加依赖关系 // Gitea API 要求 IssueMeta 格式: { owner, repo, index } - 三个字段都是必需的 // index 是 Issue Number (如 #33),不是内部 ID // 即使是同仓库的依赖,也必须显式指定 owner 和 repo const result = await ctx.client.post<GiteaIssue>( `/repos/${owner}/${repo}/issues/${args.index}/dependencies`, { owner, repo, index: args.dependencyIndex }, args.token ); // 获取依赖 Issue 的详情用于返回 const dependencyIssue = await ctx.client.get<GiteaIssue>( `/repos/${owner}/${repo}/issues/${args.dependencyIndex}`, undefined, args.token ); logger.info( { owner, repo, issue: args.index, dependency: args.dependencyIndex }, 'Issue dependency added successfully' ); return { success: true, message: `Issue #${args.index} now depends on #${args.dependencyIndex}`, issue: { id: result.id, number: result.number, title: result.title, state: result.state, html_url: result.html_url, }, dependency: { id: dependencyIssue.id, number: dependencyIssue.number, title: dependencyIssue.title, state: dependencyIssue.state, html_url: dependencyIssue.html_url, }, }; } /** * 移除 Issue 依赖关系 */ export async function removeIssueDependency( ctx: IssueToolsContext, args: { owner?: string; repo?: string; index: number; dependencyIndex: number; token?: string; } ) { logger.debug({ args }, 'Removing issue dependency'); const { owner, repo } = ctx.contextManager.resolveOwnerRepo(args.owner, args.repo); // 移除依赖关系 // Gitea API 要求 IssueMeta 格式: { owner, repo, index } - 三个字段都是必需的 // index 是 Issue Number (如 #33),不是内部 ID // 即使是同仓库的依赖,也必须显式指定 owner 和 repo await ctx.client.delete( `/repos/${owner}/${repo}/issues/${args.index}/dependencies`, { owner, repo, index: args.dependencyIndex }, args.token ); logger.info( { owner, repo, issue: args.index, dependency: args.dependencyIndex }, 'Issue dependency removed successfully' ); return { success: true, message: `Dependency on #${args.dependencyIndex} removed from Issue #${args.index}`, }; } /** * 获取依赖当前 Issue 的其他 Issue 列表(被依赖列表) */ export async function listIssueBlocks( ctx: IssueToolsContext, args: { owner?: string; repo?: string; index: number; page?: number; limit?: number; token?: string; } ) { logger.debug({ args }, 'Listing issues that depend on this issue'); const { owner, repo } = ctx.contextManager.resolveOwnerRepo(args.owner, args.repo); const params: Record<string, any> = {}; if (args.page) params.page = args.page; if (args.limit) params.limit = args.limit; const blocks = await ctx.client.get<GiteaIssue[]>( `/repos/${owner}/${repo}/issues/${args.index}/blocks`, Object.keys(params).length > 0 ? params : undefined, args.token ); logger.debug({ count: blocks.length }, 'Issue blocks listed'); return { success: true, blocks: blocks.map((issue) => ({ id: issue.id, number: issue.number, title: issue.title, state: issue.state, html_url: issue.html_url, user: { id: issue.user.id, login: issue.user.login, }, created_at: issue.created_at, updated_at: issue.updated_at, })), count: blocks.length, message: `Found ${blocks.length} issue(s) that depend on #${args.index}`, }; } /** * 获取 Issue 评论列表 */ export async function listIssueComments( ctx: IssueToolsContext, args: { owner?: string; repo?: string; index: number; since?: string; before?: string; page?: number; limit?: number; token?: string; } ) { logger.debug({ args }, 'Listing issue comments'); const { owner, repo } = ctx.contextManager.resolveOwnerRepo(args.owner, args.repo); const params: CommentListOptions = {}; if (args.since) params.since = args.since; if (args.before) params.before = args.before; if (args.page) params.page = args.page; if (args.limit) params.limit = args.limit; const comments = await ctx.client.get<GiteaComment[]>( `/repos/${owner}/${repo}/issues/${args.index}/comments`, Object.keys(params).length > 0 ? (params as any) : undefined, args.token ); logger.debug({ count: comments.length }, 'Issue comments listed'); return { success: true, comments: comments.map((comment) => ({ id: comment.id, body: comment.body, user: { id: comment.user.id, login: comment.user.login, full_name: comment.user.full_name, }, html_url: comment.html_url, created_at: comment.created_at, updated_at: comment.updated_at, })), pagination: { page: args.page || 1, limit: args.limit || 30, total: comments.length, }, }; } /** * 获取单个评论详情 */ export async function getIssueComment( ctx: IssueToolsContext, args: { owner?: string; repo?: string; id: number; token?: string; } ) { logger.debug({ args }, 'Getting issue comment'); const { owner, repo } = ctx.contextManager.resolveOwnerRepo(args.owner, args.repo); const comment = await ctx.client.get<GiteaComment>( `/repos/${owner}/${repo}/issues/comments/${args.id}`, undefined, args.token ); logger.debug({ owner, repo, commentId: comment.id }, 'Issue comment retrieved'); return { success: true, comment: { id: comment.id, body: comment.body, user: { id: comment.user.id, login: comment.user.login, full_name: comment.user.full_name, }, html_url: comment.html_url, issue_url: comment.issue_url, created_at: comment.created_at, updated_at: comment.updated_at, }, }; } /** * 编辑评论 */ export async function editIssueComment( ctx: IssueToolsContext, args: { owner?: string; repo?: string; id: number; body: string; token?: string; } ) { logger.debug({ args }, 'Editing issue comment'); const { owner, repo } = ctx.contextManager.resolveOwnerRepo(args.owner, args.repo); const updateOptions: UpdateCommentOptions = { body: args.body, }; const comment = await ctx.client.patch<GiteaComment>( `/repos/${owner}/${repo}/issues/comments/${args.id}`, updateOptions, args.token ); logger.info({ owner, repo, commentId: comment.id }, 'Issue comment edited successfully'); return { success: true, comment: { id: comment.id, body: comment.body, user: { id: comment.user.id, login: comment.user.login, }, html_url: comment.html_url, updated_at: comment.updated_at, }, }; } /** * 删除评论 */ export async function deleteIssueComment( ctx: IssueToolsContext, args: { owner?: string; repo?: string; id: number; token?: string; } ) { logger.debug({ args }, 'Deleting issue comment'); const { owner, repo } = ctx.contextManager.resolveOwnerRepo(args.owner, args.repo); await ctx.client.delete( `/repos/${owner}/${repo}/issues/comments/${args.id}`, undefined, args.token ); logger.info({ owner, repo, commentId: args.id }, 'Issue comment deleted successfully'); return { success: true, message: `Comment #${args.id} has been deleted`, }; }

Implementation Reference

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/SupenBysz/gitea-mcp-tool'

If you have feedback or need assistance with the MCP directory API, please join our Discord server