# MCP Endpoint — "the format IS the API"

Every MIDL site exposes a **Model Context Protocol (MCP)** server at a single endpoint:

```
POST /api/mcp
```

MCP is the interop protocol MCP-capable clients (Claude Desktop/Code, NLWeb agents, anything built on
`@modelcontextprotocol/sdk`) already speak. Where `agent.json`, `llms.txt`, and the `.midl.json` /
`.facts.json` twins let an agent **read** a MIDL site, MCP lets an agent **drive** it: discover the
tools, read a page's document and facts, list pages, submit telemetry, and — with an API key — create
or update a page. Because a MIDL page is already a JSON document, the format *is* the API; MCP is just
the standard envelope.

- **Transport:** MCP "Streamable HTTP", **stateless** — one JSON-RPC 2.0 message per `POST`, an
  `application/json` response. No session id, no server-push, no batching.
- **Protocol version:** `2025-06-18` (older revisions are negotiated on `initialize`).
- **Auth:** read + telemetry tools are open; **`create_frame` / `update_frame` require a Bearer API
  key** — the same key you mint at `/admin/keys` for the [Write API](/docs/write-api). The page is
  created in the key's tenant.

The examples below use `https://your-site.example` as the origin — replace it with your deployment.

## Tools

| Tool | Auth | What it does |
|---|---|---|
| `list_frames` | — | List the published pages (id, slug, description, updated_at). |
| `read_document` | — | The raw MIDL document for a page + its schema.org JSON-LD + a `ui:*→schema.org` map. |
| `read_facts` | — | The RAG-indexable facts document (same as `/render/<slug>.facts.json`). |
| `submit_telemetry` | — | Record an impression/click/conversion/engagement event for the optimizer. |
| `create_frame` | **Bearer key** | Create a page from a MIDL document (validated before persist). |
| `update_frame` | **Bearer key** | Partial-update a page by id (content/slug/description/is_active). |

## 1. Discover — `initialize` then `tools/list`

Every MCP session starts with an `initialize` handshake:

```bash
curl -sS https://your-site.example/api/mcp \
  -H 'content-type: application/json' \
  -H 'accept: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize",
       "params":{"protocolVersion":"2025-06-18","capabilities":{},
                 "clientInfo":{"name":"my-agent","version":"1.0"}}}'
```

```jsonc
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2025-06-18",
    "capabilities": { "tools": { "listChanged": false } },
    "serverInfo": { "name": "midl-site-mcp", "title": "MIDL Site (MCP)", "version": "0.1.0" },
    "instructions": "Read: list_frames, read_document ... Write ... requires an Authorization: Bearer key."
  }
}
```

A well-behaved client then sends the `notifications/initialized` notification (no `id`) — the server
answers `202 Accepted` with no body. List the tools:

```bash
curl -sS https://your-site.example/api/mcp \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
```

Each tool comes back with a JSON-Schema `inputSchema` and `annotations` (e.g. `readOnlyHint`) so a
client can render and validate calls.

## 2. Read a page — `read_document`

```bash
curl -sS https://your-site.example/api/mcp \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":3,"method":"tools/call",
       "params":{"name":"read_document","arguments":{"slug":"pricing"}}}'
```

The tool result's `content[0].text` is a JSON string with the page's MIDL `document`, its schema.org
`jsonLd` (`@type: WebPage`), and the `ui:*→schema.org` overlaps present on the page:

```jsonc
{
  "jsonrpc": "2.0", "id": 3,
  "result": {
    "content": [{ "type": "text", "text": "{ \"slug\": \"pricing\", \"url\": \".../render/pricing\", \"document\": { \"midl\": \"0.1\", \"frames\": [ ... ] }, \"jsonLd\": { \"@context\": \"https://schema.org\", \"@type\": \"WebPage\", ... }, \"schemaOrg\": [ { \"ui\": \"ui:Hero\", \"schemaOrg\": \"WPHeader\" }, { \"ui\": \"ui:Image\", \"schemaOrg\": \"ImageObject\" } ] }" }]
  }
}
```

## 3. Read a page's facts — `read_facts`

```bash
curl -sS https://your-site.example/api/mcp \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":4,"method":"tools/call",
       "params":{"name":"read_facts","arguments":{"slug":"pricing","lang":"en"}}}'
```

Returns de-duplicated bullet facts extracted from the page's semantic frames, with provenance — the
same payload as `GET /render/pricing.facts.json`.

## 4. Submit telemetry — `submit_telemetry`

```bash
curl -sS https://your-site.example/api/mcp \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":5,"method":"tools/call",
       "params":{"name":"submit_telemetry",
                 "arguments":{"frame_id":"frame-123","event_type":"conversion","variant_id":"v-a"}}}'
```

A `conversion` event must carry a `variant_id` or `design_system_id` (so the optimizer can attribute
it), exactly like `POST /api/telemetry`.

## 5. Write a page (requires a key) — `create_frame`

Mint a key at `/admin/keys`, then send it as `Authorization: Bearer <key>`:

```bash
curl -sS https://your-site.example/api/mcp \
  -H 'content-type: application/json' \
  -H 'authorization: Bearer midl_xxxxxxxxxxxx_...' \
  -d '{"jsonrpc":"2.0","id":6,"method":"tools/call",
       "params":{"name":"create_frame","arguments":{
         "slug":"launch",
         "description":"Launch page",
         "content":{
           "midl":"0.1",
           "ns":{"ui":"https://midl.ai/ui#"},
           "frames":[
             {"id":"hero","type":"ui:Hero","title":"Ship faster","subtitle":"The platform for teams"},
             {"id":"cta","type":"ui:Button","text":"Start free"}
           ]
         }}}}'
```

On success the tool returns `{ "id": "frame-…", "slug": "launch", "url": ".../render/launch" }`, and the
page is live at `/render/launch`. The document is **validated before persist** — an invalid MIDL body
comes back as a tool error (`isError: true`) carrying the validation feedback, and nothing is written.

**Without the `Authorization` header**, `create_frame`/`update_frame` return a tool error with code
`unauthorized` and persist nothing.

Update an existing page by id:

```bash
curl -sS https://your-site.example/api/mcp \
  -H 'content-type: application/json' \
  -H 'authorization: Bearer midl_xxxxxxxxxxxx_...' \
  -d '{"jsonrpc":"2.0","id":7,"method":"tools/call",
       "params":{"name":"update_frame","arguments":{"id":"frame-…","is_active":false}}}'
```

## Connecting from a stock MCP client

Point any MCP client at the endpoint URL. With the TypeScript SDK
(`@modelcontextprotocol/sdk`) over the Streamable-HTTP transport:

```ts
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';

const transport = new StreamableHTTPClientTransport(
  new URL('https://your-site.example/api/mcp'),
  // for writes, attach your key on every request:
  { requestInit: { headers: { authorization: 'Bearer midl_xxxxxxxxxxxx_...' } } }
);
const client = new Client({ name: 'my-agent', version: '1.0.0' });
await client.connect(transport);

const { tools } = await client.listTools();
const doc = await client.callTool({ name: 'read_document', arguments: { slug: 'pricing' } });
```

## schema.org mapping

`read_document` returns a `schemaOrg` array mapping each `ui:*` type the page uses to its schema.org
counterpart, so agents and NLWeb tooling get ground-truth types without parsing HTML. The clear
overlaps (e.g. `ui:Page → WebPage`, `ui:Hero → WPHeader`, `ui:Image → ImageObject`,
`ui:Nav → SiteNavigationElement`, `ui:Form → Action`) are mapped; `ui:*` types with no faithful
schema.org counterpart are simply omitted.

## Errors

- **Protocol-level** problems return a JSON-RPC `error` object: `-32700` parse error, `-32600` invalid
  request, `-32601` unknown method, `-32602` invalid params.
- **Tool-level** problems (unknown tool, missing key, not-found, invalid MIDL) return a *successful*
  JSON-RPC response whose `result` has `isError: true` and a safe message + code in the text content —
  the MCP-idiomatic shape, so an agent can read the error and correct its call. Internal/database
  details are never leaked.

## Discovery

The endpoint is advertised in the site's machine surfaces:

- `/.well-known/agent.json` → `endpoints.mcp` and an `mcp` tool entry
- `/llms.txt` → a link under "Machine-readable surfaces"
