Add label mutation tools — issue_unlabel, label_create, label_edit, label_delete #61

Open
opened 2026-08-10 09:32:37 +00:00 by mathias · 0 comments
Owner

Why

Label taxonomy is now load-bearing for how work gets planned, not decoration. In
mathias/trucker-ecommerce labels carry phase, work type, risk tier, blocking state and sprint
membership — and the MCP can only ever add.

That has already caused real problems:

  • Three issues carried stale blocked/* labels after the blocker cleared. The board said one
    thing, reality said another, and sprint planning had to work around the tracker rather than
    from it.
  • Sprint membership can't be assigned or cleared by an agent, so closing out a sprint needs a
    human in the Gitea UI.

An agent that can label but not unlabel produces a board that only accumulates. State that
can only be added is not state, it's sediment.

Scope

Four tools. label_list and issue_label already exist and are unchanged.

Tool Gitea endpoint Note
issue_unlabel DELETE /repos/{o}/{r}/issues/{i}/labels/{id} One label per call upstream; accept a list and loop
label_create POST /repos/{o}/{r}/labels Currently impossible via MCP — labels must be hand-made in the UI before any can be applied
label_edit PATCH /repos/{o}/{r}/labels/{id} Rename and recolour
label_delete DELETE /repos/{o}/{r}/labels/{id} Removes from the repo and every issue carrying it

Design notes

Follow the existing pattern. internal/gitea/labels.go for the client methods,
one file per tool under internal/tools/, tests alongside. issue_label.go is the closest
model — same allowlist check, same parseArgs, same textOK return.

Name resolution, consistently. issue_label accepts either labels (names, resolved via
ListLabels) or label_ids. issue_unlabel should accept both the same way — an agent
holding a name shouldn't have to list labels first to remove one.

Return the resulting label set after a mutation, as AddIssueLabels does. That makes the
result self-verifying: the caller sees what the issue now carries rather than trusting that the
call did what was asked.

Unlabelling a label the issue doesn't have should be a no-op returning the current set, not
an error. Idempotence matters here — an agent reconciling desired state against actual will
routinely try to remove things that aren't there.

label_delete is destructive across the whole repo. Deleting blocked/client strips it
from every issue at once, and there is no undo. The tool description must say this plainly.
Consider whether it belongs in the MCP at all, or whether repo-level label deletion is
appropriately a human action — label_edit covers renaming, which handles most of what
deletion would be reached for.

Done when

  • Client methods in internal/gitea/labels.go with tests
  • Four tool files under internal/tools/, registered, each with tests
  • issue_unlabel accepts names or IDs, mirroring issue_label
  • Unlabelling an absent label is a no-op, with an explicit test
  • Every mutation returns the resulting label set
  • label_delete's repo-wide blast radius stated in its description — or the tool is
    deliberately omitted, with the reasoning recorded
  • Tool count in the docs updated

Verification

The concrete case that motivated this, once deployed:

issue_unlabel  mathias/trucker-ecommerce  #2   blocked/google
issue_unlabel  mathias/trucker-ecommerce  #3   blocked/client
issue_unlabel  mathias/trucker-ecommerce  #25  blocked/client
label_create   mathias/trucker-ecommerce  sprint/1
issue_label    → #2 #3 #25 #27 #28 #30 #31

If that sequence runs end to end without touching the UI, this is done.

Note

This is process infrastructure, not a feature. The reason it's worth doing properly rather
than as a script on koala: a script would be a parallel path that only one machine has, while
the MCP is where the process actually lives and every agent session can reach it.

Risk: low — additive tools, no change to existing behaviour
Blocked on: nothing

## Why Label taxonomy is now load-bearing for how work gets planned, not decoration. In `mathias/trucker-ecommerce` labels carry phase, work type, risk tier, blocking state and sprint membership — and the MCP can only ever **add**. That has already caused real problems: - Three issues carried stale `blocked/*` labels after the blocker cleared. The board said one thing, reality said another, and sprint planning had to work around the tracker rather than from it. - Sprint membership can't be assigned or cleared by an agent, so closing out a sprint needs a human in the Gitea UI. An agent that can label but not unlabel produces a board that only accumulates. **State that can only be added is not state, it's sediment.** ## Scope Four tools. `label_list` and `issue_label` already exist and are unchanged. | Tool | Gitea endpoint | Note | |---|---|---| | `issue_unlabel` | `DELETE /repos/{o}/{r}/issues/{i}/labels/{id}` | One label per call upstream; accept a list and loop | | `label_create` | `POST /repos/{o}/{r}/labels` | Currently impossible via MCP — labels must be hand-made in the UI before any can be applied | | `label_edit` | `PATCH /repos/{o}/{r}/labels/{id}` | Rename and recolour | | `label_delete` | `DELETE /repos/{o}/{r}/labels/{id}` | Removes from the repo *and* every issue carrying it | ## Design notes **Follow the existing pattern.** `internal/gitea/labels.go` for the client methods, one file per tool under `internal/tools/`, tests alongside. `issue_label.go` is the closest model — same allowlist check, same `parseArgs`, same `textOK` return. **Name resolution, consistently.** `issue_label` accepts either `labels` (names, resolved via `ListLabels`) or `label_ids`. `issue_unlabel` should accept both the same way — an agent holding a name shouldn't have to list labels first to remove one. **Return the resulting label set** after a mutation, as `AddIssueLabels` does. That makes the result self-verifying: the caller sees what the issue now carries rather than trusting that the call did what was asked. **Unlabelling a label the issue doesn't have** should be a no-op returning the current set, not an error. Idempotence matters here — an agent reconciling desired state against actual will routinely try to remove things that aren't there. **`label_delete` is destructive across the whole repo.** Deleting `blocked/client` strips it from every issue at once, and there is no undo. The tool description must say this plainly. Consider whether it belongs in the MCP at all, or whether repo-level label deletion is appropriately a human action — `label_edit` covers renaming, which handles most of what deletion would be reached for. ## Done when - [ ] Client methods in `internal/gitea/labels.go` with tests - [ ] Four tool files under `internal/tools/`, registered, each with tests - [ ] `issue_unlabel` accepts names or IDs, mirroring `issue_label` - [ ] Unlabelling an absent label is a no-op, with an explicit test - [ ] Every mutation returns the resulting label set - [ ] `label_delete`'s repo-wide blast radius stated in its description — or the tool is deliberately omitted, with the reasoning recorded - [ ] Tool count in the docs updated ## Verification The concrete case that motivated this, once deployed: ``` issue_unlabel mathias/trucker-ecommerce #2 blocked/google issue_unlabel mathias/trucker-ecommerce #3 blocked/client issue_unlabel mathias/trucker-ecommerce #25 blocked/client label_create mathias/trucker-ecommerce sprint/1 issue_label → #2 #3 #25 #27 #28 #30 #31 ``` If that sequence runs end to end without touching the UI, this is done. ## Note This is process infrastructure, not a feature. The reason it's worth doing properly rather than as a script on koala: a script would be a parallel path that only one machine has, while the MCP is where the process actually lives and every agent session can reach it. **Risk:** low — additive tools, no change to existing behaviour **Blocked on:** nothing
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mathias/gitea-mcp#61