# MCP setup

Quattrino ships an official **Model Context Protocol** server. Any MCP client (Claude Desktop, Cursor, Windsurf, the
official `mcp` Python / TypeScript client libraries, `mcp-remote`) gets `authorize`, `authorization_outcome`,
`authorization_explain`, `compile_intent` and 36 more as native tools - thin adapters over the same authority engine
as the REST API. Nothing an MCP tool does can bypass a policy, an envelope, a delegation ceiling or an approval.

| | |
|---|---|
| URL | `https://quattrino.io/api/mcp/` (trailing slash) |
| transport | **streamable HTTP**, stateless, JSON responses |
| auth | HTTP header `Authorization: Bearer qtrn_sk_key_...` on **every** request (an agent API key - see Golden Path step 2). Keys are never tool arguments. |
| discovery (no auth) | `GET https://quattrino.io/.well-known/mcp.json` · `GET https://quattrino.io/api/v1/mcp/directory` (all tools with JSON input schemas, grouped, `start_here`) |
| environment | TEST/SANDBOX - no real money moves |

A plain `GET /api/mcp/` without MCP `Accept` headers returns `406` - that is the protocol, not an outage.

## 1. Get an agent key

Golden Path step 2: `python bootstrap_sandbox.py --signup` -> `.quattrino_sandbox.json` -> `api_key`, or in the web app
*Agents -> Issue key*. The key must belong to an agent with an ACTIVE standing policy and a sandbox wallet.

## 2. Configure your client

**Claude Desktop / Cursor / any config-file client** (`GET /api/v1/public/examples/mcp_config.json`):

```json
{
  "mcpServers": {
    "quattrino": {
      "url": "https://quattrino.io/api/mcp/",
      "transport": "streamable-http",
      "headers": { "Authorization": "Bearer qtrn_sk_key_..." }
    }
  }
}
```

Clients that only speak stdio can bridge: `npx mcp-remote https://quattrino.io/api/mcp/ --header "Authorization: Bearer qtrn_sk_key_..."`.

**Official Python client** (`pip install mcp`):

```python
import asyncio, json
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client

async def main():
    async with streamablehttp_client("https://quattrino.io/api/mcp/",
                                     headers={"Authorization": "Bearer qtrn_sk_key_..."}) as (read, write, _):
        async with ClientSession(read, write) as s:
            await s.initialize()
            tools = await s.list_tools()                                  # 40 tools
            me = await s.call_tool("whoami", {})
            r = await s.call_tool("authorize", {"amount_minor": 1999, "merchant": "Amazon", "purpose": "USB-C cable",
                                                 "commitment": "ONE_TIME", "task": {"max_amount_minor": 2000}})
            print(json.loads(r.content[0].text)["decision"])                # AUTHORIZED
asyncio.run(main())
```

**Official TypeScript client** (`npm i @modelcontextprotocol/sdk`):

```ts
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
const transport = new StreamableHTTPClientTransport(new URL("https://quattrino.io/api/mcp/"),
  { requestInit: { headers: { Authorization: "Bearer qtrn_sk_key_..." } } });
const client = new Client({ name: "my-agent", version: "1.0.0" });
await client.connect(transport);
const r = await client.callTool({ name: "authorize", arguments: { amount_minor: 1999, merchant: "Amazon", purpose: "USB-C cable", commitment: "ONE_TIME", task: { max_amount_minor: 2000 } } });
```

## 3. First six tool calls (the Golden Path in MCP)

| # | tool | arguments | expect |
|---|---|---|---|
| 1 | `whoami` | `{}` | `agent_id`, `organization`, `environment: sandbox`, `scopes` |
| 2 | `authority_discover` | `{}` | `allowed[]`, `conditional[]`, `denied[]`, `effective.per_action_maximum_minor: 5000`, `autonomous_maximum_minor: 2000` (these two mirror the owner's standing policy - `transaction_limit` / `transaction_auto_limit`; a `0` means the owner asks first for everything) |
| 3 | `authorize` | `{amount_minor: 1999, merchant: "Amazon", purpose: "USB-C cable", commitment: "ONE_TIME", task: {max_amount_minor: 2000}}` | `decision: AUTHORIZED`, `effective_maximum_minor: 2000` |
| 3b | `authorize` | `{amount_minor: 2500, ...}` / `{amount_minor: 7500, ...}` / `{merchant: "Amazon", purpose: "cable", commitment: "ONE_TIME"}` | `APPROVAL_REQUIRED` / `DENIED` / `NEEDS_INFORMATION` |
| 4 | `authorize` | `{mode: "binding", idempotency_key: "order-1001", execution: "external", proof_audience: "amazon.example", amount_minor: 1999, merchant: "Amazon", purpose: "USB-C cable", commitment: "ONE_TIME", task: {max_amount_minor: 2000}}` | `status: AUTHORIZED`, `authorization_id`, `portable_proof.token` (bound to `proof_audience`, same field as REST) |
| 4b | `verify_authorization_proof` | `{proof: <portable_proof.token>, audience: "amazon.example"}` | `valid: true`, `status: VALID` (a different `audience` -> `proof_audience_mismatch`) |
| 5 | `authorization_outcome` | `{authorization_id, success: true, actual_amount_minor: 1999, provider: "amazon", provider_reference: "114-2233"}` | `status: CONSUMED`, `compliance.status: COMPLIANT` |
| 6 | `authorization_explain` | `{authorization_id}` | `plain_language` |

In MCP the idempotency key is the **`idempotency_key` argument** (8+ chars, stable per unit of work) - there is no header.
A binding call without it returns the tool error `idempotency_key_required: mode='binding' needs the idempotency_key argument ...`.

## Tool groups (from `GET /api/v1/mcp/directory`)

| group | tools | when |
|---|---|---|
| `core_authorize` | `whoami`, `authority_discover`, `authorize`, `authorization_status`, `authorization_outcome`, `authorization_explain` | every integration |
| `intent_and_envelopes` | `compile_intent`, `authority_gap`, `envelope_propose_change`, `disclosure_release` | plain-language tasks, partial progress, purpose-bound disclosure |
| `portable_proof` | `authorization_proof`, `verify_authorization_proof`, `reason_codes` | executors / counterparties |
| `execution_adapters_sandbox` | `wallet_status`, `request_transaction`, `purchase_prepare`, `merchant_quote`, `merchant_purchase`, `check_approval`, ... | only when Quattrino executes in the sandbox |
| `marketplace_buyer` | `marketplace_search`, `get_service_quote`, `purchase_service`, `marketplace_get_trust_profile`, `negotiate_service_price`, `get_purchase_result` | agent-to-agent services (Credits) |
| `recurring_authorities` | `authority_charge`, `recurring_authority_get`, `recurring_authority_cancel` | recurring purchase authorities |
| `seller_operator` | `list_jobs`, `complete_job`, `fail_job` | you sell services to other agents |

Authorize-only integrations need only `core_authorize` (+ `intent_and_envelopes`, `portable_proof`).

## Errors

MCP tool errors carry the REST error code as a prefix in the text:

| text starts with | meaning |
|---|---|
| `unauthorized: pass your agent API credential as an 'Authorization: Bearer qtrn_sk_...' HTTP header ...` | header missing |
| `unauthorized: Invalid API credential` | wrong / revoked key |
| `forbidden: this credential is missing the 'approvals.read' scope ...` | scope |
| `rate_limited: ...` | 120 requests / minute per credential |
| `idempotency_key_required: ...` | binding without `idempotency_key` |

Decisions (`DENIED`, `APPROVAL_REQUIRED`, `NEEDS_INFORMATION`) are **results**, not errors - read `decision`.

## Server instructions your model will see

> Official Quattrino tools - authority infrastructure for AI agents. ... START WITH: whoami -> authority_discover -> authorize
> (dry_run) -> authorize mode=binding + idempotency_key + execution=external -> you act -> authorization_outcome ->
> authorization_explain. APPROVAL_REQUIRED: relay the question to your human and poll authorization_status; never
> re-submit. NEEDS_INFORMATION: supply the missing term or relay the ONE question; never guess. DENIED is final ...

## Limits

- Stateless server: no sessions to resume; every call re-authenticates the header.
- Same rate limits and scopes as REST (`transactions.request` for `authorize`, `approvals.read` for polling).
- The reported `serverInfo.version` is the MCP framework version, not the Quattrino contract version (API `v1`, SDK `0.31.0`).
