feat: label CRUD tools + label resource-templates #190

Closed
opened 2026-06-02 06:35:10 +00:00 by goern · 2 comments
goern commented 2026-06-02 06:35:10 +00:00 (Migrated from codeberg.org)

Problem

forgejo-mcp exposes read/assignment label tools — list_repo_labels, list_org_labels, add_issue_labels, remove_issue_labels (all in operation/issue/issue.go) — but has no label CRUD: you cannot create, edit, or delete a repo or org label through MCP.

Surfaced 2026-05-31 while creating the RFC - Request For Comments label for #176: the work fell back to raw curl against the Forgejo REST API because no MCP tool exists. Under the project's MCP-only policy that is a hard stop, not a minor annoyance.

This issue also extends the forgejo:// resource-template surface (see mcp-resources-core) to labels, so labels become URI-addressable like the existing issue / pr / commit resources.

Forgejo / Gitea API surface to wrap

Repo labels (Gitea-compatible swagger):

Method Path Purpose
GET /repos/{owner}/{repo}/labels list repo labels
GET /repos/{owner}/{repo}/labels/{id} get one label
POST /repos/{owner}/{repo}/labels create a label
PATCH /repos/{owner}/{repo}/labels/{id} edit a label
DELETE /repos/{owner}/{repo}/labels/{id} delete a label

Org labels share the shape under /orgs/{org}/labels[/{id}].

CreateLabelOption / EditLabelOption fields (relevant):

{
  "name": "RFC - Request For Comments",
  "color": "#0e8a16",
  "description": "Design/spec open for feedback before implementation"
}

SDK vs raw-HTTP

codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v3 (the pinned SDK) already provides repo-label CRUD — use these directly:

  • CreateLabel(owner, repo, CreateLabelOption) (*Label, …)
  • EditLabel(owner, repo, id, EditLabelOption) (*Label, …)
  • DeleteLabel(owner, repo, id) (…)
  • GetRepoLabel(owner, repo, id) (*Label, …) / ListRepoLabels(...)

The SDK Label struct exposes id, name, color, description, url only. It does not model Forgejo's newer exclusive / is_archived label fields — those are out of scope for the first cut (note it explicitly; do not silently drop them — a follow-up can add raw-HTTP passthrough if needed).

Org-label CRUD has no SDK method. Org labels must go through the pkg/forgejo raw-HTTP helper (DoJSON), exactly like the existing fetchOrgLabels in operation/issue/issue.go and the wiki/attachment precedent.

Proposed MCP tools

Naming consistent with existing create_* / edit_* / delete_* patterns:

  • create_label(owner, repo, name, color, description?) → returns the created label (incl. numeric id).
  • edit_label(owner, repo, id, name?, color?, description?) → updated label (PATCH semantics: only provided fields change).
  • delete_label(owner, repo, id) → success.
  • get_repo_label(owner, repo, id) → single label (fills the read-one gap; list_repo_labels already lists).

Org-label CRUD (create_org_label / edit_org_label / delete_org_label, raw-HTTP) is a secondary scope — land repo-label CRUD first, org follows the same shape via DoJSON.

color accepts the Forgejo hex form (#rrggbb); validate/normalise before send so a missing # doesn't 422.

Proposed resource-templates (additive surface)

Per AGENTS.md / openspec/specs/mcp-resources-core, labels should ALSO be URI-addressable — additive, instance-portable, coexisting with the tools above (no tool removed). Singular repo segment, matching the established forgejo://repo/{owner}/{repo}/... scheme (issue/pr/commit):

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

Requirements carried from the resources convention:

  • Place under operation/<domain>/resources*.go (labels currently live in the issue domain — keep them together or split a label domain, implementer's call); use operation/resource helpers (ParseXxx, Bounded, MapForgejoError).
  • The embedded labels list MUST use operation/resource.Bounded with EmbeddedListCap so the truncation sentinel stays consistent with every other resource; request EmbeddedListCap+1 items so Bounded's >cap check fires (mirror the resources.go comment in the issue domain).
  • Bound the list resource per docs/design/output-bounding.md: client-controlled bound + resumability + documented parameters.
  • Reads reuse the singleton pkg/forgejo client; private-repo reads return the same 403 / 404 MCP mapping via MapForgejoError.
  • An org-label listing resource (e.g. forgejo://owner/{owner}/labels) is a follow-up, gated on org-label CRUD landing.

Clients implementing resources/templates/list + resources/read resolve these URIs directly; others fall back to the tools transparently.

Why prioritise

Label management is currently impossible via MCP — any project bootstrapping its own labels (RFC, triage states, priority tiers) is blocked or forced to break the MCP-only policy. The repo-label CRUD is a thin, well-scoped wrapper over SDK methods that already exist in the pinned dependency, so the cost is low and the unblock is immediate.

Acceptance criteria

Naming note (2026-06-10): The OpenSpec adversarial review renamed tools for symmetry: create_repo_label / edit_repo_label / delete_repo_label (not create_label / edit_label / delete_label). All AC items below map to the shipped names. Org-label CRUD and org-label resource (forgejo://org/{org}/labels) were also fully implemented (not deferred).

Tools

  • create_repo_label available with (owner, repo, name, color, description?) and creates a repo label on Codeberg.
  • edit_repo_label round-trips a PATCH (only supplied fields change).
  • delete_repo_label removes the label; get_repo_label reads one back by id.
  • color normalised to Forgejo hex form before send (no 422 on a missing #).
  • Org-label CRUD implemented via pkg/forgejo raw-HTTP DoJSON: create_org_label, edit_org_label, delete_org_label, get_org_label.
  • README / tool index + AGENTS.md updated so a ToolSearch for label surfaces the new tools.

Resource-templates

  • forgejo://repo/{owner}/{repo}/labels registered, returning a bounded embedded list of labels.
  • forgejo://repo/{owner}/{repo}/label/{id} registered, returning a single label.
  • forgejo://org/{org}/labels registered, returning a bounded org-label list (bonus: org-label resource landed in same change).
  • All three live under operation/issue/resources_label.go and use operation/resource helpers (ParseXxx, Bounded, MapForgejoError).
  • The embedded list uses operation/resource.Bounded + EmbeddedListCap (shared truncation sentinel) and satisfies docs/design/output-bounding.md (client-controlled bound + resumability + documented params).
  • README "Resources" section + AGENTS.md resource table updated to list all three label resource-templates.

Notes / non-goals

  • exclusive / is_archived label fields: out of scope for the first cut (SDK v3 doesn't model them); revisit via raw-HTTP if a use case appears.
  • This issue is written to be the basis for an OpenSpec change; the change itself is generated separately.
## Problem `forgejo-mcp` exposes **read/assignment** label tools — `list_repo_labels`, `list_org_labels`, `add_issue_labels`, `remove_issue_labels` (all in `operation/issue/issue.go`) — but has **no label CRUD**: you cannot create, edit, or delete a repo or org label through MCP. Surfaced 2026-05-31 while creating the `RFC - Request For Comments` label for #176: the work fell back to raw `curl` against the Forgejo REST API because no MCP tool exists. Under the project's MCP-only policy that is a hard stop, not a minor annoyance. This issue also extends the `forgejo://` resource-template surface (see `mcp-resources-core`) to labels, so labels become URI-addressable like the existing issue / pr / commit resources. ## Forgejo / Gitea API surface to wrap Repo labels (Gitea-compatible swagger): | Method | Path | Purpose | | -------- | ------------------------------------------ | ---------------- | | `GET` | `/repos/{owner}/{repo}/labels` | list repo labels | | `GET` | `/repos/{owner}/{repo}/labels/{id}` | get one label | | `POST` | `/repos/{owner}/{repo}/labels` | create a label | | `PATCH` | `/repos/{owner}/{repo}/labels/{id}` | edit a label | | `DELETE` | `/repos/{owner}/{repo}/labels/{id}` | delete a label | Org labels share the shape under `/orgs/{org}/labels[/{id}]`. `CreateLabelOption` / `EditLabelOption` fields (relevant): ```json { "name": "RFC - Request For Comments", "color": "#0e8a16", "description": "Design/spec open for feedback before implementation" } ``` ## SDK vs raw-HTTP `codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v3` (the pinned SDK) **already** provides repo-label CRUD — use these directly: - `CreateLabel(owner, repo, CreateLabelOption) (*Label, …)` - `EditLabel(owner, repo, id, EditLabelOption) (*Label, …)` - `DeleteLabel(owner, repo, id) (…)` - `GetRepoLabel(owner, repo, id) (*Label, …)` / `ListRepoLabels(...)` The SDK `Label` struct exposes `id`, `name`, `color`, `description`, `url` only. It does **not** model Forgejo's newer `exclusive` / `is_archived` label fields — those are out of scope for the first cut (note it explicitly; do not silently drop them — a follow-up can add raw-HTTP passthrough if needed). **Org-label CRUD has no SDK method.** Org labels must go through the `pkg/forgejo` raw-HTTP helper (`DoJSON`), exactly like the existing `fetchOrgLabels` in `operation/issue/issue.go` and the wiki/attachment precedent. ## Proposed MCP tools Naming consistent with existing `create_*` / `edit_*` / `delete_*` patterns: - `create_label(owner, repo, name, color, description?)` → returns the created label (incl. numeric `id`). - `edit_label(owner, repo, id, name?, color?, description?)` → updated label (PATCH semantics: only provided fields change). - `delete_label(owner, repo, id)` → success. - `get_repo_label(owner, repo, id)` → single label (fills the read-one gap; `list_repo_labels` already lists). Org-label CRUD (`create_org_label` / `edit_org_label` / `delete_org_label`, raw-HTTP) is a **secondary scope** — land repo-label CRUD first, org follows the same shape via `DoJSON`. `color` accepts the Forgejo hex form (`#rrggbb`); validate/normalise before send so a missing `#` doesn't 422. ## Proposed resource-templates (additive surface) Per `AGENTS.md` / `openspec/specs/mcp-resources-core`, labels should ALSO be URI-addressable — additive, instance-portable, coexisting with the tools above (no tool removed). Singular `repo` segment, matching the established `forgejo://repo/{owner}/{repo}/...` scheme (issue/pr/commit): - `forgejo://repo/{owner}/{repo}/labels` → embedded, **bounded** list of repo labels - `forgejo://repo/{owner}/{repo}/label/{id}` → single repo label Requirements carried from the resources convention: - Place under `operation/<domain>/resources*.go` (labels currently live in the `issue` domain — keep them together or split a `label` domain, implementer's call); use `operation/resource` helpers (`ParseXxx`, `Bounded`, `MapForgejoError`). - The embedded labels list MUST use `operation/resource.Bounded` with `EmbeddedListCap` so the truncation sentinel stays consistent with every other resource; request `EmbeddedListCap+1` items so `Bounded`'s `>cap` check fires (mirror the `resources.go` comment in the issue domain). - Bound the list resource per [`docs/design/output-bounding.md`](docs/design/output-bounding.md): client-controlled bound + resumability + documented parameters. - Reads reuse the singleton `pkg/forgejo` client; private-repo reads return the same `403` / `404` MCP mapping via `MapForgejoError`. - An org-label listing resource (e.g. `forgejo://owner/{owner}/labels`) is a follow-up, gated on org-label CRUD landing. Clients implementing `resources/templates/list` + `resources/read` resolve these URIs directly; others fall back to the tools transparently. ## Why prioritise Label management is currently **impossible** via MCP — any project bootstrapping its own labels (RFC, triage states, priority tiers) is blocked or forced to break the MCP-only policy. The repo-label CRUD is a thin, well-scoped wrapper over SDK methods that already exist in the pinned dependency, so the cost is low and the unblock is immediate. ## Acceptance criteria > **Naming note (2026-06-10):** The OpenSpec adversarial review renamed tools for symmetry: `create_repo_label` / `edit_repo_label` / `delete_repo_label` (not `create_label` / `edit_label` / `delete_label`). All AC items below map to the shipped names. Org-label CRUD and org-label resource (`forgejo://org/{org}/labels`) were also fully implemented (not deferred). ### Tools - [x] `create_repo_label` available with `(owner, repo, name, color, description?)` and creates a repo label on Codeberg. - [x] `edit_repo_label` round-trips a PATCH (only supplied fields change). - [x] `delete_repo_label` removes the label; `get_repo_label` reads one back by id. - [x] `color` normalised to Forgejo hex form before send (no 422 on a missing `#`). - [x] Org-label CRUD implemented via `pkg/forgejo` raw-HTTP `DoJSON`: `create_org_label`, `edit_org_label`, `delete_org_label`, `get_org_label`. - [x] README / tool index + `AGENTS.md` updated so a `ToolSearch` for `label` surfaces the new tools. ### Resource-templates - [x] `forgejo://repo/{owner}/{repo}/labels` registered, returning a bounded embedded list of labels. - [x] `forgejo://repo/{owner}/{repo}/label/{id}` registered, returning a single label. - [x] `forgejo://org/{org}/labels` registered, returning a bounded org-label list (bonus: org-label resource landed in same change). - [x] All three live under `operation/issue/resources_label.go` and use `operation/resource` helpers (`ParseXxx`, `Bounded`, `MapForgejoError`). - [x] The embedded list uses `operation/resource.Bounded` + `EmbeddedListCap` (shared truncation sentinel) and satisfies `docs/design/output-bounding.md` (client-controlled bound + resumability + documented params). - [x] README "Resources" section + `AGENTS.md` resource table updated to list all three label resource-templates. ## Notes / non-goals - `exclusive` / `is_archived` label fields: out of scope for the first cut (SDK v3 doesn't model them); revisit via raw-HTTP if a use case appears. - This issue is written to be the basis for an OpenSpec change; the change itself is generated separately.
goern commented 2026-06-02 06:37:51 +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 label surface end-to-end:

  • create_labelget_repo_label / list_repo_labelsedit_label (PATCH only-supplied-fields) → delete_label round-trip.
  • Read both resource-templates: forgejo://repo/{owner}/{repo}/labels (bounded list) and forgejo://repo/{owner}/{repo}/label/{id} (single).
  • Show color normalisation (input without # still succeeds) and the bounded-list truncation sentinel when the repo has > EmbeddedListCap labels.

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_label --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}/labels) 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 label surface end-to-end: - `create_label` → `get_repo_label` / `list_repo_labels` → `edit_label` (PATCH only-supplied-fields) → `delete_label` round-trip. - Read both resource-templates: `forgejo://repo/{owner}/{repo}/labels` (bounded list) and `forgejo://repo/{owner}/{repo}/label/{id}` (single). - Show `color` normalisation (input without `#` still succeeds) and the bounded-list truncation sentinel when the repo has > `EmbeddedListCap` labels. ### 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_label --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}/labels`) 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:10 +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 label 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 label resource-template half of this issue depends on #191. Tool CLI parity here stays automatic.
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#190
No description provided.