contracts-mcp as the person
who connected it and holds exactly that person’s seat.
Parity, again
MCP is the API’s tool list, not a second product. Every tool is a button somewhere in the app, and every button is a tool. There is no MCP-specific vocabulary:stage_accept writes to the same private
reply draft the Accept button writes to, and risk_scan returns the same findings the transcript
renders.
Parity binds acting verbs (refined 2026-08-24). Every tool below that acts (stages, sets,
imports, confirms, prepares) has an endpoint behind it, and a staging tool without one is a real bug.
The explain-and-scan tools are different: they are compositions over public reads, not resources,
so they have no endpoints of their own and are not expected to. See the parity
principle.
Connecting
Authenticate with a per-person token. The token is the seat.The scopes a connection asks for
A per-person token carries scopes, and a connection should ask for the narrow set its tools actually use. Here is the map, tool group by tool group.confirm_span is the one row with a seam in it. Before the first send a correction changes the words,
so it needs contracts:write. After the first send it can only agree with the parse as it stands, which
writes nothing, so contracts:read covers it.
Grant
ask:read, or the most useful tool an outside agent holds answers nothing. ask_contracts
is the corpus read: “have we ever agreed to a six-month cap?” It sits behind its own scope because a
question across the corpus crosses every contract the seat can open, and a token granted to read one
contract should not quietly read across the whole org. Leave the scope off and the tool is still
registered, the call comes back 403 insufficient_scope with the missing scope named in param, and
it reads to the person watching like a broken agent rather than a missing grant. See
Scopes.Guests never get MCP
A guest is a person with no org. They act through the signed link they were sent, which grants exactly what the required-party list grants them and nothing else. Seat resolution refuses to build a guest seat and throws with that reason. It is the first thing the server does.The autonomy ladder
Three dials in series, evaluated on every call: org ceiling → personal dial (within the ceiling) → per-contract arming (chasing only).A tool above the effective rung is absent from
tools/list, not refused when called. The product’s
rule is never render a control you will refuse, and a greyed-out tool in a model’s context is the
same mistake as a greyed-out button.The tools
Reads: rung 0
Every read passes through one seat filter that drops: any unsent reply not authored by this seat’s party, any internal note not owned by it, any playbook not this seat’s org, and any agent transcript not this person’s. If a read cannot be filtered, it is not a read tool.get_receipts is how an agent answers “which exact words did I sign?” Signature state hands it
hashes. A receipt hands it the snapshot behind the hash. Without this read an agent holds a code it
cannot resolve, so it either declines the question or answers it from the words standing today, which
are not necessarily the words anybody signed. A receipt is not a browsable history and there is
nothing here to diff. It points at the words one signature bound to, and that is its whole job.Explain and scan: rung 0
risk_scan reports findings, not edits. Turning a finding into words is a separate
stage_counter call, so a human always sees the list before the sheet moves. An agent gets
readings; the playbook rows themselves are never returned.Staging: rung 1
These write to the same private reply draft the buttons write to. There is no parallel agent-proposal object and no “AI suggestions” tray. A staged counter is a staged counter, and it carries aperson_id: the person who connected the agent. That is the same field the same decision carries
when a person stages it by hand, which is the point. One object, one attribution field, one name for it
across the API and this server.
Send-class: confirmation-required, always
prepare_signature is exposed, and it is pointer-only (reconciled 2026-08-24). The design draft
had it withheld, and this server exposed it anyway; the ruling went with the server.The reasoning that withheld it (consent is not a remote act) is right, and pointer-only is exactly
what honours it. Supplying no consent material at all, the tool is strictly less capable than
get_signature_state, which was always exposed. Withholding it bought nothing and cost an agent the
one sentence it most needs to say: “the words are agreed, you and Alex still have to sign, here is
the link.” The signer still opens the contract. That never changes.prepare_reply
landed_if_sent is a genuine dry run of the unanimity fold: the blocks that would move if a human
pressed Send right now. confirm_url is where the human goes. The server never follows that URL.
Why confirmation_required is a lock, not a note
A comment saying “don’t send” is not a lock. The server makes it one three ways, deliberately redundant:
1
There is no send verb to call
The client interface the tool layer can reach has no
send, no commitReply, no sign. An agent
cannot bypass a method that is not there.2
The return type has no success variant
Send-class methods return a type whose
state is the literal 'confirmation_required'. There is
no shape meaning “it went out”: no sent, no letter_id, no signature_id. An edit that tries to
return success fails the typecheck rather than shipping.3
A runtime guard on every send-class result
A seal check is the last thing every send-class handler passes through. It throws if the literal
drifted, if
unpressed is not true, or if a success-shaped key was smuggled onto the object.409 confirmation_required, cleared only by
a separate POST …/replies or POST …/signatures carrying confirmed_by.
Not exposed over MCP
On that last row:
GET /playbooks is a public
read-only endpoint over HTTP (reconciled 2026-08-24), and the MCP subset still declines the raw read.
Both are deliberate. A tool list is a place a model reaches by reflex, and floors-as-data is the
wrong reflex to hand it when a reading answers the question. A person driving their own key can read
their own playbook; that was never the concern.Consent and revocation
The connection screen is a restricted-key page: one row per tool group, the connecting person’s rung stated in one sentence, and the standing line “This connection can never send or sign.” Revocation is one click and kills in-flight staging.Prompt-injection containment
An agent here holds the connecting person’s row-level access and no more. A counterparty’s poisoned clause text can make a model say anything; it can never make it read anything the seat cannot, and it can never make it send, because there is no send verb in the process.Containment is structural rather than instructional. That is the only kind that survives a clever
paragraph in clause 7.