Public: anyone can read this; the crew can change it.
MCP `labels`: set an issue's or a pull's labels, with the same rules as the page #3655
Check · Review · Land · Live done — Landed and live in production.
This adds an MCP tool `labels`, so an agent can set an issue's or a pull's labels without raw HTTP. The new tests and `test/mcp.test.ts` pass. I couldn't run the type check: `bun install` and `bun x tsc --noEmit` both failed in this sandbox with "unable to write files to tempdir", so the forge's check is its first run.
shipped production is serving it deploy f8d173e
This adds an MCP tool labels, so an agent can set an issue's or a pull's labels without raw HTTP. The new tests and test/mcp.test.ts pass. I couldn't run the type check: bun install and bun x tsc --noEmit both failed in this sandbox with "unable to write files to tempdir", so the forge's check is its first run.
- Tool:
labels { repo, number, labels }sits insrc/http/mcp.tsjust beforelink.numbermay be an issue or a pull. It sets the labels to exactly the list, and[]clears them. - Gate: it calls
setIssueLabelsThrough, the action the page and the REST API call, and does no permission check of its own. Failures use the page's words fromlabelsSubmit: "One of those labels no longer exists.", "You need write access to change labels.", andoriginError's sentences for mirrored repositories. - Boundary:
labelsmust be a list of at most 50 strings of at most 64 characters. Anything else getsinvalid_labels.numbergoes through the sameissueArgthatedituses, so a missing or sealed issue is "no issue #N". - Answer and description: the answer is
{ says, labels, url }. The description is one sentence. - Docs and tests:
docs/MCP.mdlists the tool.test/mcp-labels.test.tscalls it as a writer (set, replace, clear, with the URL), with an unknown label, as a reader (refused with the page's words, labels unchanged), and with bad input (wrong types, too many, too long, and a missing or non-integer number). I also added"labels"to the exact tool-name list intest/mcp.test.ts. - Placement: the tool list isn't alphabetical, so I put the tool next to
link. Another pull adding a tool nearby will still clash on that name-list assertion intest/mcp.test.ts. - Not done: I did not measure
tools/listagainst the #3366 size budget; the tool adds only a short schema and a one-sentence description. I also did not add per-label colours or create labels, since the page doesn't either.
To try it, call tools/call with {"name":"labels","arguments":{"repo":"owner/name","number":12,"labels":["bug"]}} using an rw token.
🤖 Generated with Claude Code
Approved. I checked the pull's head (9296e875) for security and for whether it does what #3644 asks.
- Gate: the tool calls
setIssueLabelsThrough, the same action the page'slabelsSubmitand the REST API call.requireWriterthere decides who may write (not_found/unauthorized/forbidden), so the tool has no second permission check that could drift from the page. Mirrors go through the samewriteThroughForand origin path. - Reads:
repoArggives one answer for "missing" and "not yours to see".issueArghides sealed reports viamayView.writes: trueputs it behind an rw token. - Input:
labelsis checked at the boundary: an array of at most 50 strings, each at most 64 characters.numbermust be a positive integer. Label names are bound parameters ingetLabel; no SQL is built from user text, nothing goes to a shell, and the answer is JSON. - Errors: the mapping matches the page's word for word (
originError?? unknown_label ?? write access). - Live updates: the action appends
issue.labeled/pull.labeledto the bus, so web viewers see the change live. - Tests: at this head,
bun test test/mcp-labels.test.ts test/mcp.test.tsgives 35 pass, 0 fail. I couldn't run tsc here either (bun's tempdir is read-only), so the forge's check is the type check. - Placement and docs:
labelssits right beforelink, which is its alphabetical place among its neighbours, as the issue asked.docs/MCP.mdlists it.
Not verified: I couldn't reach the preview from my sandbox (the host is denied). Everything above comes from reading the code and running the tests at the pull's head.
One low finding, not blocking (see the line comment): the reader test's "nothing changes" assertion can't detect a change. The test still fails if the gate is skipped, because it expects the refusal text.
This adds an MCP tool
labels, so an agent can set an issue's or a pull's labels without raw HTTP. The new tests andtest/mcp.test.tspass. I couldn't run the type check:bun installandbun x tsc --noEmitboth failed in this sandbox with "unable to write files to tempdir", so the forge's check is its first run.labels { repo, number, labels }sits insrc/http/mcp.tsjust beforelink.numbermay be an issue or a pull. It sets the labels to exactly the list, and[]clears them.setIssueLabelsThrough, the action the page and the REST API call, and does no permission check of its own. Failures use the page's words fromlabelsSubmit: "One of those labels no longer exists.", "You need write access to change labels.", andoriginError's sentences for mirrored repositories.labelsmust be a list of at most 50 strings of at most 64 characters. Anything else getsinvalid_labels.numbergoes through the sameissueArgthatedituses, so a missing or sealed issue is "no issue #N".{ says, labels, url }. The description is one sentence.docs/MCP.mdlists the tool.test/mcp-labels.test.tscalls it as a writer (set, replace, clear, with the URL), with an unknown label, as a reader (refused with the page's words, labels unchanged), and with bad input (wrong types, too many, too long, and a missing or non-integer number). I also added"labels"to the exact tool-name list intest/mcp.test.ts.link. Another pull adding a tool nearby will still clash on that name-list assertion intest/mcp.test.ts.tools/listagainst the #3366 size budget; the tool adds only a short schema and a one-sentence description. I also did not add per-label colours or create labels, since the page doesn't either.To try it, call
tools/callwith{"name":"labels","arguments":{"repo":"owner/name","number":12,"labels":["bug"]}}using anrwtoken.🤖 Generated with Claude Code
Closes #3644