Forráskód Böngészése

feat: implement webhook management tools #7

- Add TypeScript types for webhooks (GogsWebhook, GogsWebhookEvent, GogsWebhookType, GogsWebhookConfig) in src/types.ts
- Implement webhook API methods in GogsClient: listHooks, createHook, updateHook, deleteHook
- Add MCP tools with Zod schemas: list_hooks, create_hook, update_hook, delete_hook
- Add tool handlers for all webhook operations
- Update README.md with comprehensive webhook tools documentation including examples
- Update CLAUDE.md with webhook management category
- Remove webhook endpoints from 'not implemented' table in README

Implements issue #7 - Webhooks support for CI/CD integration

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

Co-Authored-By: Claude <noreply@anthropic.com>
claude 9 hónapja
szülő
commit
3702cf0f80
5 módosított fájl, 461 hozzáadás és 4 törlés
  1. 1 0
      CLAUDE.md
  2. 72 4
      README.md
  3. 59 0
      src/gogs-client.ts
  4. 296 0
      src/server.ts
  5. 33 0
      src/types.ts

+ 1 - 0
CLAUDE.md

@@ -39,6 +39,7 @@ The server exposes tools in these categories:
 - **Release Management**: list_releases
 - **Collaborator Management**: list_collaborators, check_collaborator, add_collaborator, remove_collaborator
 - **Public Key Management**: list_user_keys, list_my_keys, get_public_key, create_public_key, delete_public_key
+- **Webhook Management**: list_hooks, create_hook, update_hook, delete_hook
 
 ## Development Commands
 

+ 72 - 4
README.md

@@ -703,6 +703,78 @@ Create a new public SSH key for the authenticated user. Returns the created key
 Delete a public SSH key by ID. Only the authenticated user can delete their own keys.
 - `keyId` (number, required): Public key ID to delete
 
+### Webhook Management Tools
+
+#### `list_hooks`
+List all webhooks configured for a repository. Returns webhook information including ID, type, events, active status, and configuration.
+- `owner` (string, required): Repository owner username
+- `repo` (string, required): Repository name
+
+**Example response:**
+```json
+[
+  {
+    "id": 2,
+    "type": "gogs",
+    "events": ["create", "delete", "fork", "push", "issues", "pull_request", "issue_comment", "release"],
+    "active": true,
+    "config": {
+      "content_type": "json",
+      "url": "http://example.com/webhook"
+    },
+    "updated_at": "2025-10-30T20:50:16Z",
+    "created_at": "2025-10-28T14:45:17Z"
+  }
+]
+```
+
+#### `create_hook`
+Create a new webhook for a repository. Supports both Gogs and Slack webhook types with customizable event triggers.
+- `owner` (string, required): Repository owner username
+- `repo` (string, required): Repository name
+- `type` (string, required): Webhook type - either "gogs" or "slack"
+- `config` (object, required): Webhook configuration
+  - `url` (string, required): URL to send webhook payloads to
+  - `content_type` (string, required): Content type for payload - either "json" or "form"
+  - `secret` (string, optional): Optional secret for webhook validation
+  - `channel` (string, optional): Slack channel (for slack webhooks)
+  - `username` (string, optional): Slack username (for slack webhooks)
+  - `icon_url` (string, optional): Slack icon URL (for slack webhooks)
+  - `color` (string, optional): Slack color (for slack webhooks)
+- `events` (array, optional): Array of events that trigger the webhook. Available events: "create", "delete", "fork", "push", "issues", "issue_comment", "pull_request", "release". Default: ["push"]
+- `active` (boolean, optional): Whether the webhook is active (default: false)
+
+**Example:**
+```json
+{
+  "owner": "myuser",
+  "repo": "myrepo",
+  "type": "gogs",
+  "config": {
+    "url": "https://example.com/webhook",
+    "content_type": "json",
+    "secret": "mysecret"
+  },
+  "events": ["push", "issues", "pull_request"],
+  "active": true
+}
+```
+
+#### `update_hook`
+Update an existing webhook's configuration, events, or active status.
+- `owner` (string, required): Repository owner username
+- `repo` (string, required): Repository name
+- `hookId` (number, required): Webhook ID to update
+- `config` (object, required): Webhook configuration (same structure as create_hook)
+- `events` (array, optional): Array of events that trigger the webhook
+- `active` (boolean, optional): Whether the webhook is active
+
+#### `delete_hook`
+Delete a webhook from a repository.
+- `owner` (string, required): Repository owner username
+- `repo` (string, required): Repository name
+- `hookId` (number, required): Webhook ID to delete
+
 ## Documented but Not Yet Implemented API Endpoints
 
 The following Gogs API v1 endpoints are officially documented but not yet implemented in this MCP server. These represent potential features for future development:
@@ -721,10 +793,6 @@ The following Gogs API v1 endpoints are officially documented but not yet implem
 | **Deploy Keys** | `/repos/:username/:reponame/keys/:id` | GET | Get a deploy key | [Link](https://github.com/gogs/docs-api/blob/master/Repositories/Deploy%20Keys.md) |
 | **Deploy Keys** | `/repos/:username/:reponame/keys` | POST | Add a new deploy key | [Link](https://github.com/gogs/docs-api/blob/master/Repositories/Deploy%20Keys.md) |
 | **Deploy Keys** | `/repos/:username/:reponame/keys/:id` | DELETE | Remove a deploy key | [Link](https://github.com/gogs/docs-api/blob/master/Repositories/Deploy%20Keys.md) |
-| **Webhooks** | `/repos/:username/:reponame/hooks` | GET | List hooks | [Link](https://github.com/gogs/docs-api/blob/master/Repositories/Webhooks.md) |
-| **Webhooks** | `/repos/:username/:reponame/hooks` | POST | Create a hook | [Link](https://github.com/gogs/docs-api/blob/master/Repositories/Webhooks.md) |
-| **Webhooks** | `/repos/:username/:reponame/hooks/:id` | PATCH | Edit a hook | [Link](https://github.com/gogs/docs-api/blob/master/Repositories/Webhooks.md) |
-| **Webhooks** | `/repos/:username/:reponame/hooks/:id` | DELETE | Delete a hook | [Link](https://github.com/gogs/docs-api/blob/master/Repositories/Webhooks.md) |
 
 ### Contributing New Endpoints
 

+ 59 - 0
src/gogs-client.ts

@@ -23,6 +23,10 @@ import type {
   GogsCollaborator,
   GogsEmail,
   GogsPublicKey,
+  GogsWebhook,
+  GogsWebhookEvent,
+  GogsWebhookType,
+  GogsWebhookConfig,
 } from './types.js';
 
 export class GogsClient {
@@ -737,4 +741,59 @@ export class GogsClient {
   async deletePublicKey(keyId: number): Promise<void> {
     await this.client.delete(`/user/keys/${keyId}`);
   }
+
+  /**
+   * List webhooks for a repository
+   */
+  async listHooks(owner: string, repo: string): Promise<GogsWebhook[]> {
+    const response = await this.client.get<GogsWebhook[]>(`/repos/${owner}/${repo}/hooks`);
+    return response.data;
+  }
+
+  /**
+   * Create a new webhook for a repository
+   */
+  async createHook(
+    owner: string,
+    repo: string,
+    data: {
+      type: GogsWebhookType;
+      config: GogsWebhookConfig;
+      events?: GogsWebhookEvent[];
+      active?: boolean;
+    }
+  ): Promise<GogsWebhook> {
+    const response = await this.client.post<GogsWebhook>(
+      `/repos/${owner}/${repo}/hooks`,
+      data
+    );
+    return response.data;
+  }
+
+  /**
+   * Update an existing webhook
+   */
+  async updateHook(
+    owner: string,
+    repo: string,
+    hookId: number,
+    data: {
+      config: GogsWebhookConfig;
+      events?: GogsWebhookEvent[];
+      active?: boolean;
+    }
+  ): Promise<GogsWebhook> {
+    const response = await this.client.patch<GogsWebhook>(
+      `/repos/${owner}/${repo}/hooks/${hookId}`,
+      data
+    );
+    return response.data;
+  }
+
+  /**
+   * Delete a webhook from a repository
+   */
+  async deleteHook(owner: string, repo: string, hookId: number): Promise<void> {
+    await this.client.delete(`/repos/${owner}/${repo}/hooks/${hookId}`);
+  }
 }

+ 296 - 0
src/server.ts

@@ -352,6 +352,55 @@ const DeletePublicKeySchema = z.object({
   keyId: z.number().describe('Public key ID to delete'),
 });
 
+const ListHooksSchema = z.object({
+  owner: z.string().describe('Repository owner username'),
+  repo: z.string().describe('Repository name'),
+});
+
+const CreateHookSchema = z.object({
+  owner: z.string().describe('Repository owner username'),
+  repo: z.string().describe('Repository name'),
+  type: z.enum(['gogs', 'slack']).describe('Webhook type (gogs or slack)'),
+  config: z.object({
+    url: z.string().describe('URL to send webhook payloads to'),
+    content_type: z.enum(['json', 'form']).describe('Content type for payload (json or form)'),
+    secret: z.string().optional().describe('Optional secret for webhook validation'),
+    channel: z.string().optional().describe('Slack channel (for slack webhooks)'),
+    username: z.string().optional().describe('Slack username (for slack webhooks)'),
+    icon_url: z.string().optional().describe('Slack icon URL (for slack webhooks)'),
+    color: z.string().optional().describe('Slack color (for slack webhooks)'),
+  }).describe('Webhook configuration'),
+  events: z.array(z.enum(['create', 'delete', 'fork', 'push', 'issues', 'issue_comment', 'pull_request', 'release']))
+    .optional()
+    .describe('Events that trigger the webhook (default: ["push"])'),
+  active: z.boolean().optional().describe('Whether the webhook is active (default: false)'),
+});
+
+const UpdateHookSchema = z.object({
+  owner: z.string().describe('Repository owner username'),
+  repo: z.string().describe('Repository name'),
+  hookId: z.number().describe('Webhook ID to update'),
+  config: z.object({
+    url: z.string().describe('URL to send webhook payloads to'),
+    content_type: z.enum(['json', 'form']).describe('Content type for payload (json or form)'),
+    secret: z.string().optional().describe('Optional secret for webhook validation'),
+    channel: z.string().optional().describe('Slack channel (for slack webhooks)'),
+    username: z.string().optional().describe('Slack username (for slack webhooks)'),
+    icon_url: z.string().optional().describe('Slack icon URL (for slack webhooks)'),
+    color: z.string().optional().describe('Slack color (for slack webhooks)'),
+  }).describe('Webhook configuration'),
+  events: z.array(z.enum(['create', 'delete', 'fork', 'push', 'issues', 'issue_comment', 'pull_request', 'release']))
+    .optional()
+    .describe('Events that trigger the webhook'),
+  active: z.boolean().optional().describe('Whether the webhook is active'),
+});
+
+const DeleteHookSchema = z.object({
+  owner: z.string().describe('Repository owner username'),
+  repo: z.string().describe('Repository name'),
+  hookId: z.number().describe('Webhook ID to delete'),
+});
+
 /**
  * Create and configure an MCP server with all tools and handlers
  */
@@ -1670,6 +1719,187 @@ export function createMcpServer(gogsClient: GogsClient): Server {
             required: ['keyId'],
           },
         },
+        {
+          name: 'list_hooks',
+          description: 'List all webhooks for a repository',
+          inputSchema: {
+            type: 'object',
+            properties: {
+              owner: {
+                type: 'string',
+                description: 'Repository owner username',
+              },
+              repo: {
+                type: 'string',
+                description: 'Repository name',
+              },
+            },
+            required: ['owner', 'repo'],
+          },
+        },
+        {
+          name: 'create_hook',
+          description: 'Create a new webhook for a repository',
+          inputSchema: {
+            type: 'object',
+            properties: {
+              owner: {
+                type: 'string',
+                description: 'Repository owner username',
+              },
+              repo: {
+                type: 'string',
+                description: 'Repository name',
+              },
+              type: {
+                type: 'string',
+                enum: ['gogs', 'slack'],
+                description: 'Webhook type (gogs or slack)',
+              },
+              config: {
+                type: 'object',
+                properties: {
+                  url: {
+                    type: 'string',
+                    description: 'URL to send webhook payloads to',
+                  },
+                  content_type: {
+                    type: 'string',
+                    enum: ['json', 'form'],
+                    description: 'Content type for payload (json or form)',
+                  },
+                  secret: {
+                    type: 'string',
+                    description: 'Optional secret for webhook validation',
+                  },
+                  channel: {
+                    type: 'string',
+                    description: 'Slack channel (for slack webhooks)',
+                  },
+                  username: {
+                    type: 'string',
+                    description: 'Slack username (for slack webhooks)',
+                  },
+                  icon_url: {
+                    type: 'string',
+                    description: 'Slack icon URL (for slack webhooks)',
+                  },
+                  color: {
+                    type: 'string',
+                    description: 'Slack color (for slack webhooks)',
+                  },
+                },
+                required: ['url', 'content_type'],
+                description: 'Webhook configuration',
+              },
+              events: {
+                type: 'array',
+                items: {
+                  type: 'string',
+                  enum: ['create', 'delete', 'fork', 'push', 'issues', 'issue_comment', 'pull_request', 'release'],
+                },
+                description: 'Events that trigger the webhook (default: ["push"])',
+              },
+              active: {
+                type: 'boolean',
+                description: 'Whether the webhook is active (default: false)',
+              },
+            },
+            required: ['owner', 'repo', 'type', 'config'],
+          },
+        },
+        {
+          name: 'update_hook',
+          description: 'Update an existing webhook',
+          inputSchema: {
+            type: 'object',
+            properties: {
+              owner: {
+                type: 'string',
+                description: 'Repository owner username',
+              },
+              repo: {
+                type: 'string',
+                description: 'Repository name',
+              },
+              hookId: {
+                type: 'number',
+                description: 'Webhook ID to update',
+              },
+              config: {
+                type: 'object',
+                properties: {
+                  url: {
+                    type: 'string',
+                    description: 'URL to send webhook payloads to',
+                  },
+                  content_type: {
+                    type: 'string',
+                    enum: ['json', 'form'],
+                    description: 'Content type for payload (json or form)',
+                  },
+                  secret: {
+                    type: 'string',
+                    description: 'Optional secret for webhook validation',
+                  },
+                  channel: {
+                    type: 'string',
+                    description: 'Slack channel (for slack webhooks)',
+                  },
+                  username: {
+                    type: 'string',
+                    description: 'Slack username (for slack webhooks)',
+                  },
+                  icon_url: {
+                    type: 'string',
+                    description: 'Slack icon URL (for slack webhooks)',
+                  },
+                  color: {
+                    type: 'string',
+                    description: 'Slack color (for slack webhooks)',
+                  },
+                },
+                required: ['url', 'content_type'],
+                description: 'Webhook configuration',
+              },
+              events: {
+                type: 'array',
+                items: {
+                  type: 'string',
+                  enum: ['create', 'delete', 'fork', 'push', 'issues', 'issue_comment', 'pull_request', 'release'],
+                },
+                description: 'Events that trigger the webhook',
+              },
+              active: {
+                type: 'boolean',
+                description: 'Whether the webhook is active',
+              },
+            },
+            required: ['owner', 'repo', 'hookId', 'config'],
+          },
+        },
+        {
+          name: 'delete_hook',
+          description: 'Delete a webhook from a repository',
+          inputSchema: {
+            type: 'object',
+            properties: {
+              owner: {
+                type: 'string',
+                description: 'Repository owner username',
+              },
+              repo: {
+                type: 'string',
+                description: 'Repository name',
+              },
+              hookId: {
+                type: 'number',
+                description: 'Webhook ID to delete',
+              },
+            },
+            required: ['owner', 'repo', 'hookId'],
+          },
+        },
       ],
     };
   });
@@ -2511,6 +2741,72 @@ export function createMcpServer(gogsClient: GogsClient): Server {
           };
         }
 
+        case 'list_hooks': {
+          const { owner, repo } = ListHooksSchema.parse(args);
+          const hooks = await gogsClient.listHooks(owner, repo);
+          return {
+            content: [
+              {
+                type: 'text',
+                text: JSON.stringify(hooks, null, 2),
+              },
+            ],
+          };
+        }
+
+        case 'create_hook': {
+          const { owner, repo, type, config, events, active } = CreateHookSchema.parse(args);
+          const hook = await gogsClient.createHook(owner, repo, {
+            type,
+            config,
+            events,
+            active,
+          });
+          return {
+            content: [
+              {
+                type: 'text',
+                text: JSON.stringify(hook, null, 2),
+              },
+            ],
+          };
+        }
+
+        case 'update_hook': {
+          const { owner, repo, hookId, config, events, active } = UpdateHookSchema.parse(args);
+          const hook = await gogsClient.updateHook(owner, repo, hookId, {
+            config,
+            events,
+            active,
+          });
+          return {
+            content: [
+              {
+                type: 'text',
+                text: JSON.stringify(hook, null, 2),
+              },
+            ],
+          };
+        }
+
+        case 'delete_hook': {
+          const { owner, repo, hookId } = DeleteHookSchema.parse(args);
+          await gogsClient.deleteHook(owner, repo, hookId);
+          return {
+            content: [
+              {
+                type: 'text',
+                text: JSON.stringify({
+                  success: true,
+                  message: `Successfully deleted webhook ${hookId} from ${owner}/${repo}`,
+                  hookId,
+                  repository: `${owner}/${repo}`
+                }, null, 2),
+              },
+            ],
+          };
+        }
+
         default:
           throw new Error(`Unknown tool: ${name}`);
       }

+ 33 - 0
src/types.ts

@@ -212,3 +212,36 @@ export interface GogsPublicKey {
   read_only?: boolean;
   fingerprint?: string;
 }
+
+export type GogsWebhookEvent =
+  | 'create'
+  | 'delete'
+  | 'fork'
+  | 'push'
+  | 'issues'
+  | 'issue_comment'
+  | 'pull_request'
+  | 'release';
+
+export type GogsWebhookType = 'gogs' | 'slack';
+
+export interface GogsWebhookConfig {
+  url: string;
+  content_type: 'json' | 'form';
+  secret?: string;
+  // Slack-specific config
+  channel?: string;
+  username?: string;
+  icon_url?: string;
+  color?: string;
+}
+
+export interface GogsWebhook {
+  id: number;
+  type: GogsWebhookType;
+  events: GogsWebhookEvent[];
+  active: boolean;
+  config: GogsWebhookConfig;
+  updated_at: string;
+  created_at: string;
+}