Autonomy Ladder

The MCP contract for the Gateway

The Gateway MCP server (repo belay, built separately) answers ask_autonomy_ladder from a stand-in ledger today. To answer from the live Ladder it calls this Worker's MCP endpoint. Nothing in belay was changed by this repo.

Connection

EndpointPOST https://ladder.ankitu.com/mcp
TransportMCP Streamable HTTP, stateless: each POST carries one JSON-RPC 2.0 message (or a batch) and gets the answer as application/json. No session id, no SSE. GET /mcp answers 405.
Protocol version2025-06-18 (returned by initialize)
AuthAuthorization: Bearer <workspace key>. One key = one workspace. The Gateway keeps it as its own secret (for example LADDER_KEY_VANTROW), never in code. Without a valid key: HTTP 401 with JSON-RPC error -32001.
Outside sign-in/mcp is excluded from Cloudflare Access; the workspace key is the only guard.

Tools

which_steps_run_alone

Input: { "queue"?: string }. Output (structuredContent):

{ "workspace": "vantrow-bills", "steps": [
  { "queue": "bills", "step": "post_tms", "name": "Post to the TMS", "lane": "auto", "runs_alone": true, "money_moving": true,
    "why": "Audits in the last 2 weeks: 100.0% right on 80; the step drops back if the outcome test or the audits turn.",
    "last_4_weeks_accuracy": 0.996, "audit_rate": 0.1, "evidence": "https://ladder.ankitu.com/v1/steps/bills/post_tms/evidence" } ] }

The text content starts with one sentence, for example "Runs alone (Auto): Read the bill; Post to the TMS. 5 other steps need a person."

step_evidence

Input: { "step": string, "queue"?: string }. step may be the id (decide_hold) or the plain name (Decide hold or release); names match case- and punctuation-insensitively, then by containment. Output: the evidence pack (lane, why, weekly figures with estimated accuracy, Wilson 95% lower bounds, rolling 2- and 4-week windows, operator error rate, every lane change with its rule and numbers, the outcome test's state, recent disagreements and audits). Unknown step: isError: true with a sentence suggesting which_steps_run_alone.

route_item

Input: { "step": string, "item": string, "queue"?: string, "features"?: object }. Output: the same answer as POST /v1/route (decision = act_alone | propose | human_only, lane, audit, audit_rate, reason, route_id). The call is logged like any route call.

How ask_autonomy_ladder maps onto this

ask_autonomy_ladder(step) takes one step name. The Gateway should:

  1. call step_evidence with { "step": <name> } on the workspace whose queue holds that step (bills: key for vantrow-bills; check calls: key for checkcall-loads; the real IIA gate: key for iia-gate);
  2. answer with lane, why, and figures.rolling_4.accuracy ("how often the agent agreed with the person over the last four weeks") from the result;
  3. on isError, call which_steps_run_alone and offer the nearest names.

The stand-in's step names differ from the live ones (for example "Check the fuel surcharge" is part of "Rate check" here; "Log location and ETA" is "Capture the ETA"). The live names are:

WorkspaceSteps
vantrow-bills (queue bills)Read the bill; Match to BOL and LOA; Rate check; Decide hold or release; Draft the correction; Post to the TMS; Customer email
checkcall-loads (queue loads)Spot the quiet load; Read the driver's reply; Capture the ETA; Tag the delay reason; Decide hand-off; Update the board; Write the dispatcher note
iia-gate (queue bills)Decide hold or release (the real rightbill rule-tier gate as the agent)

Example exchange

→ {"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"step_evidence","arguments":{"step":"Decide hold or release"}}}
← {"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"Decide hold or release is in shadow. Evidence to week 14: …"}],"structuredContent":{ … }}}