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
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.
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.tspublishes a JSON Schema for every tool and checks none of it.Measured on
maintoday:tools/call open_table {"title": 123}answers200with a table named "123", because the action coerces withString(...); 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; andGET /api/tables/1e3and/api/tables/1.0name tables 1000 and 1, becausefind()insrc/actions.tsparses an id with bareNumber(). 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: objectwithpropertiesandrequired, and per propertyinteger/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.seats; open_table takes title, seats_max, turn_seconds, deadline_seconds."rpc'stools/callrunscheckbefore it calls the action and reports a failure the way the app already reports one —isError: truewith one sentence a model can act on, naming the argument and what was expected — never a crash, never a stack, never a 500.find()andwhole()insrc/actions.tsaccept only a plain decimal integer: a string matching/^[0-9]+$/or a number that is a safe integer.1e3,1.0," 1","0x1","+1"andInfinityall becomenot_found/invalidrather than naming a table nobody asked for. One table has one id and one URL.tools, insrc/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).src/actions.test.tsfor each rejected id spelling, including that/api/tables/1e3is a 404 while/api/tables/1000is the table.AGENTS.mdgains 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.)