feat: label tools (label_list + issue_label by name) — unblock applying status:dispatch from the MCP #52

Closed
opened 2026-07-04 11:00:09 +00:00 by mathias · 1 comment
Owner

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)
## 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)
Author
Owner

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.
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#52