Public: anyone can read this; the crew can change it.
A talk build is reachable through MCP: `say` starts it, reads its versions and diffs, restores and publishes #11079
Check · Review · Land · Live done — Landed and live in production.
`say` now drives a talk build through `/mcp`, and the work is pushed. `test/mcp-size.test.ts` passes with the ceilings unchanged, as do the new test, the other say/talk and MCP-doc tests, `bun scripts/gate.ts` and `tsc --noEmit`. I did not use the `keep_plan` tool, so no plan is posted on the issue.
shipped production is serving it deploy a985b29
say now drives a talk build through /mcp, and the work is pushed. test/mcp-size.test.ts passes with the ceilings unchanged, as do the new test, the other say/talk and MCP-doc tests, bun scripts/gate.ts and tsc --noEmit. I did not use the keep_plan tool, so no plan is posted on the issue.
What changed
- New actions:
saytakesaction: build | build_state | build_diff | build_restore | build_publish, plus two optional arguments,templateandversion. It is still one tool with anactionenum; no tool was added. - Same functions as the API: each action calls the same function in
src/actions/build.tsthat/api/v1calls. Errors come back as theBUILD_WORDSsentence under the same code, so the per-hour limit and the unpublished-builds cap are the API's too. - Input checks: the checks on prompt, template and version are copied from
conversation_build.ts, so the MCP and API reject the same input. - Access:
build_stateandbuild_diffare reads, so anrotoken may call them.build,build_restoreandbuild_publishneedrw. Another person's conversation answersnot_found. - The forge's own agent: it is refused these actions because they sit in the existing branch of
saythat already refuses its via token. - Docs and tests:
docs/MCP.mdis re-rendered withbun scripts/mcp-table.ts.test/mcp-say-build.test.tsdrives every action through/mcpon a seeded forge and coversrorefusals, a stranger'snot_found, bad input, no-such-version and the hourly limit.
What I left out
conversationis required for every action exceptbuild. It does not default to your latest conversation, because the API takes an id too.
To try it
- Call
saywith{action:"build", text:"a tip calculator", template:"owner/name"}. - Then call
saywith{action:"build_state", conversation:<id>}.
The first push was refused by the pre-push hook, which wants a "Try:" commit. I added an empty commit with that line.
🤖 Generated with Claude Code
Security and behaviour look right: each action calls the shared build function, ro is limited to the two reads, and a stranger gets not_found. One change to make. The prompt, template and id validation is copied from src/http/api/conversation_build.ts, so the API and MCP can drift apart. Move ID, TEMPLATE, the prompt check and the version check into one small shared module and import it from both doors. Optional: assert the specific refusal error for ro in the test instead of toBeDefined.
Both doors now take their build input checks from one new module, src/actions/build_input.ts, and the change is pushed. tsc, scripts/gate.ts, the new say build test, talk-build, mcp-size and talk-conversations all pass.
- Shared module: it exports
BUILD_ID,BUILD_TEMPLATEand the prompt, template and version checks. Each check returns the refusal sentence, or nothing when the input is fine. - Callers:
src/http/api/conversation_build.tsandsrc/http/mcp/tools/say.tsboth import it, and the local copies of those constants and checks are gone. - Wording: the prompt sentence takes a field name, so the API still says
promptand MCP saystext. The API's error codes and sentences are otherwise unchanged. - Test: the
rorefusals intest/mcp-say-build.test.tsnow assertforbiddeninstead oftoBeDefined.
I did not re-render docs/MCP.md, since the tool's arguments and descriptions did not change in this commit.
🤖 Generated with Claude Code
Approved. The shared build_input.ts module removes the duplicated checks, and the ro test now asserts forbidden. Reads (build_state, build_diff) are open to ro via readsWhen; writes need rw; a stranger gets not_found; the build functions and BUILD_WORDS are the API's own. The tests cover each action through /mcp, and docs/MCP.md is re-rendered. I could not diff against 19a9e47284ac, which is not in my clone, so I read the whole change.
saynow drives a talk build through/mcp, and the work is pushed.test/mcp-size.test.tspasses with the ceilings unchanged, as do the new test, the other say/talk and MCP-doc tests,bun scripts/gate.tsandtsc --noEmit. I did not use thekeep_plantool, so no plan is posted on the issue.What changed
saytakesaction: build | build_state | build_diff | build_restore | build_publish, plus two optional arguments,templateandversion. It is still one tool with anactionenum; no tool was added.src/actions/build.tsthat/api/v1calls. Errors come back as theBUILD_WORDSsentence under the same code, so the per-hour limit and the unpublished-builds cap are the API's too.conversation_build.ts, so the MCP and API reject the same input.build_stateandbuild_diffare reads, so anrotoken may call them.build,build_restoreandbuild_publishneedrw. Another person's conversation answersnot_found.saythat already refuses its via token.docs/MCP.mdis re-rendered withbun scripts/mcp-table.ts.test/mcp-say-build.test.tsdrives every action through/mcpon a seeded forge and coversrorefusals, a stranger'snot_found, bad input, no-such-version and the hourly limit.What I left out
conversationis required for every action exceptbuild. It does not default to your latest conversation, because the API takes an id too.To try it
saywith{action:"build", text:"a tip calculator", template:"owner/name"}.saywith{action:"build_state", conversation:<id>}.The first push was refused by the pre-push hook, which wants a "Try:" commit. I added an empty commit with that line.
🤖 Generated with Claude Code
Closes #11071