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, anapplication/jsonresponse. No session id, no server-push, no batching. - Protocol version:
2025-06-18(older revisions are negotiated oninitialize). - Auth: read + telemetry tools are open;
create_frame/update_framerequire a Bearer API key — the same key you mint at/admin/keysfor the 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:
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"}}}'
{
"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:
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
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:
{
"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
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
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>:
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:
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:
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
errorobject:-32700parse error,-32600invalid request,-32601unknown method,-32602invalid params. - Tool-level problems (unknown tool, missing key, not-found, invalid MIDL) return a successful
JSON-RPC response whose
resulthasisError: trueand 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.mcpand anmcptool entry/llms.txt→ a link under "Machine-readable surfaces"