> ## Documentation Index
> Fetch the complete documentation index at: https://docs.contracts.io/llms.txt
> Use this file to discover all available pages before exploring further.

# MCP

> The contracts.io MCP server: the same verbs, one seat down. It can never send and never sign.

<Warning>
  **Draft — ahead of the backend; nothing here is live yet.** This surface is
  written against the design, not against a running server. There is no
  `api.contracts.io` to call, no key to issue, and every response below is an
  illustration rather than a recording. It is published so the shape can be argued
  with before it is built.

  It also predates **CONTRACT-SCHEMA v0** and still uses the older nouns — *ask* for
  Proposal, *receipt* for Version, *relationship* for Room. The
  [glossary](/glossary) maps them.
</Warning>

An external agent (someone's Claude, a Slack bot, an ATS) connects to `contracts-mcp` **as the person
who connected it** and holds exactly that person's seat.

<Warning>
  **This connection can never send or sign.**

  That is the standing line on the consent screen, and it is in the server's MCP `instructions` on every
  connection, so the model is told before it plans rather than after it tries.
</Warning>

## 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.

<Note>
  **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](/api/introduction#the-parity-principle).
</Note>

The server is a **client of the public API**. It does not get a privileged connection, an in-process
database handle, or a service role. If an agent needs a datum, the API grows a verb that everyone gets.

## Connecting

Authenticate with a **per-person token**. The token is the seat.

```
Authorization: Bearer <per-person token>
Contracts-Version: 2026-08-24
```

<Warning>
  Do **not** authenticate this server with an org key (`sk_live_…`). An org key must name the human it
  acts for on every write, and MCP already has one: the person who connected it.
</Warning>

### 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.

| Tool group                                                                                                                                     | Scope it needs                                                                             |
| ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| Reads, explain and scan                                                                                                                        | `contracts:read`                                                                           |
| `ask_contracts`                                                                                                                                | `ask:read`, on top of `contracts:read`                                                     |
| `stage_accept`, `stage_counter`, `stage_decline`, `stage_fact_change`, `apply_playbook`, `withdraw_decision`, `withdraw_ask`, `set_reply_note` | `decisions:write`                                                                          |
| `create_contract`, `set_fact`, `confirm_span` (before the first send)                                                                          | `contracts:write`                                                                          |
| `list_contracts`, `get_receipts`, `confirm_fact`, `confirm_span` (after the first send)                                                        | `contracts:read`                                                                           |
| `import_document`                                                                                                                              | `imports:write`                                                                            |
| `prepare_send`, `prepare_reply`, `prepare_signature`                                                                                           | Nothing beyond the reads they compose. They assemble a sheet; a person presses the button. |

`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.

<Warning>
  **`send` and `sign` are never requested by an MCP connection.** Both are real, grantable scopes. This
  connection does not ask for either one, because there is no send verb and no sign verb behind the tool
  list to use them. A consent screen that offered them would be describing a different product.
</Warning>

<Note>
  **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](/api/authentication#scopes).
</Note>

### 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).

| Rung                      | Which tools appear                                                                                                           | Set where                           |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- |
| **0 · Reads**             | The read, explain and scan tools, plus `prepare_signature`, which is pointer-only, so it reads like a read.                  | always on                           |
| **1 · Drafts on ask**     | ＋ the staging tools, ＋ `prepare_send`, `prepare_reply`. **The default for every person who can hold an MCP connection.**     | opt-in dial, within the org ceiling |
| **2 · Drafts on arrival** | Same tools, fired on `letter.received`, and the agent says it pre-staged. No new tools.                                      | opt-in                              |
| **3 · Chases**            | `arm_chase`, `send_nudge`: **not exposed over MCP at all.** Nudges go out in a human's name; arming happens on the contract. | opt-in, armed per contract          |
| **none**                  | Send / sign                                                                                                                  | **never exists**                    |

<Note>
  **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.
</Note>

Connect at rung 0 and the list visibly shrinks. The staging and send-class tools are simply not
registered for that turn.

## 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.

| Tool                  | Returns                                                                                                                                                                                                                                                                                                                                                           |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `get_contract`        | The sheet as this seat sees it: `state`, `blocks[]`, `facts[]`, `parties[]`, `words_hash`, `ready`. Clause text is the fold, never a stored string.                                                                                                                                                                                                               |
| `get_letter`          | One committed round: `from`, `note`, `asks[]`, `sent_at`. Immutable.                                                                                                                                                                                                                                                                                              |
| `list_asks`           | Live proposals with `standing`, `proposed`, `why`, per-party `tally`, and `decided_by_me` from **your** unsent round.                                                                                                                                                                                                                                             |
| `get_facts`           | The audit view: `name`, `value`, `inferred`, `occurrences[]`.                                                                                                                                                                                                                                                                                                     |
| `list_parties`        | The required-party list. **Public capacity only**, never org authority.                                                                                                                                                                                                                                                                                           |
| `get_relationship`    | The counterparty pair: `contracts[]`, `people[]`, `assets[]`. Derived, read-only.                                                                                                                                                                                                                                                                                 |
| `get_room_state`      | Everything on one contract in a single read: letters, open asks with tallies, landed and voided asks, **who is owed a reply and since when**, your own unsent round, signature states. → [`GET /contracts/{id}/state`](/api/contracts#read-the-whole-state-of-a-contract).                                                                                        |
| `get_signature_state` | `required_signers[]`, `held[]`, `bound[]`, `void[]`, `withdrawn[]`, `words_hash`, `stale`. **All four signature states** (reconciled 2026-08-24; `void[]` and `withdrawn[]` were missing here). `required_signers[]` is people, not party ids, and each row carries that person's signature and its state, so *"who still has to sign"* is one read and no joins. |
| `get_receipts`        | The receipts on this contract: `hash`, `snapshot_at`, `signed_by[]`. Rung 0. → [`GET /contracts/{id}/receipts`](/api/signing#receipts).                                                                                                                                                                                                                           |
| `list_contracts`      | The index this seat can open, filterable by `state`, `origin`, `relationship` and `updated_since`. How an agent answers *"list the agreements we imported"* or *"what has changed since Friday"* without walking the attention feed. → [`GET /contracts`](/api/contracts#list-contracts).                                                                         |
| `get_attention`       | The Monday door: every contract waiting on this person, why, and how much is staged but unsent. Grouped by relationship, ordered by urgency.                                                                                                                                                                                                                      |

<Note>
  **`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.
</Note>

### Explain and scan: rung 0

| Tool                    | Returns                                                                                                                                                                                                                  |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `explain_clause`        | Prose in the seat's voice, citing the standing words. Stages nothing.                                                                                                                                                    |
| `summarize_letter`      | The first-open line, *"Priya asks three things; two are inside your playbook"*, plus `asks_grouped[]`, `inside_playbook[]`, `outside[]`.                                                                                 |
| `what_changed`          | `blocks_landed[]`, `asks_open[]`, `signatures_voided[]`. The catch-up read.                                                                                                                                              |
| `risk_scan`             | Clause-by-clause against your own playbook, every block visited, **findings ordered by consequence not by diff size.** Returns findings, **not edits**.                                                                  |
| `check_ready`           | `{ ready, blocking[] }`, computed from public state only.                                                                                                                                                                |
| `ask_contracts`         | English question over the corpus; `{ answer, citations[], matched, examined, searched }`. Any number you show a person comes from `matched` or from counting citations, never from the answer text. See [Ask](/api/ask). |
| `list_unverified_spans` | The blocks an import was least sure of, each pointing at the original.                                                                                                                                                   |

<Note>
  `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.
</Note>

### 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 a
**`person_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.

| Tool                | Notes                                                                                                                                                                                                                                                                                                |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `stage_accept`      | A staged decision on an ask.                                                                                                                                                                                                                                                                         |
| `stage_counter`     | **`why` is required.** A counter without a reason is a fight.                                                                                                                                                                                                                                        |
| `stage_decline`     | Implemented as a counter proposing the standing words. One mechanic, not two.                                                                                                                                                                                                                        |
| `stage_fact_change` | One grouped ask across every clause the fact touches: *"1 ask · touches 3 clauses"*.                                                                                                                                                                                                                 |
| `apply_playbook`    | `{ staged[], skipped[] }`. **Everything outside the playbook is skipped and named, with its reason.** Nothing is ever accepted by omission. → [`POST /contracts/{id}/decisions/apply-playbook`](/api/replies#apply-your-playbook-to-a-whole-letter).                                                 |
| `withdraw_decision` | The Undo the margin already offers. Removes something from **your unsent round**, which never left your side.                                                                                                                                                                                        |
| `withdraw_ask`      | Pull back a proposal your side **already sent**, so the other side no longer has to answer it. Your own side's only: to answer theirs, decide on it.                                                                                                                                                 |
| `confirm_fact`      | Agree that a fact a parse pulled out of a document is right, clearing its `inferred` mark. Writes no words and stages nothing, so it works before or after a send. The other half of the pair whose blocks half is `confirm_span`.                                                                   |
| `set_reply_note`    | The covering sentence on the round. → [`PATCH /contracts/{id}/reply`](/api/replies#set-the-reply-note).                                                                                                                                                                                              |
| `create_contract`   | Origination. Returns `questions[]` (≤3, only genuine forks) and `origin: "mcp"`. **Standardized from `draft_contract`** (reconciled 2026-08-24): one verb, one name across the harness, this server and these docs. `draft_contract` survives only as an undocumented alias in the running scaffold. |
| `set_fact`          | Applies before first Send; stages after.                                                                                                                                                                                                                                                             |
| `import_document`   | Parse to a contract, returning **two** verification surfaces: the facts the parse guessed at, and the blocks it was least sure of. The blocks come back as two fields, not one: `unverified_count`, an integer, and `unverified`, the list of block ids.                                             |
| `confirm_span`      | Agree with the parse, or correct it before the first send. See the warning below for what happens after.                                                                                                                                                                                             |

<Warning>
  **After the first send, `confirm_span` does not stage anything.** Earlier drafts of this page said it
  staged a counter. It does not, and it never did.

  Before the first send nobody else holds the words, so a correction applies straight to them, and the
  call needs `contracts:write`. After the first send the other side is holding the same words, so this call
  can only agree that the parse read them correctly. Sending `text` returns `400 invalid_request`, and
  the scope stays `contracts:read`, because agreeing with words that are already there writes nothing at
  all. It stages no decision, opens no question, and needs no `decisions:write`.

  **To change a word after the first send, stage a decision:** `POST /contracts/{id}/decisions` with a
  `block_id`, over MCP `stage_counter` with a `block_id`. Every word change is a question the other side
  gets to answer, and this one is no exception.
</Warning>

<Warning>
  **Every staging tool must produce a visible mark on the sheet within the same turn**: a redline, a
  staged label, the reply bar counting up. A tool whose only output is chat text is a tool that made a
  second record.
</Warning>

### Send-class: confirmation-required, always

| Tool                | Returns                                                                                                                                                      |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `prepare_send`      | `confirmation_required` ＋ the addressed Send sheet, unpressed.                                                                                               |
| `prepare_reply`     | `confirmation_required` ＋ the assembled round exactly as the counterparty would receive it, plus `landed_if_sent[]`.                                         |
| `prepare_signature` | `confirmation_required` ＋ `block_awake`, `required_signers[]`, `confirm_url`. **Pointer-only: never a name, never an intent, never a hash to bind.** Rung 0. |

<Note>
  **`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.
</Note>

```jsonc prepare_reply theme={null}
{
  "state": "confirmation_required",
  "unpressed": true,
  "confirm_url": "https://contracts.io/c/con_1?confirm=reply",
  "confirm": "1 decision staged, not sent. Open the contract and press Send.",
  "note": "This connection can never send or sign. A person must confirm this in the app.",
  "contract": "con_1",
  "words_hash": "sha256:9f2c...",
  "letter_preview": { "from": "...", "to": ["..."], "note": "", "decisions": [ /* ... */ ] },
  "decisions": 1,
  "landed_if_sent": []
}
```

`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.**

<Warning>
  **A harness that retries a `confirmation_required` result is a bug, not a fallback.** It is not a rate
  limit and not a transient failure. It is the end of the road. The next actor is a person.
</Warning>

## 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:

<Steps>
  <Step title="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.
  </Step>

  <Step title="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.
  </Step>

  <Step title="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.
  </Step>
</Steps>

Over HTTP the same wall is [`409 confirmation_required`](/api/errors#confirmation-required), cleared only by
a separate `POST …/replies` or `POST …/signatures` carrying `confirmed_by`.

## Not exposed over MCP

| Tool                         | Why not                                                                                |
| ---------------------------- | -------------------------------------------------------------------------------------- |
| `discard_reply`              | Destructive to a human's private draft, in a place they cannot see.                    |
| `read_asset`, `get_thread`   | Asset provenance and internal-adjacent talk need their own design pass.                |
| `arm_chase`, `send_nudge`    | Nudges go out in a human's name; arming happens on the contract.                       |
| `get_playbook` as a raw read | An agent gets *readings* from `risk_scan`, not the org's floors as data to exfiltrate. |

<Note>
  On that last row: [`GET /playbooks`](/api/replies#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.
</Note>

## 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.

<Note>
  Containment is **structural rather than instructional**. That is the only kind that survives a clever
  paragraph in clause 7.
</Note>
