The MCP can create a repo, mark it dispatch-eligible (dispatch_allow, #43), and create issues — but it cannot apply a label to an issue. Since dispatch triggers on the status:dispatch label, the MCP stops one step short of the only action that starts dispatched work. A repo created as "dispatchable" can't actually be dispatched to through this connector.
Concretely: issue_create accepts labels as []int64 (integer IDs), but there is no tool to discover those IDs and no tool to apply/remove labels on an existing issue. In practice every issue gets created unlabeled, and there's no MCP path to add status:dispatch afterwards. The Gitea UI is also not a reliable fallback for this in our setup.
Goal
Make labels usable end-to-end from the MCP, by name (so callers never need opaque integer IDs).
Scope — two small tools
Follow the existing tool pattern exactly: one <name>.go + <name>_test.go pair each, struct with New* / Descriptor() / Call(), registered in internal/tools/registry.go (RegisterAll). Mirror the structure of issue_create.go. Respect the allowlist Check(owner) gate as every mutating tool does.
Tool 1: label_list
Input: owner, repo.
Lists the repo's labels via the Gitea labels API, returning {id, name, color} for each.
Read-only; still gate on allowlist for consistency.
Tool 2: issue_label
Input: owner, repo, number, and labels as an array of strings (label names). Optionally also accept integer IDs, but names are the primary interface.
Resolves names → IDs internally (reuse the same client call label_list uses), then applies them to the issue via the Gitea "add labels to issue" API (additive, not replace).
If a name doesn't exist in the repo, return a clear error naming the missing label (do not silently skip).
Gate on allowlist Check(owner).
Client layer
Add the needed methods to internal/gitea (e.g. ListLabels(ctx, owner, repo) and AddIssueLabels(ctx, owner, repo, number, ids)), following the existing client method style. Keep the name→ID resolution in the tool layer or a small shared helper — whichever matches house conventions.
Acceptance criteria
label_list returns id/name/color for a repo's labels.
issue_label applies one or more labels by name to an existing issue; verified against a fake/mocked gitea client in tests.
Unknown label name → clear, specific error.
Both tools registered in RegisterAll (and therefore present in the dispatch round-trip test per the registry comment).
Table-driven tests following the repo idiom (testify), covering: successful list; successful apply-by-name; unknown-name error; allowlist rejection.
task check passes.
Why this is priority
This is the missing seam that makes dispatch_allow meaningful: without it, creating dispatchable repos and issues can't lead to actual dispatch runs from the MCP. It directly unblocks driving parallax's build issues (e.g. parallax#3) through the dispatcher.
Out of scope
Label creation/deletion/editing (only list + apply needed now).
Removing labels from issues (can be a later addition if needed).
parallax#3 (first build issue currently blocked from dispatch by the missing label-apply capability)
## Problem
The MCP can create a repo, mark it dispatch-eligible (`dispatch_allow`, #43), and create issues — but it **cannot apply a label to an issue**. Since dispatch triggers on the `status:dispatch` label, the MCP stops one step short of the only action that starts dispatched work. A repo created as "dispatchable" can't actually be dispatched to through this connector.
Concretely: `issue_create` accepts `labels` as `[]int64` (integer IDs), but there is **no tool to discover those IDs** and no tool to apply/remove labels on an existing issue. In practice every issue gets created unlabeled, and there's no MCP path to add `status:dispatch` afterwards. The Gitea UI is also not a reliable fallback for this in our setup.
## Goal
Make labels usable end-to-end from the MCP, **by name** (so callers never need opaque integer IDs).
## Scope — two small tools
Follow the existing tool pattern exactly: one `<name>.go` + `<name>_test.go` pair each, struct with `New*` / `Descriptor()` / `Call()`, registered in `internal/tools/registry.go` (`RegisterAll`). Mirror the structure of `issue_create.go`. Respect the allowlist `Check(owner)` gate as every mutating tool does.
### Tool 1: `label_list`
- Input: `owner`, `repo`.
- Lists the repo's labels via the Gitea labels API, returning `{id, name, color}` for each.
- Read-only; still gate on allowlist for consistency.
### Tool 2: `issue_label`
- Input: `owner`, `repo`, `number`, and `labels` as an array of **strings (label names)**. Optionally also accept integer IDs, but names are the primary interface.
- Resolves names → IDs internally (reuse the same client call `label_list` uses), then applies them to the issue via the Gitea "add labels to issue" API (additive, not replace).
- If a name doesn't exist in the repo, return a clear error naming the missing label (do not silently skip).
- Gate on allowlist `Check(owner)`.
## Client layer
Add the needed methods to `internal/gitea` (e.g. `ListLabels(ctx, owner, repo)` and `AddIssueLabels(ctx, owner, repo, number, ids)`), following the existing client method style. Keep the name→ID resolution in the tool layer or a small shared helper — whichever matches house conventions.
## Acceptance criteria
- `label_list` returns id/name/color for a repo's labels.
- `issue_label` applies one or more labels **by name** to an existing issue; verified against a fake/mocked gitea client in tests.
- Unknown label name → clear, specific error.
- Both tools registered in `RegisterAll` (and therefore present in the dispatch round-trip test per the registry comment).
- Table-driven tests following the repo idiom (testify), covering: successful list; successful apply-by-name; unknown-name error; allowlist rejection.
- `task check` passes.
## Why this is priority
This is the missing seam that makes `dispatch_allow` meaningful: without it, creating dispatchable repos and issues can't lead to actual dispatch runs from the MCP. It directly unblocks driving parallax's build issues (e.g. parallax#3) through the dispatcher.
## Out of scope
- Label creation/deletion/editing (only list + apply needed now).
- Removing labels from issues (can be a later addition if needed).
- Org-level label management.
## Related
- #43 (added `dispatch_allow`)
- parallax#3 (first build issue currently blocked from dispatch by the missing label-apply capability)
Shipped in v0.8.0 (82823aa), plus a follow-up fix in v0.9.1 (ac33795) from independent review.
label_list (owner, repo → id/name/color for every label) and issue_label (owner, repo, number, labels-by-name — resolves via label_list's name→ID map, additive POST, unknown name fails closed naming exactly which label is missing) — both registered, both allowlist-gated, both TDD'd against the confirmed live Gitea label API shapes.
Independent adversarial review caught a real bug before it could bite a caller: the advertised schema marked labels as required, but the code already treated labels/label_ids as either-or — so an MCP client that validates against the advertised schema (as clients commonly do) would reject a label_ids-only call as invalid, even though the code path existed and was meant to serve it. That path also had zero test coverage. Fixed in v0.9.1: labels dropped from required (owner/repo/number remain), added TestIssueLabelAppliesByIDOnly (asserts ListLabels is never called when IDs are already known) and a schema-lock test.
task check green throughout (exit 0, 0 FAIL — verified). This unblocks applying status:dispatch to parallax#3 from the MCP. Closing #52.
Shipped in v0.8.0 (`82823aa`), plus a follow-up fix in v0.9.1 (`ac33795`) from independent review.
**`label_list`** (owner, repo → id/name/color for every label) and **`issue_label`** (owner, repo, number, labels-by-name — resolves via `label_list`'s name→ID map, additive POST, unknown name fails closed naming exactly which label is missing) — both registered, both allowlist-gated, both TDD'd against the confirmed live Gitea label API shapes.
**Independent adversarial review caught a real bug before it could bite a caller:** the advertised schema marked `labels` as *required*, but the code already treated `labels`/`label_ids` as either-or — so an MCP client that validates against the advertised schema (as clients commonly do) would reject a `label_ids`-only call as invalid, even though the code path existed and was meant to serve it. That path also had zero test coverage. Fixed in v0.9.1: `labels` dropped from `required` (owner/repo/number remain), added `TestIssueLabelAppliesByIDOnly` (asserts `ListLabels` is never called when IDs are already known) and a schema-lock test.
`task check` green throughout (exit 0, 0 FAIL — verified). This unblocks applying `status:dispatch` to parallax#3 from the MCP. Closing #52.
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.
Problem
The MCP can create a repo, mark it dispatch-eligible (
dispatch_allow, #43), and create issues — but it cannot apply a label to an issue. Since dispatch triggers on thestatus:dispatchlabel, the MCP stops one step short of the only action that starts dispatched work. A repo created as "dispatchable" can't actually be dispatched to through this connector.Concretely:
issue_createacceptslabelsas[]int64(integer IDs), but there is no tool to discover those IDs and no tool to apply/remove labels on an existing issue. In practice every issue gets created unlabeled, and there's no MCP path to addstatus:dispatchafterwards. The Gitea UI is also not a reliable fallback for this in our setup.Goal
Make labels usable end-to-end from the MCP, by name (so callers never need opaque integer IDs).
Scope — two small tools
Follow the existing tool pattern exactly: one
<name>.go+<name>_test.gopair each, struct withNew*/Descriptor()/Call(), registered ininternal/tools/registry.go(RegisterAll). Mirror the structure ofissue_create.go. Respect the allowlistCheck(owner)gate as every mutating tool does.Tool 1:
label_listowner,repo.{id, name, color}for each.Tool 2:
issue_labelowner,repo,number, andlabelsas an array of strings (label names). Optionally also accept integer IDs, but names are the primary interface.label_listuses), then applies them to the issue via the Gitea "add labels to issue" API (additive, not replace).Check(owner).Client layer
Add the needed methods to
internal/gitea(e.g.ListLabels(ctx, owner, repo)andAddIssueLabels(ctx, owner, repo, number, ids)), following the existing client method style. Keep the name→ID resolution in the tool layer or a small shared helper — whichever matches house conventions.Acceptance criteria
label_listreturns id/name/color for a repo's labels.issue_labelapplies one or more labels by name to an existing issue; verified against a fake/mocked gitea client in tests.RegisterAll(and therefore present in the dispatch round-trip test per the registry comment).task checkpasses.Why this is priority
This is the missing seam that makes
dispatch_allowmeaningful: without it, creating dispatchable repos and issues can't lead to actual dispatch runs from the MCP. It directly unblocks driving parallax's build issues (e.g. parallax#3) through the dispatcher.Out of scope
Related
dispatch_allow)Shipped in v0.8.0 (
82823aa), plus a follow-up fix in v0.9.1 (ac33795) from independent review.label_list(owner, repo → id/name/color for every label) andissue_label(owner, repo, number, labels-by-name — resolves vialabel_list's name→ID map, additive POST, unknown name fails closed naming exactly which label is missing) — both registered, both allowlist-gated, both TDD'd against the confirmed live Gitea label API shapes.Independent adversarial review caught a real bug before it could bite a caller: the advertised schema marked
labelsas required, but the code already treatedlabels/label_idsas either-or — so an MCP client that validates against the advertised schema (as clients commonly do) would reject alabel_ids-only call as invalid, even though the code path existed and was meant to serve it. That path also had zero test coverage. Fixed in v0.9.1:labelsdropped fromrequired(owner/repo/number remain), addedTestIssueLabelAppliesByIDOnly(assertsListLabelsis never called when IDs are already known) and a schema-lock test.task checkgreen throughout (exit 0, 0 FAIL — verified). This unblocks applyingstatus:dispatchto parallax#3 from the MCP. Closing #52.