feat: expose repository webhook management tools #136

Closed
opened 2026-05-20 07:29:55 +00:00 by goern · 7 comments
goern commented 2026-05-20 07:29:55 +00:00 (Migrated from codeberg.org)

Problem

forgejo-mcp currently has no MCP tools for managing repository webhooks. A ToolSearch for hook / webhook against the running server returns no matches, and inspecting the published tool list confirms there is no create_repo_hook, list_repo_hooks, edit_repo_hook or delete_repo_hook surface.

This gap blocks any workflow that needs to wire a Forgejo / Codeberg repository up to an external system — concretely, my current motivation is registering a Pipelines-as-Code controller webhook on a Codeberg-hosted project. The accepted MCP-only policy in that project forbids falling back to gh, berg, forgejo-cli, tea, or raw curl, so the missing tool stops the work cold rather than just being slightly annoying.

Forgejo / Gitea API surface to wrap

The upstream API has supported webhook CRUD on a repository for years (Gitea-compatible swagger):

Method Path Purpose
GET /repos/{owner}/{repo}/hooks list hooks
GET /repos/{owner}/{repo}/hooks/{id} get one hook
POST /repos/{owner}/{repo}/hooks create a hook
PATCH /repos/{owner}/{repo}/hooks/{id} edit hook
DELETE /repos/{owner}/{repo}/hooks/{id} delete hook
POST /repos/{owner}/{repo}/hooks/{id}/tests trigger a test delivery

The CreateHookOption body schema (relevant fields):

{
  "type": "forgejo",
  "active": true,
  "branch_filter": "*",
  "events": ["push", "pull_request"],
  "config": {
    "url": "https://pac-controller.example.com",
    "content_type": "json",
    "secret": "<webhook secret>",
    "http_method": "post"
  }
}

type accepts at least forgejo, gitea, gogs, slack, discord, dingtalk, telegram, matrix, wechatwork, packagist, feishu, msteams — start with forgejo since that is the canonical for Codeberg-style instances. Org-level (/orgs/{org}/hooks) and system-level (/admin/hooks) webhooks have the same shape and could land in a follow-up.

Proposed MCP tools

Naming consistent with the existing mcp__codeberg__create_branch / update_file / delete_branch patterns:

  • create_repo_hook(owner, repo, url, type, events[], content_type, secret?, http_method?, active?, branch_filter?) → returns the created hook (incl. its numeric id).
  • list_repo_hooks(owner, repo, page?, limit?) → list of hooks.
  • get_repo_hook(owner, repo, id) → single hook.
  • edit_repo_hook(owner, repo, id, ...patch fields) → updated hook.
  • delete_repo_hook(owner, repo, id) → success.
  • test_repo_hook(owner, repo, id) → trigger a test delivery (optional, nice-to-have for diagnosing).

secret should be marked sensitive in the schema so MCP hosts can mask it in transcripts.

Resource-templates (additive surface)

Per the project's resources convention (AGENTS.md, openspec/specs/mcp-resources-core), entities should ALSO be exposed as forgejo:// resource-templates — additive, instance-portable, and coexisting with the tools above (no tool removed). The mcp-resource-templates OpenSpec change explicitly defers webhook resources to this issue, so webhooks are tracked here rather than in that change.

Proposed resource-templates (singular repo segment, matching the mcp-resources-core URI scheme — note the deferral text used a plural repos shorthand; the normative form is singular):

  • forgejo://repo/{owner}/{repo}/hooks → embedded, bounded list of hooks
  • forgejo://repo/{owner}/{repo}/hooks/{id} → single hook

Requirements carried from the resources convention:

  • Place under operation/<domain>/resources*.go; use the operation/resource helpers (ParseXxx, Bounded, MapForgejoError).
  • The embedded list MUST use operation/resource.Bounded so the truncation sentinel stays consistent with every other resource.
  • Bound the list resource per docs/design/output-bounding.md: client-controlled bound + resumability + documented parameters.
  • The secret field MUST be masked in resource reads exactly as in the tool results (same masked hook representation Forgejo returns).
  • Reads reuse the singleton pkg/forgejo client; private-repo reads return the same 403 / 404 MCP error mapping the tools use, via MapForgejoError.

Clients that implement resources/templates/list + resources/read resolve these URIs directly; clients without template support fall back to the tools transparently.

Why prioritise

Without this, every CI/CD / observability / mirroring integration that runs through a webhook either needs a human in the Codeberg UI or breaks the project's MCP-only policy. Adding the five core CRUD tools is a small, well-scoped wrapper around an API surface that has been stable in Forgejo/Gitea for a decade.

Acceptance criteria

Tools

  • create_repo_hook available with the parameters listed above and successfully creates a Forgejo-style webhook on Codeberg.
  • list_repo_hooks, get_repo_hook, edit_repo_hook, delete_repo_hook round-trip the created hook.
  • secret parameter not echoed in tool-result payloads (return the masked hook representation Forgejo already gives back).
  • README / tool index updated so a ToolSearch for webhook actually surfaces the new tools.

Resource-templates

  • forgejo://repo/{owner}/{repo}/hooks registered as a resource-template returning a bounded, embedded list of hooks.
  • forgejo://repo/{owner}/{repo}/hooks/{id} registered as a resource-template returning a single hook.
  • Both live under operation/<domain>/resources*.go and use operation/resource helpers (ParseXxx, Bounded, MapForgejoError).
  • The embedded hooks list uses operation/resource.Bounded (shared truncation sentinel) and satisfies docs/design/output-bounding.md (client-controlled bound + resumability + documented params).
  • secret masked in resource reads identically to the tool results.
  • README "Resources" section updated to list the two hook resource-templates.
## Problem `forgejo-mcp` currently has no MCP tools for managing repository webhooks. A `ToolSearch` for `hook` / `webhook` against the running server returns no matches, and inspecting the published tool list confirms there is no `create_repo_hook`, `list_repo_hooks`, `edit_repo_hook` or `delete_repo_hook` surface. This gap blocks any workflow that needs to wire a Forgejo / Codeberg repository up to an external system — concretely, my current motivation is registering a [Pipelines-as-Code](https://pipelinesascode.com/) controller webhook on a Codeberg-hosted project. The accepted MCP-only policy in that project forbids falling back to `gh`, `berg`, `forgejo-cli`, `tea`, or raw `curl`, so the missing tool stops the work cold rather than just being slightly annoying. ## Forgejo / Gitea API surface to wrap The upstream API has supported webhook CRUD on a repository for years (Gitea-compatible swagger): | Method | Path | Purpose | | -------- | --------------------------------------------- | ------------------------ | | `GET` | `/repos/{owner}/{repo}/hooks` | list hooks | | `GET` | `/repos/{owner}/{repo}/hooks/{id}` | get one hook | | `POST` | `/repos/{owner}/{repo}/hooks` | create a hook | | `PATCH` | `/repos/{owner}/{repo}/hooks/{id}` | edit hook | | `DELETE` | `/repos/{owner}/{repo}/hooks/{id}` | delete hook | | `POST` | `/repos/{owner}/{repo}/hooks/{id}/tests` | trigger a test delivery | The `CreateHookOption` body schema (relevant fields): ```json { "type": "forgejo", "active": true, "branch_filter": "*", "events": ["push", "pull_request"], "config": { "url": "https://pac-controller.example.com", "content_type": "json", "secret": "<webhook secret>", "http_method": "post" } } ``` `type` accepts at least `forgejo`, `gitea`, `gogs`, `slack`, `discord`, `dingtalk`, `telegram`, `matrix`, `wechatwork`, `packagist`, `feishu`, `msteams` — start with `forgejo` since that is the canonical for Codeberg-style instances. Org-level (`/orgs/{org}/hooks`) and system-level (`/admin/hooks`) webhooks have the same shape and could land in a follow-up. ## Proposed MCP tools Naming consistent with the existing `mcp__codeberg__create_branch` / `update_file` / `delete_branch` patterns: - `create_repo_hook(owner, repo, url, type, events[], content_type, secret?, http_method?, active?, branch_filter?)` → returns the created hook (incl. its numeric `id`). - `list_repo_hooks(owner, repo, page?, limit?)` → list of hooks. - `get_repo_hook(owner, repo, id)` → single hook. - `edit_repo_hook(owner, repo, id, ...patch fields)` → updated hook. - `delete_repo_hook(owner, repo, id)` → success. - `test_repo_hook(owner, repo, id)` → trigger a test delivery (optional, nice-to-have for diagnosing). `secret` should be marked sensitive in the schema so MCP hosts can mask it in transcripts. ## Resource-templates (additive surface) Per the project's resources convention (`AGENTS.md`, `openspec/specs/mcp-resources-core`), entities should ALSO be exposed as `forgejo://` resource-templates — additive, instance-portable, and coexisting with the tools above (no tool removed). The `mcp-resource-templates` OpenSpec change explicitly defers webhook resources to this issue, so webhooks are tracked here rather than in that change. Proposed resource-templates (singular `repo` segment, matching the `mcp-resources-core` URI scheme — note the deferral text used a plural `repos` shorthand; the normative form is singular): - `forgejo://repo/{owner}/{repo}/hooks` → embedded, bounded list of hooks - `forgejo://repo/{owner}/{repo}/hooks/{id}` → single hook Requirements carried from the resources convention: - Place under `operation/<domain>/resources*.go`; use the `operation/resource` helpers (`ParseXxx`, `Bounded`, `MapForgejoError`). - The embedded list MUST use `operation/resource.Bounded` so the truncation sentinel stays consistent with every other resource. - Bound the list resource per [`docs/design/output-bounding.md`](docs/design/output-bounding.md): client-controlled bound + resumability + documented parameters. - The `secret` field MUST be masked in resource reads exactly as in the tool results (same masked hook representation Forgejo returns). - Reads reuse the singleton `pkg/forgejo` client; private-repo reads return the same `403` / `404` MCP error mapping the tools use, via `MapForgejoError`. Clients that implement `resources/templates/list` + `resources/read` resolve these URIs directly; clients without template support fall back to the tools transparently. ## Why prioritise Without this, every CI/CD / observability / mirroring integration that runs through a webhook either needs a human in the Codeberg UI or breaks the project's MCP-only policy. Adding the five core CRUD tools is a small, well-scoped wrapper around an API surface that has been stable in Forgejo/Gitea for a decade. ## Acceptance criteria ### Tools - [ ] `create_repo_hook` available with the parameters listed above and successfully creates a Forgejo-style webhook on Codeberg. - [ ] `list_repo_hooks`, `get_repo_hook`, `edit_repo_hook`, `delete_repo_hook` round-trip the created hook. - [ ] `secret` parameter not echoed in tool-result payloads (return the masked hook representation Forgejo already gives back). - [ ] README / tool index updated so a `ToolSearch` for `webhook` actually surfaces the new tools. ### Resource-templates - [ ] `forgejo://repo/{owner}/{repo}/hooks` registered as a resource-template returning a bounded, embedded list of hooks. - [ ] `forgejo://repo/{owner}/{repo}/hooks/{id}` registered as a resource-template returning a single hook. - [ ] Both live under `operation/<domain>/resources*.go` and use `operation/resource` helpers (`ParseXxx`, `Bounded`, `MapForgejoError`). - [ ] The embedded hooks list uses `operation/resource.Bounded` (shared truncation sentinel) and satisfies `docs/design/output-bounding.md` (client-controlled bound + resumability + documented params). - [ ] `secret` masked in resource reads identically to the tool results. - [ ] README "Resources" section updated to list the two hook resource-templates.
goern commented 2026-06-02 06:37:45 +00:00 (Migrated from codeberg.org)

Two cross-cutting requirements for the OpenSpec change

1. Showboat demo is a merge gate

The OpenSpec change for this issue MUST ship a showboat demo, co-located with the spec, before openspec sync/archive. Built against a running instance during the implementation PR; read at acceptance without re-execution. No demo → no sync/archive.

Demo must exercise the webhook surface end-to-end:

  • create_repo_hooklist_repo_hooks / get_repo_hookedit_repo_hooktest_repo_hookdelete_repo_hook round-trip.
  • Read both resource-templates: forgejo://repo/{owner}/{repo}/hooks (bounded list) and forgejo://repo/{owner}/{repo}/hooks/{id} (single).
  • Show secret masked in both tool results and resource reads.

2. CLI parity — tools AND resources

New surface must be reachable via the --cli mode (cmd/cli.go), not only over MCP.

  • Tools: parity is automatic — --cli dispatches by tool name via registerToolsWithDomains + cliExec. Just confirm forgejo-mcp --cli create_repo_hook --args '{...}' works and the tools appear in --cli list. Add to the acceptance demo.
  • Resources: ⚠️ gap — the CLI has no resource read path today (resources aren't registered in --cli mode; there is no forgejo:// reader). The change must either add a CLI resource-read command (e.g. forgejo-mcp --cli read forgejo://repo/{owner}/{repo}/hooks) wired to the same handlers, or explicitly file a follow-up. Resource parity over CLI is NOT free like tool parity is.

Both points should land as acceptance-criteria checkboxes in the change's tasks.

## Two cross-cutting requirements for the OpenSpec change ### 1. Showboat demo is a merge gate The OpenSpec change for this issue MUST ship a [showboat](.claude/skills/showboat/SKILL.md) demo, co-located with the spec, **before** `openspec sync`/`archive`. Built against a running instance during the implementation PR; read at acceptance without re-execution. No demo → no sync/archive. Demo must exercise the webhook surface end-to-end: - `create_repo_hook` → `list_repo_hooks` / `get_repo_hook` → `edit_repo_hook` → `test_repo_hook` → `delete_repo_hook` round-trip. - Read both resource-templates: `forgejo://repo/{owner}/{repo}/hooks` (bounded list) and `forgejo://repo/{owner}/{repo}/hooks/{id}` (single). - Show `secret` masked in **both** tool results and resource reads. ### 2. CLI parity — tools AND resources New surface must be reachable via the `--cli` mode (`cmd/cli.go`), not only over MCP. - **Tools:** parity is automatic — `--cli` dispatches by tool name via `registerToolsWithDomains` + `cliExec`. Just confirm `forgejo-mcp --cli create_repo_hook --args '{...}'` works and the tools appear in `--cli list`. Add to the acceptance demo. - **Resources:** ⚠️ gap — the CLI has **no resource read path today** (resources aren't registered in `--cli` mode; there is no `forgejo://` reader). The change must either add a CLI resource-read command (e.g. `forgejo-mcp --cli read forgejo://repo/{owner}/{repo}/hooks`) wired to the same handlers, or explicitly file a follow-up. Resource parity over CLI is NOT free like tool parity is. Both points should land as acceptance-criteria checkboxes in the change's tasks.
goern commented 2026-06-02 06:39:09 +00:00 (Migrated from codeberg.org)

CLI resource-parity (point 2 above) split out to #191 — one reader for all resource-templates instead of re-deriving the plumbing per feature. The webhook resource-template half of this issue depends on #191. Tool CLI parity here stays automatic.

CLI resource-parity (point 2 above) split out to #191 — one reader for all resource-templates instead of re-deriving the plumbing per feature. The webhook resource-template half of this issue depends on #191. Tool CLI parity here stays automatic.
goern commented 2026-06-17 07:21:47 +00:00 (Migrated from codeberg.org)

Session minutes — 2026-06-17

OpenSpec planning complete. PR #257 merged (scheduled auto-merge after CI).

Artifacts created

Artifact Notes
proposal.md Motivation, 6 tools + 2 resource templates, impact
design.md 5 decisions: operation/hook/ package, URI scheme, config-key allowlist for secret masking, fire-and-forget test, two-path bounding
specs/repo-webhook-tools/spec.md Normative requirements + scenarios for all 8 surfaces
tasks.md 20-checkpoint implementation checklist
battle-test.md Adversarial review: 7 critiques, 6 concede-patch applied, 1 concede-future
journal.jsonl Turn/artifact log for future grading

Key design decisions

  • Secret masking: explicit config-key allowlist (url, content_type, http_method, branch_filter) — raw Config map[string]string MUST NOT be embedded in the payload struct
  • Output bounding: tool = unbounded enumeration path (no ceiling); resource = cap 30 via EmbeddedListCap
  • URI scheme: forgejo://repo/{owner}/{repo}/hooks{?page,limit} (collection) + forgejo://repo/{owner}/{repo}/hook/{id} (single)

Future work filed

  • Rate/confirmation guard for test_repo_hook (server-initiated outbound delivery, loop-abuse concern)

Next step

Run /opsx:apply on openspec/changes/repo-webhook-tools to start implementation.

## Session minutes — 2026-06-17 OpenSpec planning complete. PR #257 merged (scheduled auto-merge after CI). ### Artifacts created | Artifact | Notes | |---|---| | `proposal.md` | Motivation, 6 tools + 2 resource templates, impact | | `design.md` | 5 decisions: `operation/hook/` package, URI scheme, config-key allowlist for secret masking, fire-and-forget test, two-path bounding | | `specs/repo-webhook-tools/spec.md` | Normative requirements + scenarios for all 8 surfaces | | `tasks.md` | 20-checkpoint implementation checklist | | `battle-test.md` | Adversarial review: 7 critiques, 6 concede-patch applied, 1 concede-future | | `journal.jsonl` | Turn/artifact log for future grading | ### Key design decisions - **Secret masking**: explicit config-key allowlist (`url`, `content_type`, `http_method`, `branch_filter`) — raw `Config map[string]string` MUST NOT be embedded in the payload struct - **Output bounding**: tool = unbounded enumeration path (no ceiling); resource = cap 30 via `EmbeddedListCap` - **URI scheme**: `forgejo://repo/{owner}/{repo}/hooks{?page,limit}` (collection) + `forgejo://repo/{owner}/{repo}/hook/{id}` (single) ### Future work filed - Rate/confirmation guard for `test_repo_hook` (server-initiated outbound delivery, loop-abuse concern) ### Next step Run `/opsx:apply` on `openspec/changes/repo-webhook-tools` to start implementation.
goern commented 2026-06-17 08:08:08 +00:00 (Migrated from codeberg.org)

/triage

/triage
op1st-gitops commented 2026-06-17 08:10:52 +00:00 (Migrated from codeberg.org)

🤖 Automated triage

Summary: Requests six webhook-management MCP tools (list/get/create/edit/delete/test) plus two forgejo:// resource-templates for repository hooks. The Forgejo API surface is fully mapped, tool signatures are specified, secret-masking requirements defined, acceptance criteria are itemised for both tools and resource-templates, and OpenSpec planning is complete with all design artefacts merged. Implementation is ready to start via /opsx:apply.

Labels applied: Kind/Feature, Kind/OpenSpec, Reviewed/Confirmed, hermes-ready

🤖 **Automated triage** **Summary:** Requests six webhook-management MCP tools (list/get/create/edit/delete/test) plus two forgejo:// resource-templates for repository hooks. The Forgejo API surface is fully mapped, tool signatures are specified, secret-masking requirements defined, acceptance criteria are itemised for both tools and resource-templates, and OpenSpec planning is complete with all design artefacts merged. Implementation is ready to start via /opsx:apply. **Labels applied:** `Kind/Feature`, `Kind/OpenSpec`, `Reviewed/Confirmed`, `hermes-ready` <!-- ca-pipeline:triage:issue=136 -->
brenner-axiom commented 2026-06-17 08:17:55 +00:00 (Migrated from codeberg.org)

hermes-issue-worker: agent failed to process this issue (attempt 1/3)

Agent output
Hermes Agents Factory
=====================

Available tools:
  claude              - Claude Code CLI (Max subscription)
  hermes              - Hermes Agent orchestrator
  pi                  - pi.dev coding agent

Git config:
brenner-axiom
christoph+brenner@goern.name

usage: hermes [-h] [--version] [--resume SESSION] [--continue [SESSION_NAME]]
              [--worktree] [--skills SKILLS] [--yolo] [--pass-session-id]
              {chat,model,gateway,setup,whatsapp,login,logout,auth,status,cron,webhook,doctor,config,pairing,skills,plugins,honcho,memory,tools,mcp,sessions,insights,claw,version,update,uninstall,acp,profile,completion}
              ...
hermes: error: argument command: invalid choice: 'bash' (choose from 'chat', 'model', 'gateway', 'setup', 'whatsapp', 'login', 'logout', 'auth', 'status', 'cron', 'webhook', 'doctor', 'config', 'pairing', 'skills', 'plugins', 'honcho', 'memory', 'tools', 'mcp', 'sessions', 'insights', 'claw', 'version', 'update', 'uninstall', 'acp', 'profile', 'completion')
hermes-issue-worker: agent failed to process this issue (attempt 1/3) <details><summary>Agent output</summary> ``` Hermes Agents Factory ===================== Available tools: claude - Claude Code CLI (Max subscription) hermes - Hermes Agent orchestrator pi - pi.dev coding agent Git config: brenner-axiom christoph+brenner@goern.name usage: hermes [-h] [--version] [--resume SESSION] [--continue [SESSION_NAME]] [--worktree] [--skills SKILLS] [--yolo] [--pass-session-id] {chat,model,gateway,setup,whatsapp,login,logout,auth,status,cron,webhook,doctor,config,pairing,skills,plugins,honcho,memory,tools,mcp,sessions,insights,claw,version,update,uninstall,acp,profile,completion} ... hermes: error: argument command: invalid choice: 'bash' (choose from 'chat', 'model', 'gateway', 'setup', 'whatsapp', 'login', 'logout', 'auth', 'status', 'cron', 'webhook', 'doctor', 'config', 'pairing', 'skills', 'plugins', 'honcho', 'memory', 'tools', 'mcp', 'sessions', 'insights', 'claw', 'version', 'update', 'uninstall', 'acp', 'profile', 'completion') ``` </details>
goern commented 2026-06-17 08:27:22 +00:00 (Migrated from codeberg.org)

Implemented and shipped in commits fa64928a9d74f6 on main.

What landed

6 MCP tools under operation/hook/:

  • list_repo_hooks — paginated, no server ceiling (unbounded enumeration path)
  • get_repo_hook — single hook by numeric ID
  • create_repo_hook — accepts type, url, events, secret, content_type, http_method, branch_filter, active
  • edit_repo_hook — PATCH semantics; only supplied fields change
  • delete_repo_hook — returns success or MCP not-found error
  • test_repo_hook — triggers live delivery; description warns callers

2 resource templates:

  • forgejo://repo/{owner}/{repo}/hooks — bounded list (cap 30, sentinel list_repo_hooks)
  • forgejo://repo/{owner}/{repo}/hook/{id} — single hook (singular hook, per the normative URI scheme)

Secret never echoed: safeHook() uses an explicit config-key allowlist (url, content_type, http_method, branch_filter, created_at) — Config["secret"] is never read. This holds for all six tool handlers and both resource handlers.

Docs: AGENTS.md resource table updated; demos/README.md section 10 added; showboat demo at openspec/changes/repo-webhook-tools/specs/repo-webhook-tools/repo-webhook-tools.demo.md.

All acceptance criteria checked.

Implemented and shipped in commits `fa64928` → `a9d74f6` on `main`. ## What landed **6 MCP tools** under `operation/hook/`: - `list_repo_hooks` — paginated, no server ceiling (unbounded enumeration path) - `get_repo_hook` — single hook by numeric ID - `create_repo_hook` — accepts `type`, `url`, `events`, `secret`, `content_type`, `http_method`, `branch_filter`, `active` - `edit_repo_hook` — PATCH semantics; only supplied fields change - `delete_repo_hook` — returns success or MCP not-found error - `test_repo_hook` — triggers live delivery; description warns callers **2 resource templates**: - `forgejo://repo/{owner}/{repo}/hooks` — bounded list (cap 30, sentinel `list_repo_hooks`) - `forgejo://repo/{owner}/{repo}/hook/{id}` — single hook (singular `hook`, per the normative URI scheme) **Secret never echoed**: `safeHook()` uses an explicit config-key allowlist (`url`, `content_type`, `http_method`, `branch_filter`, `created_at`) — `Config["secret"]` is never read. This holds for all six tool handlers and both resource handlers. **Docs**: AGENTS.md resource table updated; `demos/README.md` section 10 added; showboat demo at `openspec/changes/repo-webhook-tools/specs/repo-webhook-tools/repo-webhook-tools.demo.md`. All acceptance criteria checked.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
agentic-forges/forgejo-mcp#136
No description provided.