Public: anyone can read this; the crew can change it.
MCP `app_notes`: an app's admin lists the notes sent from its forge button, hides one, bans or unbans its sender #7273
Check · Review · Land · Live done — Landed and live in production.
This adds an MCP tool, `app_notes` (`{ app, action?, note? }`), so an app's admin can handle notes from the forge button through MCP, as they already can on the studio page and in the terminal. With no `action` it lists the shown notes and the banned senders, as `ownerNotes` returns them. `hide`, `ban` or `unban` with a `note` id goes through `moderateNote` or `unbanSender`, so it uses the same ad…
shipped production is serving it deploy da65c36
This adds an MCP tool, app_notes ({ app, action?, note? }), so an app's admin can handle notes from the forge button through MCP, as they already can on the studio page and in the terminal. With no action it lists the shown notes and the banned senders, as ownerNotes returns them. hide, ban or unban with a note id goes through moderateNote or unbanSender, so it uses the same admin check and the same steps as the page: hiding closes the note's issue, and its picture is dropped.
What it answers:
- Admin check: anyone who is not an admin of the app's repository gets
forbidden, both when listing and when acting. - Not found: an unknown app, or an unknown note, is
not_found. - Invalid: an unknown action, or a missing or too-long note id, is
invalid. - Read-only tokens: writing needs an
rwtoken. A call with noactioncounts as a read, so a read-only token can list notes but cannot act on them.
What to look at:
src/http/mcp/tools/app_notes.tsis the tool.src/http/mcp/tools/index.tsand the tools table indocs/MCP.mdwere regenerated bybun scripts/mcp-tools.ts.test/mcp-app-notes.test.tssends notes through the button's endpoint (feedback()insrc/apps/feedback.ts), then lists, hides, bans and unbans them through/mcp. It checks the database after each step. It also checks the refusals: a team member with write access but not admin on an org repository, a read-only token, an unknown app or note, and an unknown action.test/mcp.test.tsgets one line: it lists every tool name, soapp_notesis added to that list. The old list was correct; it just did not include the new tool.
I made it a new tool rather than an action on an existing one, because no existing tool covers app notes and the issue asks for this one. Its description is one sentence. bun test test/mcp-size.test.ts passes without raising any limit.
bun x tsc --noEmit is clean. scripts/fast.ts picked 857 test files (about 75 minutes), so instead I ran the MCP-wide tests (mcp, mcp-size, mcp-tools, mcp-docs, mcp-write-scope, llms-tools, mcp-cards-coverage, mcp-discovery-walk) plus apps-feedback and the new test. After the one-line list fix, mcp, mcp-size and the new test pass on a re-run; the others passed on the first run.
To try it: on a preview, send a note from an app's forge button, then call app_notes with { "app": "owner/name" }, and then with { "app": "owner/name", "action": "hide", "note": "<id>" }.
🤖 Generated with Claude Code
Approved. The tool is a thin door over the actions the studio card (web/studio/notes.ts noteModerate) and the terminal (tui/screens/notes.ts) already use: ownerNotes, moderateNote and unbanSender each check canAdmin on the authenticated MCP user. Writes need an rw token because of writes: true and readsWithout: "action". The input checks match the page's (an action from the enum, a note id of 1–64 characters, not_found for an unknown note). planRepo/repoArg resolves the app, so a private repository is not exposed. No SQL or shell is built from input. The JSON output carries no HTML.
The tests prove the behaviour end to end. A note goes in through the real feedback() endpoint. Listing, hide (which also closes the issue), ban and unban are checked against the database. The refusals are covered too: a writer who is not an admin, an ro token trying to act, an unknown app or note, a bad action and a missing note. bun test test/mcp-app-notes.test.ts passes at b155661a (3 pass). On the preview, / answers 200 and /mcp without a token refuses as it should. Small and single-purpose, it reuses the existing actions, and the tools/list size budget holds without raising any limit.
This adds an MCP tool,
app_notes({ app, action?, note? }), so an app's admin can handle notes from the forge button through MCP, as they already can on the studio page and in the terminal. With noactionit lists the shown notes and the banned senders, asownerNotesreturns them.hide,banorunbanwith anoteid goes throughmoderateNoteorunbanSender, so it uses the same admin check and the same steps as the page: hiding closes the note's issue, and its picture is dropped.What it answers:
forbidden, both when listing and when acting.not_found.invalid.rwtoken. A call with noactioncounts as a read, so a read-only token can list notes but cannot act on them.What to look at:
src/http/mcp/tools/app_notes.tsis the tool.src/http/mcp/tools/index.tsand the tools table indocs/MCP.mdwere regenerated bybun scripts/mcp-tools.ts.test/mcp-app-notes.test.tssends notes through the button's endpoint (feedback()insrc/apps/feedback.ts), then lists, hides, bans and unbans them through/mcp. It checks the database after each step. It also checks the refusals: a team member with write access but not admin on an org repository, a read-only token, an unknown app or note, and an unknown action.test/mcp.test.tsgets one line: it lists every tool name, soapp_notesis added to that list. The old list was correct; it just did not include the new tool.I made it a new tool rather than an
actionon an existing one, because no existing tool covers app notes and the issue asks for this one. Its description is one sentence.bun test test/mcp-size.test.tspasses without raising any limit.bun x tsc --noEmitis clean.scripts/fast.tspicked 857 test files (about 75 minutes), so instead I ran the MCP-wide tests (mcp,mcp-size,mcp-tools,mcp-docs,mcp-write-scope,llms-tools,mcp-cards-coverage,mcp-discovery-walk) plusapps-feedbackand the new test. After the one-line list fix,mcp,mcp-sizeand the new test pass on a re-run; the others passed on the first run.To try it: on a preview, send a note from an app's forge button, then call
app_noteswith{ "app": "owner/name" }, and then with{ "app": "owner/name", "action": "hide", "note": "<id>" }.🤖 Generated with Claude Code
Closes #7214