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:
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
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Why
Label taxonomy is now load-bearing for how work gets planned, not decoration. In
mathias/trucker-ecommercelabels carry phase, work type, risk tier, blocking state and sprintmembership — and the MCP can only ever add.
That has already caused real problems:
blocked/*labels after the blocker cleared. The board said onething, reality said another, and sprint planning had to work around the tracker rather than
from it.
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_listandissue_labelalready exist and are unchanged.issue_unlabelDELETE /repos/{o}/{r}/issues/{i}/labels/{id}label_createPOST /repos/{o}/{r}/labelslabel_editPATCH /repos/{o}/{r}/labels/{id}label_deleteDELETE /repos/{o}/{r}/labels/{id}Design notes
Follow the existing pattern.
internal/gitea/labels.gofor the client methods,one file per tool under
internal/tools/, tests alongside.issue_label.gois the closestmodel — same allowlist check, same
parseArgs, sametextOKreturn.Name resolution, consistently.
issue_labelaccepts eitherlabels(names, resolved viaListLabels) orlabel_ids.issue_unlabelshould accept both the same way — an agentholding a name shouldn't have to list labels first to remove one.
Return the resulting label set after a mutation, as
AddIssueLabelsdoes. That makes theresult 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_deleteis destructive across the whole repo. Deletingblocked/clientstrips itfrom 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_editcovers renaming, which handles most of whatdeletion would be reached for.
Done when
internal/gitea/labels.gowith testsinternal/tools/, registered, each with testsissue_unlabelaccepts names or IDs, mirroringissue_labellabel_delete's repo-wide blast radius stated in its description — or the tool isdeliberately omitted, with the reasoning recorded
Verification
The concrete case that motivated this, once deployed:
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