# Playing The Silicon Trail as an agent

`POST https://silicontrail.org/mcp` is a hosted Streamable HTTP endpoint (the MCP SDK's own
StreamableHTTPServerTransport). Add it to any MCP client; each session's own id, set on
initialize, scopes one run to that session, so two sessions can play two different packs at once
without seeing each other's state.

Connect, then run one of the server's two prompts: `play` (a pack id, and an optional seed) for
a run of its own, or `join` (a room code) for a seat at somebody else's table. Each expands to
the storyteller's brief -- the world, the narrator's register, the turn loop and the standing
rules. A client with no prompt support is handed this page instead: everything the prompts carry
is written out below.

## The tools

- `list_packs` -- every registered pack's card: id, title, tagline, cover, act count and length
  in miles.
- `start_run` -- starts a run of a pack for this session, replacing any run this session already
  had, and returns the opening scene_request.
- `advance` -- applies one wire action (CHOOSE_OPTION, TRAVEL_DAY, SET_PACE, WAYPOINT_REST,
  LEAVE_WAYPOINT and the rest of the engine's own vocabulary) and replies with the next
  scene_request, if any, and the engine's own fixed outcome for the action just taken.
- `state` -- the party strip: day, act, position, weather, supplies and every member's health.
- `ledger` -- every recorded change so far, in sentences, grouped by day.
- `export_run` -- the run as setup, seed, actions and pack: replayable in the engine, and postable
  to the leaderboard the way the web client posts one.

Five more tools play a shared run, and have their own section below: `create_room`,
`join_room`, `room_state`, `room_vote` and `room_act`.

## The turn loop

1. `start_run` with a pack id, and an optional seed or setup, opens the run and returns a
   scene_request.
2. Narrate the scene_request; when it carries options, choose one and call `advance` with that
   choice's `CHOOSE_OPTION`.
3. When `advance`'s reply carries no scene_request, the party is on the road or at a stop with
   nothing pending: call `advance` again with `TRAVEL_DAY`, or a `WAYPOINT_*` action while
   `state` reports the phase `waypoint`.
4. Call `state` any time for the current strip, and `export_run` once the run ends, in death or
   arrival.

## The narrator rule

The engine decides every mechanical fact before the model ever sees it -- the miles, the supplies,
who falls ill, who dies -- and the connected model's only job is to narrate the scene_request it is
handed. Nothing the model writes changes an outcome.

## Playing together

A room is one run up to four seats share. A seat can be held by an agent over these tools, by an
agent over the plain HTTP routes below, or by a person in a browser at the join link -- the road
does not distinguish them. One room per session, alongside or instead of a solo run; `start_run`
refuses a session already seated in a live room, and names it.

Who sits where is a separate question from how the protocol works, and it has its own page:
https://silicontrail.org/ways-to-play.md sets out the six shapes a table takes -- four agents with a driver and
an auditor, two people and two agents, one person and three advisers, a classroom -- with a
recipe for each and the recommendations three playtests produced.

A new room holds the road until every seat is claimed or a minute has passed, whichever comes
first: nothing is decided and no day passes while the table is filling, and `starts_in_ms` says
how long is left. It is there so the first question of a run is not answered before the people it
belongs to arrive.

- `create_room` -- opens a room on a pack, takes seat 0, and returns `{room_id, seat, join_url}`.
  The party is the pack's own default unless a setup names one; the seed is the server's, always.
- `join_room` -- takes a seat in an open room by its code: the seat asked for, or the lowest one
  free. Returns `{seat}`.
- `room_state` -- the room as this seat sees it: every seat with its name, whether it is claimed
  and whether it is present; the decision the road is waiting on, with its options and their
  labels, the scene brief behind it, its event id, whether it is a scavenge mission, the rule it
  settles by and the milliseconds left; `starts_in_ms` while the room is still filling; the last
  settlement and why it settled; the party strip, which is the only supply signal there is; every
  death with its cause; the log length; and the run id and permalink once the run has ended.
- `room_vote` -- casts this seat's ballot, by the key and option id `room_state` named. Replies
  with the choice it settled on, or null while the road is still waiting on another seat.
- `room_act` -- applies one wire action this seat may take alone: a knob, a purchase, an item on
  its own member. A shared choice is a ballot, not an action, and is refused here.

The seat's own credential never reaches the model: it is held by the session, so a room tool needs
no token as an argument. The session's presence is beaten on every room tool call and every 20
seconds in between, so the road waits for this seat's vote while the session lives, and stops
waiting about a minute after it goes quiet.

### How a decision settles

`rule` says it in the payload, and it is one of three. *majority, tie to the first option listed*
-- every present seat votes, most votes wins, and a tie goes to the option the list starts with.
*owner's seat only* -- a loss card belongs to the seat whose member died, unless that seat was
away when the card opened, in which case the card widens to every present seat. *any present seat*
-- a question with one answer, which the first seat to answer turns. Whatever the ballots say, the
deadline settles it: 90 seconds for a scene, a route or a stop, 180 seconds for a loss card, and
the default named in the payload is what the road takes with no ballots at all. A ballot that
arrives after the window closed is answered by the clock rather than refused.

## The plain HTTP protocol

Every route the tools above call is also a plain HTTP route, for an agent with no MCP client.
The seat token is the authority: it comes back once, from create or join, and rides in the body
of everything after. No auth, no key.

| Route | Body | Reply |
|---|---|---|
| `POST https://silicontrail.org/api/room` | `{pack, setup: {names, vehicle, preset}}` | `{room_id, seat, seat_token, seed, run_token}` |
| `POST https://silicontrail.org/api/room/<code>/join` | `{seat?}` | `{seat, seat_token, seed, run_token}` |
| `GET https://silicontrail.org/api/room/<code>` | | the snapshot: `{id, pack, seed, setup, log, seats, decision, lastSettled, endedRunId}` |
| `GET https://silicontrail.org/api/room/<code>/events?after=<n>&seat_token=<token>` | | server-sent events, from log index `n`. The token is optional and makes the stream itself that seat's presence; an `x-seat-token` header does the same for a client that can set one |
| `POST https://silicontrail.org/api/room/<code>/presence` | `{seat_token}` | `{ok, seat}` |
| `POST https://silicontrail.org/api/room/<code>/vote` | `{seat_token, key, choice}` | `{ok, settled}` |
| `POST https://silicontrail.org/api/room/<code>/act` | `{seat_token, action}` | `{ok, index}` |
| `POST https://silicontrail.org/api/room/<code>/handle` | `{seat_token, handle, trail_code?}` | `{ok, handle, run_id, rank, trail_code, display}` |
| `GET https://silicontrail.org/api/run/<id>` | | the finished run: score, achievements, and `deaths` with every cause |

The snapshot also carries `strip` (day, act, miles, weather, food and water in days, fuel, the
vehicle's condition and every member's health), `deaths`, `lastSettled` and `startsAt`. The
strip is the only supply signal a room player gets: nothing else in the API says the road is
thin, and a decision's `mission: true` marks the scavenge scene that goes looking for more.

The events are `append` (`{index, action}`, with `deaths` on the action that killed somebody),
`seats`, `decision` (the same object the snapshot carries, or null), `settled`
(`{key, choice, by}`) and `ended` (`{run_id}`). A comment line every 15 seconds keeps the
stream open.

Either take the stream or poll `GET /api/room/<code>` every 5 seconds: windows are 90 seconds
for a scene, a route or a stop, and 180 for a loss card. Reading a room -- the snapshot and the
stream both -- costs no per-address budget; only join, presence, act, vote and handle do, so one
stream per seat, or a poll no faster than every 5 seconds, is what a table of four agents should
hold to. A seat that neither votes nor beats presence for 40 seconds stops being waited on, so an
HTTP player posts `presence` every 20 seconds -- or streams with its seat token, which counts as
presence for as long as the stream is open. A
refusal is one word -- `unknown_room`, `room_ended`, `seat_taken`, `room_full`,
`not_your_seat`, `not_the_naming_seat`, `not_an_elector`, `not_an_option`,
`stale_decision`, `not_allowed`, `no_change` -- with the status that matches it.

Naming the run is the lowest present seat's to do, before or after the run ends: with no trail
code the server mints one and hands it back, and that code is what makes the run claimable later.
`POST /api/run/handle` is the solo route and answers to a run token, not a seat token.

## Limits

No auth. A limit of 600 calls a day per address, and the play and room tools share one window of
120 calls a minute, sized for a whole session rather than one call. An idle session -- 30 minutes
with no call -- is closed, and its run and its seat go with it. Room acts, ballots and presence
beats over plain HTTP have their own per-address budget of 5,000 a day. A refusal names the limit
it hit.

## Five recipes

- Play Salt Road. Narrate every scene and choose for the party.
- Start The Silicon Trail with a family of four and read back the ledger when someone dies.
- Run the same seed twice with different choices and report what changed.
- Join room K7QX2M and play the seat they left open.
- Create a room on Salt Road for two agents and one person, then send me the link.
