Переглянути джерело

feat: implement Gogs label management

This commit implements comprehensive label management functionality for the Gogs MCP server, addressing issue #3.

## Changes Made:

### Core Implementation (src/gogs-client.ts):
- Added 10 new label management methods to GogsClient class:
  - listLabels: List all labels in a repository
  - getLabel: Get a specific label by ID
  - createLabel: Create a new label with name and color
  - updateLabel: Update an existing label's name or color
  - deleteLabel: Delete a label from a repository
  - listIssueLabels: List labels on a specific issue
  - addIssueLabels: Add multiple labels to an issue
  - removeIssueLabel: Remove a specific label from an issue
  - replaceIssueLabels: Replace all labels on an issue
  - removeAllIssueLabels: Remove all labels from an issue

### MCP Server Tools (src/server.ts):
- Added 10 new Zod schemas for label tool validation
- Registered 10 new MCP tools for label management
- Implemented handlers for all label operations
- Tools follow consistent naming convention: snake_case (e.g., list_labels, create_label)

### Documentation (README.md):
- Added Label Management section to Features list
- Added comprehensive documentation for all 10 label management tools
- Included parameter descriptions and requirements for each tool

### Usage Examples (USAGE_EXAMPLES.md):
- Added Label Management section with practical examples
- Provided examples for all 10 label operations
- Demonstrated natural language usage patterns

## API Endpoints Implemented:
- GET /repos/:owner/:repo/labels - List repository labels
- GET /repos/:owner/:repo/labels/:id - Get single label
- POST /repos/:owner/:repo/labels - Create label
- PATCH /repos/:owner/:repo/labels/:id - Update label
- DELETE /repos/:owner/:repo/labels/:id - Delete label
- GET /repos/:owner/:repo/issues/:number/labels - List issue labels
- POST /repos/:owner/:repo/issues/:number/labels - Add labels to issue
- DELETE /repos/:owner/:repo/issues/:number/labels/:id - Remove label from issue
- PUT /repos/:owner/:repo/issues/:number/labels - Replace issue labels
- DELETE /repos/:owner/:repo/issues/:number/labels - Remove all issue labels

## Testing:
- Project builds successfully with TypeScript compiler
- All new methods properly compiled to JavaScript
- No compilation errors or type issues

Resolves: #3

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

Co-Authored-By: Claude <noreply@anthropic.com>
claude 9 місяців тому
батько
коміт
72483e25bb
4 змінених файлів з 706 додано та 0 видалено
  1. 79 0
      README.md
  2. 60 0
      USAGE_EXAMPLES.md
  3. 126 0
      src/gogs-client.ts
  4. 441 0
      src/server.ts

+ 79 - 0
README.md

@@ -33,6 +33,18 @@ This MCP server provides tools to:
   - Add comments to issues
   - Edit existing comments
 
+- **Label Management**
+  - List all labels in a repository
+  - Get specific label details
+  - Create new labels
+  - Update existing labels
+  - Delete labels
+  - List labels on an issue
+  - Add labels to an issue
+  - Remove labels from an issue
+  - Replace all labels on an issue
+  - Remove all labels from an issue
+
 ## Installation
 
 ### Prerequisites
@@ -396,6 +408,73 @@ Edit an existing comment on an issue.
 - `commentId` (number, required): Comment ID
 - `body` (string, required): Updated comment text
 
+### Label Management Tools
+
+#### `list_labels`
+List all labels in a repository.
+- `owner` (string, required): Repository owner username
+- `repo` (string, required): Repository name
+
+#### `get_label`
+Get a specific label by ID.
+- `owner` (string, required): Repository owner username
+- `repo` (string, required): Repository name
+- `labelId` (number, required): Label ID
+
+#### `create_label`
+Create a new label in a repository.
+- `owner` (string, required): Repository owner username
+- `repo` (string, required): Repository name
+- `name` (string, required): Label name
+- `color` (string, required): 7-character hex color code with leading # (e.g., #ff0000)
+
+#### `update_label`
+Update an existing label.
+- `owner` (string, required): Repository owner username
+- `repo` (string, required): Repository name
+- `labelId` (number, required): Label ID
+- `name` (string, optional): Label name
+- `color` (string, optional): 7-character hex color code with leading # (e.g., #ff0000)
+
+#### `delete_label`
+Delete a label from a repository.
+- `owner` (string, required): Repository owner username
+- `repo` (string, required): Repository name
+- `labelId` (number, required): Label ID
+
+#### `list_issue_labels`
+List all labels on a specific issue.
+- `owner` (string, required): Repository owner username
+- `repo` (string, required): Repository name
+- `number` (number, required): Issue number
+
+#### `add_issue_labels`
+Add labels to an issue.
+- `owner` (string, required): Repository owner username
+- `repo` (string, required): Repository name
+- `number` (number, required): Issue number
+- `labels` (array of numbers, required): Array of label IDs to add
+
+#### `remove_issue_label`
+Remove a specific label from an issue.
+- `owner` (string, required): Repository owner username
+- `repo` (string, required): Repository name
+- `number` (number, required): Issue number
+- `labelId` (number, required): Label ID to remove
+
+#### `replace_issue_labels`
+Replace all labels on an issue with a new set.
+- `owner` (string, required): Repository owner username
+- `repo` (string, required): Repository name
+- `number` (number, required): Issue number
+- `labels` (array of numbers, required): Array of label IDs to set (replaces all existing labels)
+
+#### `remove_all_issue_labels`
+Remove all labels from an issue.
+- `owner` (string, required): Repository owner username
+- `repo` (string, required): Repository name
+- `number` (number, required): Issue number
+
 ## Development
 
 ### Run in Development Mode

+ 60 - 0
USAGE_EXAMPLES.md

@@ -249,6 +249,66 @@ console.log(data); // {"status":"ok",...}
 
 **Claude** will use the `update_issue` tool with `state: "closed"` to close the issue.
 
+### Label Management
+
+**You**: "List all labels in owner/repo"
+
+**Claude** will use the `list_labels` tool to fetch all repository labels.
+
+---
+
+**You**: "Create a new label called 'bug' with red color (#ff0000) in owner/repo"
+
+**Claude** will use the `create_label` tool to create the label.
+
+---
+
+**You**: "Get details about label ID 3 in owner/repo"
+
+**Claude** will use the `get_label` tool to fetch the label information.
+
+---
+
+**You**: "Update label ID 3 in owner/repo to change its name to 'critical-bug' and color to #cc0000"
+
+**Claude** will use the `update_label` tool to modify the label.
+
+---
+
+**You**: "Delete label ID 5 from owner/repo"
+
+**Claude** will use the `delete_label` tool to remove the label.
+
+---
+
+**You**: "What labels are on issue #7 in owner/repo?"
+
+**Claude** will use the `list_issue_labels` tool to fetch labels on the specific issue.
+
+---
+
+**You**: "Add labels 3 and 5 to issue #7 in owner/repo"
+
+**Claude** will use the `add_issue_labels` tool to add the specified labels to the issue.
+
+---
+
+**You**: "Remove label ID 3 from issue #7 in owner/repo"
+
+**Claude** will use the `remove_issue_label` tool to remove that specific label.
+
+---
+
+**You**: "Replace all labels on issue #7 with labels 4 and 6"
+
+**Claude** will use the `replace_issue_labels` tool to set exactly those labels on the issue.
+
+---
+
+**You**: "Remove all labels from issue #7 in owner/repo"
+
+**Claude** will use the `remove_all_issue_labels` tool to clear all labels from the issue.
+
 ## Complex Workflows
 
 ### Code Review Workflow

+ 126 - 0
src/gogs-client.ts

@@ -14,6 +14,7 @@ import type {
   GogsConfig,
   GogsIssue,
   GogsIssueComment,
+  GogsLabel,
 } from './types.js';
 
 export class GogsClient {
@@ -300,4 +301,129 @@ export class GogsClient {
     );
     return response.data;
   }
+
+  /**
+   * List all labels in a repository
+   */
+  async listLabels(owner: string, repo: string): Promise<GogsLabel[]> {
+    const response = await this.client.get<GogsLabel[]>(`/repos/${owner}/${repo}/labels`);
+    return response.data;
+  }
+
+  /**
+   * Get a specific label by ID
+   */
+  async getLabel(owner: string, repo: string, labelId: number): Promise<GogsLabel> {
+    const response = await this.client.get<GogsLabel>(`/repos/${owner}/${repo}/labels/${labelId}`);
+    return response.data;
+  }
+
+  /**
+   * Create a new label
+   */
+  async createLabel(
+    owner: string,
+    repo: string,
+    data: {
+      name: string;
+      color: string;
+    }
+  ): Promise<GogsLabel> {
+    const response = await this.client.post<GogsLabel>(
+      `/repos/${owner}/${repo}/labels`,
+      data
+    );
+    return response.data;
+  }
+
+  /**
+   * Update an existing label
+   */
+  async updateLabel(
+    owner: string,
+    repo: string,
+    labelId: number,
+    data: {
+      name?: string;
+      color?: string;
+    }
+  ): Promise<GogsLabel> {
+    const response = await this.client.patch<GogsLabel>(
+      `/repos/${owner}/${repo}/labels/${labelId}`,
+      data
+    );
+    return response.data;
+  }
+
+  /**
+   * Delete a label
+   */
+  async deleteLabel(owner: string, repo: string, labelId: number): Promise<void> {
+    await this.client.delete(`/repos/${owner}/${repo}/labels/${labelId}`);
+  }
+
+  /**
+   * List labels on a specific issue
+   */
+  async listIssueLabels(owner: string, repo: string, issueNumber: number): Promise<GogsLabel[]> {
+    const response = await this.client.get<GogsLabel[]>(
+      `/repos/${owner}/${repo}/issues/${issueNumber}/labels`
+    );
+    return response.data;
+  }
+
+  /**
+   * Add labels to an issue
+   */
+  async addIssueLabels(
+    owner: string,
+    repo: string,
+    issueNumber: number,
+    labelIds: number[]
+  ): Promise<GogsLabel[]> {
+    const response = await this.client.post<GogsLabel[]>(
+      `/repos/${owner}/${repo}/issues/${issueNumber}/labels`,
+      { labels: labelIds }
+    );
+    return response.data;
+  }
+
+  /**
+   * Remove a label from an issue
+   */
+  async removeIssueLabel(
+    owner: string,
+    repo: string,
+    issueNumber: number,
+    labelId: number
+  ): Promise<void> {
+    await this.client.delete(`/repos/${owner}/${repo}/issues/${issueNumber}/labels/${labelId}`);
+  }
+
+  /**
+   * Replace all labels on an issue
+   */
+  async replaceIssueLabels(
+    owner: string,
+    repo: string,
+    issueNumber: number,
+    labelIds: number[]
+  ): Promise<GogsLabel[]> {
+    const response = await this.client.put<GogsLabel[]>(
+      `/repos/${owner}/${repo}/issues/${issueNumber}/labels`,
+      { labels: labelIds }
+    );
+    return response.data;
+  }
+
+  /**
+   * Remove all labels from an issue
+   */
+  async removeAllIssueLabels(
+    owner: string,
+    repo: string,
+    issueNumber: number
+  ): Promise<void> {
+    await this.client.delete(`/repos/${owner}/${repo}/issues/${issueNumber}/labels`);
+  }
 }

+ 441 - 0
src/server.ts

@@ -137,6 +137,71 @@ const UpdateIssueCommentSchema = z.object({
   body: z.string().describe('Updated comment text'),
 });
 
+const ListLabelsSchema = z.object({
+  owner: z.string().describe('Repository owner username'),
+  repo: z.string().describe('Repository name'),
+});
+
+const GetLabelSchema = z.object({
+  owner: z.string().describe('Repository owner username'),
+  repo: z.string().describe('Repository name'),
+  labelId: z.number().describe('Label ID'),
+});
+
+const CreateLabelSchema = z.object({
+  owner: z.string().describe('Repository owner username'),
+  repo: z.string().describe('Repository name'),
+  name: z.string().describe('Label name'),
+  color: z.string().describe('7-character hex color code with leading # (e.g., #ff0000)'),
+});
+
+const UpdateLabelSchema = z.object({
+  owner: z.string().describe('Repository owner username'),
+  repo: z.string().describe('Repository name'),
+  labelId: z.number().describe('Label ID'),
+  name: z.string().optional().describe('Label name'),
+  color: z.string().optional().describe('7-character hex color code with leading # (e.g., #ff0000)'),
+});
+
+const DeleteLabelSchema = z.object({
+  owner: z.string().describe('Repository owner username'),
+  repo: z.string().describe('Repository name'),
+  labelId: z.number().describe('Label ID'),
+});
+
+const ListIssueLabelsSchema = z.object({
+  owner: z.string().describe('Repository owner username'),
+  repo: z.string().describe('Repository name'),
+  number: z.number().describe('Issue number'),
+});
+
+const AddIssueLabelsSchema = z.object({
+  owner: z.string().describe('Repository owner username'),
+  repo: z.string().describe('Repository name'),
+  number: z.number().describe('Issue number'),
+  labels: z.array(z.number()).describe('Array of label IDs to add'),
+});
+
+const RemoveIssueLabelSchema = z.object({
+  owner: z.string().describe('Repository owner username'),
+  repo: z.string().describe('Repository name'),
+  number: z.number().describe('Issue number'),
+  labelId: z.number().describe('Label ID to remove'),
+});
+
+const ReplaceIssueLabelsSchema = z.object({
+  owner: z.string().describe('Repository owner username'),
+  repo: z.string().describe('Repository name'),
+  number: z.number().describe('Issue number'),
+  labels: z.array(z.number()).describe('Array of label IDs to set (replaces all existing labels)'),
+});
+
+const RemoveAllIssueLabelsSchema = z.object({
+  owner: z.string().describe('Repository owner username'),
+  repo: z.string().describe('Repository name'),
+  number: z.number().describe('Issue number'),
+});
+
 /**
  * Create and configure an MCP server with all tools and handlers
  */
@@ -629,6 +694,252 @@ export function createMcpServer(gogsClient: GogsClient): Server {
             required: ['owner', 'repo', 'commentId', 'body'],
           },
         },
+        {
+          name: 'list_labels',
+          description: 'List all labels in a repository',
+          inputSchema: {
+            type: 'object',
+            properties: {
+              owner: {
+                type: 'string',
+                description: 'Repository owner username',
+              },
+              repo: {
+                type: 'string',
+                description: 'Repository name',
+              },
+            },
+            required: ['owner', 'repo'],
+          },
+        },
+        {
+          name: 'get_label',
+          description: 'Get a specific label by ID',
+          inputSchema: {
+            type: 'object',
+            properties: {
+              owner: {
+                type: 'string',
+                description: 'Repository owner username',
+              },
+              repo: {
+                type: 'string',
+                description: 'Repository name',
+              },
+              labelId: {
+                type: 'number',
+                description: 'Label ID',
+              },
+            },
+            required: ['owner', 'repo', 'labelId'],
+          },
+        },
+        {
+          name: 'create_label',
+          description: 'Create a new label in a repository',
+          inputSchema: {
+            type: 'object',
+            properties: {
+              owner: {
+                type: 'string',
+                description: 'Repository owner username',
+              },
+              repo: {
+                type: 'string',
+                description: 'Repository name',
+              },
+              name: {
+                type: 'string',
+                description: 'Label name',
+              },
+              color: {
+                type: 'string',
+                description: '7-character hex color code with leading # (e.g., #ff0000)',
+              },
+            },
+            required: ['owner', 'repo', 'name', 'color'],
+          },
+        },
+        {
+          name: 'update_label',
+          description: 'Update an existing label',
+          inputSchema: {
+            type: 'object',
+            properties: {
+              owner: {
+                type: 'string',
+                description: 'Repository owner username',
+              },
+              repo: {
+                type: 'string',
+                description: 'Repository name',
+              },
+              labelId: {
+                type: 'number',
+                description: 'Label ID',
+              },
+              name: {
+                type: 'string',
+                description: 'Label name',
+              },
+              color: {
+                type: 'string',
+                description: '7-character hex color code with leading # (e.g., #ff0000)',
+              },
+            },
+            required: ['owner', 'repo', 'labelId'],
+          },
+        },
+        {
+          name: 'delete_label',
+          description: 'Delete a label from a repository',
+          inputSchema: {
+            type: 'object',
+            properties: {
+              owner: {
+                type: 'string',
+                description: 'Repository owner username',
+              },
+              repo: {
+                type: 'string',
+                description: 'Repository name',
+              },
+              labelId: {
+                type: 'number',
+                description: 'Label ID',
+              },
+            },
+            required: ['owner', 'repo', 'labelId'],
+          },
+        },
+        {
+          name: 'list_issue_labels',
+          description: 'List all labels on a specific 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: 'add_issue_labels',
+          description: 'Add labels 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',
+              },
+              labels: {
+                type: 'array',
+                items: {
+                  type: 'number',
+                },
+                description: 'Array of label IDs to add',
+              },
+            },
+            required: ['owner', 'repo', 'number', 'labels'],
+          },
+        },
+        {
+          name: 'remove_issue_label',
+          description: 'Remove a specific label from 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',
+              },
+              labelId: {
+                type: 'number',
+                description: 'Label ID to remove',
+              },
+            },
+            required: ['owner', 'repo', 'number', 'labelId'],
+          },
+        },
+        {
+          name: 'replace_issue_labels',
+          description: 'Replace all labels on an issue with a new set',
+          inputSchema: {
+            type: 'object',
+            properties: {
+              owner: {
+                type: 'string',
+                description: 'Repository owner username',
+              },
+              repo: {
+                type: 'string',
+                description: 'Repository name',
+              },
+              number: {
+                type: 'number',
+                description: 'Issue number',
+              },
+              labels: {
+                type: 'array',
+                items: {
+                  type: 'number',
+                },
+                description: 'Array of label IDs to set (replaces all existing labels)',
+              },
+            },
+            required: ['owner', 'repo', 'number', 'labels'],
+          },
+        },
+        {
+          name: 'remove_all_issue_labels',
+          description: 'Remove all labels from 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'],
+          },
+        },
       ],
     };
   });
@@ -914,6 +1225,136 @@ export function createMcpServer(gogsClient: GogsClient): Server {
           };
         }
 
+        case 'list_labels': {
+          const { owner, repo } = ListLabelsSchema.parse(args);
+          const labels = await gogsClient.listLabels(owner, repo);
+          return {
+            content: [
+              {
+                type: 'text',
+                text: JSON.stringify(labels, null, 2),
+              },
+            ],
+          };
+        }
+
+        case 'get_label': {
+          const { owner, repo, labelId } = GetLabelSchema.parse(args);
+          const label = await gogsClient.getLabel(owner, repo, labelId);
+          return {
+            content: [
+              {
+                type: 'text',
+                text: JSON.stringify(label, null, 2),
+              },
+            ],
+          };
+        }
+
+        case 'create_label': {
+          const { owner, repo, name, color } = CreateLabelSchema.parse(args);
+          const label = await gogsClient.createLabel(owner, repo, { name, color });
+          return {
+            content: [
+              {
+                type: 'text',
+                text: JSON.stringify(label, null, 2),
+              },
+            ],
+          };
+        }
+
+        case 'update_label': {
+          const { owner, repo, labelId, name, color } = UpdateLabelSchema.parse(args);
+          const label = await gogsClient.updateLabel(owner, repo, labelId, { name, color });
+          return {
+            content: [
+              {
+                type: 'text',
+                text: JSON.stringify(label, null, 2),
+              },
+            ],
+          };
+        }
+
+        case 'delete_label': {
+          const { owner, repo, labelId } = DeleteLabelSchema.parse(args);
+          await gogsClient.deleteLabel(owner, repo, labelId);
+          return {
+            content: [
+              {
+                type: 'text',
+                text: `Label ${labelId} deleted successfully from ${owner}/${repo}`,
+              },
+            ],
+          };
+        }
+
+        case 'list_issue_labels': {
+          const { owner, repo, number } = ListIssueLabelsSchema.parse(args);
+          const labels = await gogsClient.listIssueLabels(owner, repo, number);
+          return {
+            content: [
+              {
+                type: 'text',
+                text: JSON.stringify(labels, null, 2),
+              },
+            ],
+          };
+        }
+
+        case 'add_issue_labels': {
+          const { owner, repo, number, labels } = AddIssueLabelsSchema.parse(args);
+          const result = await gogsClient.addIssueLabels(owner, repo, number, labels);
+          return {
+            content: [
+              {
+                type: 'text',
+                text: JSON.stringify(result, null, 2),
+              },
+            ],
+          };
+        }
+
+        case 'remove_issue_label': {
+          const { owner, repo, number, labelId } = RemoveIssueLabelSchema.parse(args);
+          await gogsClient.removeIssueLabel(owner, repo, number, labelId);
+          return {
+            content: [
+              {
+                type: 'text',
+                text: `Label ${labelId} removed successfully from issue #${number}`,
+              },
+            ],
+          };
+        }
+
+        case 'replace_issue_labels': {
+          const { owner, repo, number, labels } = ReplaceIssueLabelsSchema.parse(args);
+          const result = await gogsClient.replaceIssueLabels(owner, repo, number, labels);
+          return {
+            content: [
+              {
+                type: 'text',
+                text: JSON.stringify(result, null, 2),
+              },
+            ],
+          };
+        }
+
+        case 'remove_all_issue_labels': {
+          const { owner, repo, number } = RemoveAllIssueLabelsSchema.parse(args);
+          await gogsClient.removeAllIssueLabels(owner, repo, number);
+          return {
+            content: [
+              {
+                type: 'text',
+                text: `All labels removed successfully from issue #${number}`,
+              },
+            ],
+          };
+        }
+
         default:
           throw new Error(`Unknown tool: ${name}`);
       }