forgejo-mcp/openspec/changes/label-crud/proposal.md
Christoph Görn ca69d3b196
docs: 📝 OpenSpec change label-crud (label CRUD tools + label resources)
Spec-only change for Codeberg #190 — no implementation, so docs: keeps
the changelog clean (code ships under feat: when the tasks are applied).

Capabilities:
- label-crud: repo + org label CRUD tools (create/edit/delete/get,
  repo-prefixed names), SDK for repo + raw-HTTP DoJSON for org, shared
  hex-color normalisation (6-digit), PATCH edit, safe delete with a
  delete_mode "safe"|"force" enum + in-use guard.
- mcp-resource-label: forgejo://repo/{o}/{r}/labels{?page,limit},
  /label/{id}, and forgejo://org/{org}/labels{?page,limit}.
- mcp-resources-core (modified): adds a Collection-resource requirement.

Adversarially reviewed by a debate team; synthesis + open questions
recorded in design.md. Pre-code verification gates: org get-one endpoint,
3-digit hex acceptance.

Codeberg #190
2026-06-02 10:45:19 +02:00

6.6 KiB

Why

forgejo-mcp exposes read and 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.

This surfaced 2026-05-31 (Codeberg #190) 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 — any project bootstrapping its own labels (RFC, triage states, priority tiers) is blocked.

Repo-label CRUD is a thin wrapper over forgejo-sdk/v3 methods that already ship in the pinned dependency; org-label CRUD has no SDK method and goes through the existing pkg/forgejo raw-HTTP DoJSON helper, exactly like fetchOrgLabels. The change also extends the additive forgejo:// resource-template surface (per mcp-resources-core) to labels, so labels become URI-addressable like the existing issue / pr / commit resources.

What Changes

  • Add repo-label CRUD MCP tools, naming consistent with existing create_* / edit_* / delete_* patterns:
    • create_repo_label(owner, repo, name, color, description?) → created label (incl. numeric id)
    • edit_repo_label(owner, repo, id, name?, color?, description?) → updated label (PATCH: only supplied fields change)
    • delete_repo_label(owner, repo, id, delete_mode?) → success — refuses an in-use label unless delete_mode="force" (see below)
    • get_repo_label(owner, repo, id) → single label (fills the read-one gap; list_repo_labels already lists)
  • Add org-label CRUD MCP tools (same shape, raw-HTTP DoJSON against /orgs/{org}/labels[/{id}]):
    • create_org_label(org, name, color, description?)
    • edit_org_label(org, id, name?, color?, description?)
    • delete_org_label(org, id, delete_mode?) → refuses an in-use label unless delete_mode="force"
    • get_org_label(org, id) → single org label
  • Safe delete: delete_repo_label / delete_org_label first count issues/PRs referencing the label. If the count is > 0 they stop and report the in-use count instead of deleting; delete_mode="force" overrides and deletes anyway. (Repo count via the SDK ListRepoIssues pagination filtered by label name — pkg/forgejo.DoJSON cannot read response headers, so no raw X-Total-Count; org count iterates the org's repos and is best-effort — it MAY under-count labels used in repos the token cannot read, and the refusal message says so. See design D9.)
  • Implement repo-label CRUD via the pinned codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v3 methods (CreateLabel, EditLabel, DeleteLabel, GetRepoLabel); org-label CRUD via pkg/forgejo DoJSON — no new dependency.
  • Normalise color to the Forgejo hex form (#rrggbb) before send so a missing # does not 422 (both repo and org tools).
  • Add additive forgejo:// resource-templates, coexisting with the tools (no tool removed):
    • forgejo://repo/{owner}/{repo}/labels → bounded list of repo labels
    • forgejo://repo/{owner}/{repo}/label/{id} → single repo label
    • forgejo://org/{org}/labels → bounded list of org labels (gate lifted now org CRUD lands here)
  • Reuse operation/resource helpers (ParseXxx, Bounded + EmbeddedListCap, MapForgejoError) and satisfy docs/design/output-bounding.md for the list resources.
  • Surface the new tools and resource-templates in the README tool/resource tables and AGENTS.md.

Capabilities

New Capabilities

  • label-crud: Repo- and org-label create / edit / delete / get-one MCP tools. Repo labels wrap forgejo-sdk/v3; org labels use pkg/forgejo raw-HTTP DoJSON. Shared hex-color normalisation, PATCH edit semantics, and safe-delete (delete_mode enum + in-use guard).
  • mcp-resource-label: forgejo://repo/{owner}/{repo}/labels (bounded list), forgejo://repo/{owner}/{repo}/label/{id} (single), and forgejo://org/{org}/labels (bounded org-label list) resource-templates, following the mcp-resources-core convention.

Modified Capabilities

  • mcp-resources-core: Adds a Collection resource requirement. forgejo://repo/{owner}/{repo}/labels is the first standalone collection-as-resource in the scheme (every prior resource is a single-entity fetch; lists were embedded or tool-only). Rather than let that precedent harden implicitly, this change records it normatively in the core framework: a bounded collection MAY be a resource, capped with EmbeddedListCap + the shared sentinel, without removing the corresponding list tool. Future list entities (org labels, branches, milestones) cite this rule. No existing requirement is altered — this is a new requirement on the capability.

Existing list/assignment label tools (list_repo_labels, list_org_labels, add_issue_labels, remove_issue_labels) are unchanged.

Dependency / ordering

The mcp-resources-core delta above assumes the mcp-resource-templates change has archived (it is the home of the mcp-resources-core capability, currently unarchived). This change SHOULD archive after mcp-resource-templates; if it lands first, the core-spec delta is held until the base capability exists in openspec/specs/.

Impact

  • Affected code: a label tool file under operation/<domain>/ (issue domain currently owns labels — keep them together or split a label domain, implementer's call) registered from operation/operation.go; resources*.go for the three label templates; a ParseLabel (repo) and ParseOrgLabels (org list) parser in operation/resource; an org-label DoJSON block in pkg/forgejo mirroring fetchOrgLabels; a shared in-use-count helper.
  • No new external dependencies. Repo-label CRUD uses SDK methods already pinned; org-label CRUD uses the existing pkg/forgejo DoJSON helper; resources use the existing mcp-go resource APIs and the singleton pkg/forgejo client.
  • No breaking changes. All existing tools remain; resource-unaware clients see exactly today's surface.
  • Documentation: README tool table gains the eight label tools; README "Resources" section + AGENTS.md resource table gain the three label templates.

Out of Scope

  • exclusive / is_archived label fields: SDK v3 Label does not model them (exposes id, name, color, description, url only); for parity the org-label raw-HTTP path also omits them in the first cut. Revisit via raw-HTTP passthrough (both paths) if a use case appears.
  • Implementation: this change is proposal + design + specs + tasks. Code lands when tasks are applied.