瀏覽代碼

Add issue (ticket) handling functionality to MCP server

Implemented comprehensive issue management tools including:
- list_issues: List issues in a repository with filtering options
- get_issue: Get details of a specific issue
- create_issue: Create new issues with title, body, assignees, etc.
- update_issue: Update existing issues including state changes
- list_issue_comments: List all comments on an issue
- create_issue_comment: Add comments to issues
- update_issue_comment: Edit existing comments

Added type definitions for GogsIssue, GogsIssueComment, GogsLabel,
and GogsMilestone to support full issue management capabilities.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Claude (AgentBox) 9 月之前
父節點
當前提交
6d94336784
共有 3 個文件被更改,包括 551 次插入0 次删除
  1. 124 0
      src/gogs-client.ts
  2. 383 0
      src/index.ts
  3. 44 0
      src/types.ts

+ 124 - 0
src/gogs-client.ts

@@ -12,6 +12,8 @@ import type {
   GogsBranch,
   GogsCommit,
   GogsConfig,
+  GogsIssue,
+  GogsIssueComment,
 } from './types.js';
 
 export class GogsClient {
@@ -176,4 +178,126 @@ export class GogsClient {
     });
     return response.data;
   }
+
+  /**
+   * List issues in a repository
+   */
+  async listIssues(
+    owner: string,
+    repo: string,
+    options?: {
+      state?: 'open' | 'closed' | 'all';
+      labels?: string;
+      page?: number;
+      per_page?: number;
+    }
+  ): Promise<GogsIssue[]> {
+    const response = await this.client.get<GogsIssue[]>(`/repos/${owner}/${repo}/issues`, {
+      params: {
+        state: options?.state || 'open',
+        labels: options?.labels,
+        page: options?.page || 1,
+        per_page: options?.per_page || 30,
+      },
+    });
+    return response.data;
+  }
+
+  /**
+   * Get a specific issue
+   */
+  async getIssue(owner: string, repo: string, number: number): Promise<GogsIssue> {
+    const response = await this.client.get<GogsIssue>(`/repos/${owner}/${repo}/issues/${number}`);
+    return response.data;
+  }
+
+  /**
+   * Create a new issue
+   */
+  async createIssue(
+    owner: string,
+    repo: string,
+    data: {
+      title: string;
+      body?: string;
+      assignee?: string;
+      milestone?: number;
+      labels?: number[];
+    }
+  ): Promise<GogsIssue> {
+    const response = await this.client.post<GogsIssue>(
+      `/repos/${owner}/${repo}/issues`,
+      data
+    );
+    return response.data;
+  }
+
+  /**
+   * Update an existing issue
+   */
+  async updateIssue(
+    owner: string,
+    repo: string,
+    number: number,
+    data: {
+      title?: string;
+      body?: string;
+      assignee?: string;
+      milestone?: number;
+      state?: 'open' | 'closed';
+      labels?: number[];
+    }
+  ): Promise<GogsIssue> {
+    const response = await this.client.patch<GogsIssue>(
+      `/repos/${owner}/${repo}/issues/${number}`,
+      data
+    );
+    return response.data;
+  }
+
+  /**
+   * List comments on an issue
+   */
+  async listIssueComments(
+    owner: string,
+    repo: string,
+    number: number
+  ): Promise<GogsIssueComment[]> {
+    const response = await this.client.get<GogsIssueComment[]>(
+      `/repos/${owner}/${repo}/issues/${number}/comments`
+    );
+    return response.data;
+  }
+
+  /**
+   * Create a comment on an issue
+   */
+  async createIssueComment(
+    owner: string,
+    repo: string,
+    number: number,
+    body: string
+  ): Promise<GogsIssueComment> {
+    const response = await this.client.post<GogsIssueComment>(
+      `/repos/${owner}/${repo}/issues/${number}/comments`,
+      { body }
+    );
+    return response.data;
+  }
+
+  /**
+   * Edit a comment on an issue
+   */
+  async updateIssueComment(
+    owner: string,
+    repo: string,
+    commentId: number,
+    body: string
+  ): Promise<GogsIssueComment> {
+    const response = await this.client.patch<GogsIssueComment>(
+      `/repos/${owner}/${repo}/issues/comments/${commentId}`,
+      { body }
+    );
+    return response.data;
+  }
 }

+ 383 - 0
src/index.ts

@@ -112,6 +112,63 @@ const GetCommitsSchema = z.object({
   page: z.number().optional().describe('Page number (default: 1)'),
 });
 
+const ListIssuesSchema = z.object({
+  owner: z.string().describe('Repository owner username'),
+  repo: z.string().describe('Repository name'),
+  state: z.enum(['open', 'closed', 'all']).optional().describe('Filter by state (default: open)'),
+  labels: z.string().optional().describe('Comma-separated list of label names'),
+  page: z.number().optional().describe('Page number (default: 1)'),
+  per_page: z.number().optional().describe('Results per page (default: 30)'),
+});
+
+const GetIssueSchema = z.object({
+  owner: z.string().describe('Repository owner username'),
+  repo: z.string().describe('Repository name'),
+  number: z.number().describe('Issue number'),
+});
+
+const CreateIssueSchema = z.object({
+  owner: z.string().describe('Repository owner username'),
+  repo: z.string().describe('Repository name'),
+  title: z.string().describe('Issue title'),
+  body: z.string().optional().describe('Issue description'),
+  assignee: z.string().optional().describe('Username to assign the issue to'),
+  milestone: z.number().optional().describe('Milestone ID'),
+  labels: z.array(z.number()).optional().describe('Array of label IDs'),
+});
+
+const UpdateIssueSchema = z.object({
+  owner: z.string().describe('Repository owner username'),
+  repo: z.string().describe('Repository name'),
+  number: z.number().describe('Issue number'),
+  title: z.string().optional().describe('Issue title'),
+  body: z.string().optional().describe('Issue description'),
+  assignee: z.string().optional().describe('Username to assign the issue to'),
+  milestone: z.number().optional().describe('Milestone ID'),
+  state: z.enum(['open', 'closed']).optional().describe('Issue state'),
+  labels: z.array(z.number()).optional().describe('Array of label IDs'),
+});
+
+const ListIssueCommentsSchema = z.object({
+  owner: z.string().describe('Repository owner username'),
+  repo: z.string().describe('Repository name'),
+  number: z.number().describe('Issue number'),
+});
+
+const CreateIssueCommentSchema = z.object({
+  owner: z.string().describe('Repository owner username'),
+  repo: z.string().describe('Repository name'),
+  number: z.number().describe('Issue number'),
+  body: z.string().describe('Comment text'),
+});
+
+const UpdateIssueCommentSchema = z.object({
+  owner: z.string().describe('Repository owner username'),
+  repo: z.string().describe('Repository name'),
+  commentId: z.number().describe('Comment ID'),
+  body: z.string().describe('Updated comment text'),
+});
+
 // Register tools
 server.setRequestHandler(ListToolsRequestSchema, async () => {
   return {
@@ -365,6 +422,228 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
           required: ['owner', 'repo'],
         },
       },
+      {
+        name: 'list_issues',
+        description: 'List issues in a repository',
+        inputSchema: {
+          type: 'object',
+          properties: {
+            owner: {
+              type: 'string',
+              description: 'Repository owner username',
+            },
+            repo: {
+              type: 'string',
+              description: 'Repository name',
+            },
+            state: {
+              type: 'string',
+              enum: ['open', 'closed', 'all'],
+              description: 'Filter by state (default: open)',
+            },
+            labels: {
+              type: 'string',
+              description: 'Comma-separated list of label names',
+            },
+            page: {
+              type: 'number',
+              description: 'Page number (default: 1)',
+            },
+            per_page: {
+              type: 'number',
+              description: 'Results per page (default: 30)',
+            },
+          },
+          required: ['owner', 'repo'],
+        },
+      },
+      {
+        name: 'get_issue',
+        description: 'Get a specific issue by number',
+        inputSchema: {
+          type: 'object',
+          properties: {
+            owner: {
+              type: 'string',
+              description: 'Repository owner username',
+            },
+            repo: {
+              type: 'string',
+              description: 'Repository name',
+            },
+            number: {
+              type: 'number',
+              description: 'Issue number',
+            },
+          },
+          required: ['owner', 'repo', 'number'],
+        },
+      },
+      {
+        name: 'create_issue',
+        description: 'Create a new issue in a repository',
+        inputSchema: {
+          type: 'object',
+          properties: {
+            owner: {
+              type: 'string',
+              description: 'Repository owner username',
+            },
+            repo: {
+              type: 'string',
+              description: 'Repository name',
+            },
+            title: {
+              type: 'string',
+              description: 'Issue title',
+            },
+            body: {
+              type: 'string',
+              description: 'Issue description',
+            },
+            assignee: {
+              type: 'string',
+              description: 'Username to assign the issue to',
+            },
+            milestone: {
+              type: 'number',
+              description: 'Milestone ID',
+            },
+            labels: {
+              type: 'array',
+              items: {
+                type: 'number',
+              },
+              description: 'Array of label IDs',
+            },
+          },
+          required: ['owner', 'repo', 'title'],
+        },
+      },
+      {
+        name: 'update_issue',
+        description: 'Update an existing issue (including closing or reopening)',
+        inputSchema: {
+          type: 'object',
+          properties: {
+            owner: {
+              type: 'string',
+              description: 'Repository owner username',
+            },
+            repo: {
+              type: 'string',
+              description: 'Repository name',
+            },
+            number: {
+              type: 'number',
+              description: 'Issue number',
+            },
+            title: {
+              type: 'string',
+              description: 'Issue title',
+            },
+            body: {
+              type: 'string',
+              description: 'Issue description',
+            },
+            assignee: {
+              type: 'string',
+              description: 'Username to assign the issue to',
+            },
+            milestone: {
+              type: 'number',
+              description: 'Milestone ID',
+            },
+            state: {
+              type: 'string',
+              enum: ['open', 'closed'],
+              description: 'Issue state',
+            },
+            labels: {
+              type: 'array',
+              items: {
+                type: 'number',
+              },
+              description: 'Array of label IDs',
+            },
+          },
+          required: ['owner', 'repo', 'number'],
+        },
+      },
+      {
+        name: 'list_issue_comments',
+        description: 'List all comments on an issue',
+        inputSchema: {
+          type: 'object',
+          properties: {
+            owner: {
+              type: 'string',
+              description: 'Repository owner username',
+            },
+            repo: {
+              type: 'string',
+              description: 'Repository name',
+            },
+            number: {
+              type: 'number',
+              description: 'Issue number',
+            },
+          },
+          required: ['owner', 'repo', 'number'],
+        },
+      },
+      {
+        name: 'create_issue_comment',
+        description: 'Add a comment to an issue',
+        inputSchema: {
+          type: 'object',
+          properties: {
+            owner: {
+              type: 'string',
+              description: 'Repository owner username',
+            },
+            repo: {
+              type: 'string',
+              description: 'Repository name',
+            },
+            number: {
+              type: 'number',
+              description: 'Issue number',
+            },
+            body: {
+              type: 'string',
+              description: 'Comment text',
+            },
+          },
+          required: ['owner', 'repo', 'number', 'body'],
+        },
+      },
+      {
+        name: 'update_issue_comment',
+        description: 'Edit an existing comment on an issue',
+        inputSchema: {
+          type: 'object',
+          properties: {
+            owner: {
+              type: 'string',
+              description: 'Repository owner username',
+            },
+            repo: {
+              type: 'string',
+              description: 'Repository name',
+            },
+            commentId: {
+              type: 'number',
+              description: 'Comment ID',
+            },
+            body: {
+              type: 'string',
+              description: 'Updated comment text',
+            },
+          },
+          required: ['owner', 'repo', 'commentId', 'body'],
+        },
+      },
     ],
   };
 });
@@ -546,6 +825,110 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
         };
       }
 
+      case 'list_issues': {
+        const { owner, repo, state, labels, page, per_page } = ListIssuesSchema.parse(args);
+        const issues = await gogsClient.listIssues(owner, repo, { state, labels, page, per_page });
+        return {
+          content: [
+            {
+              type: 'text',
+              text: JSON.stringify(issues, null, 2),
+            },
+          ],
+        };
+      }
+
+      case 'get_issue': {
+        const { owner, repo, number } = GetIssueSchema.parse(args);
+        const issue = await gogsClient.getIssue(owner, repo, number);
+        return {
+          content: [
+            {
+              type: 'text',
+              text: JSON.stringify(issue, null, 2),
+            },
+          ],
+        };
+      }
+
+      case 'create_issue': {
+        const { owner, repo, title, body, assignee, milestone, labels } = CreateIssueSchema.parse(args);
+        const issue = await gogsClient.createIssue(owner, repo, {
+          title,
+          body,
+          assignee,
+          milestone,
+          labels,
+        });
+        return {
+          content: [
+            {
+              type: 'text',
+              text: JSON.stringify(issue, null, 2),
+            },
+          ],
+        };
+      }
+
+      case 'update_issue': {
+        const { owner, repo, number, title, body, assignee, milestone, state, labels } = UpdateIssueSchema.parse(args);
+        const issue = await gogsClient.updateIssue(owner, repo, number, {
+          title,
+          body,
+          assignee,
+          milestone,
+          state,
+          labels,
+        });
+        return {
+          content: [
+            {
+              type: 'text',
+              text: JSON.stringify(issue, null, 2),
+            },
+          ],
+        };
+      }
+
+      case 'list_issue_comments': {
+        const { owner, repo, number } = ListIssueCommentsSchema.parse(args);
+        const comments = await gogsClient.listIssueComments(owner, repo, number);
+        return {
+          content: [
+            {
+              type: 'text',
+              text: JSON.stringify(comments, null, 2),
+            },
+          ],
+        };
+      }
+
+      case 'create_issue_comment': {
+        const { owner, repo, number, body } = CreateIssueCommentSchema.parse(args);
+        const comment = await gogsClient.createIssueComment(owner, repo, number, body);
+        return {
+          content: [
+            {
+              type: 'text',
+              text: JSON.stringify(comment, null, 2),
+            },
+          ],
+        };
+      }
+
+      case 'update_issue_comment': {
+        const { owner, repo, commentId, body } = UpdateIssueCommentSchema.parse(args);
+        const comment = await gogsClient.updateIssueComment(owner, repo, commentId, body);
+        return {
+          content: [
+            {
+              type: 'text',
+              text: JSON.stringify(comment, null, 2),
+            },
+          ],
+        };
+      }
+
       default:
         throw new Error(`Unknown tool: ${name}`);
     }

+ 44 - 0
src/types.ts

@@ -82,3 +82,47 @@ export interface GogsConfig {
   serverUrl: string;
   accessToken?: string;
 }
+
+export interface GogsIssue {
+  id: number;
+  number: number;
+  user: GogsUser;
+  title: string;
+  body: string;
+  state: 'open' | 'closed';
+  comments: number;
+  created_at: string;
+  updated_at: string;
+  labels?: GogsLabel[];
+  milestone?: GogsMilestone;
+  assignee?: GogsUser;
+  pull_request?: {
+    html_url: string;
+    diff_url: string;
+    patch_url: string;
+  };
+}
+
+export interface GogsLabel {
+  id: number;
+  name: string;
+  color: string;
+}
+
+export interface GogsMilestone {
+  id: number;
+  title: string;
+  description: string;
+  state: 'open' | 'closed';
+  open_issues: number;
+  closed_issues: number;
+  due_on?: string;
+}
+
+export interface GogsIssueComment {
+  id: number;
+  user: GogsUser;
+  body: string;
+  created_at: string;
+  updated_at: string;
+}