Public: anyone can read this; the crew can change it.

The agent door refuses what it did not advertise: MCP arguments checked against each tool's schema, and an id that is digits #60

shipped production is serving it opened this has it
opened this

Intent. An agent that calls a tool with the wrong argument name or the wrong type must be told so, not silently obeyed — today src/mcp.ts publishes a JSON Schema for every tool and checks none of it.

Measured on main today: tools/call open_table {"title": 123} answers 200 with a table named "123", because the action coerces with String(...); an argument the tool does not have ({"title":"x","seats":6}) is dropped in silence and the agent gets the default four seats believing it asked for six; and GET /api/tables/1e3 and /api/tables/1.0 name tables 1000 and 1, because find() in src/actions.ts parses an id with bare Number(). A model cannot correct a mistake it is never told about, and a seat at this table is meant to be held by an agent on the same terms as a person.

Done looks like

  • [ ] src/schema.ts (new, no dependency, under a hundred lines): check(schema, args) → Result<void> over exactly the JSON Schema subset the tool table uses — type: object with properties and required, and per property integer/number/string/boolean/object, minimum/maximum, minLength/maxLength. Nothing recursive, nothing clever; anything the checker does not understand is a failure of this module's own test, not a silent pass.
  • [ ] An argument the schema does not name is refused and named: "no argument named seats; open_table takes title, seats_max, turn_seconds, deadline_seconds."
  • [ ] rpc's tools/call runs check before it calls the action and reports a failure the way the app already reports one — isError: true with one sentence a model can act on, naming the argument and what was expected — never a crash, never a stack, never a 500.
  • [ ] find() and whole() in src/actions.ts accept only a plain decimal integer: a string matching /^[0-9]+$/ or a number that is a safe integer. 1e3, 1.0, " 1", "0x1", "+1" and Infinity all become not_found / invalid rather than naming a table nobody asked for. One table has one id and one URL.
  • [ ] Tests, table-driven over tools, in src/server.test.ts: for every tool, a call with a wrong type, a call with an unknown argument and a call missing a required argument are each refused with the field named; and a good call still reaches the action and still works (so the door has not become a second rules engine).
  • [ ] Tests in src/actions.test.ts for each rejected id spelling, including that /api/tables/1e3 is a 404 while /api/tables/1000 is the table.
  • [ ] AGENTS.md gains one line under The shape: a door may refuse a shape it advertises; a rule about the game still lives in the action.

Files src/schema.ts (new), src/schema.test.ts (new), src/mcp.ts, src/actions.ts, src/server.test.ts, src/actions.test.ts, AGENTS.md.

Risks The checker must stay a shape-checker: no defaults invented here, no coercion, no game rule — the action still validates everything it always did, and the "a good call still reaches the action" test is what keeps that honest. Refusing unknown arguments is deliberate: a client that sends extras is a client that thinks it asked for something.

Brief that fits @mason to build, @sentinel to review the boundary. (A person assigns.)

commented

Declined: This issue is a build task — implement src/schema.ts, wire check() into rpc's tools/call, tighten find()/whole() in src/actions.ts, and write the accompanying tests — and the issue itself assigns it to @mason to build with @sentinel to review the boundary. I'm Juniper: I review committed or previewed work and write anchored comments, I don't write application code or push commits. A person should route this to @mason.

commented

Parked by the crew: the run ended without a pull: declined: This issue is a build task — implement src/schema.ts, wire check() into rpc's tools/call, tighten find()/whole() in src/actions.ts, and write the accompanying tests — and the issue itsel

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs