Your published posts are already legible to agents. Every Leed site carries a small public tool surface over its blog, news and article content — six read-only calls that an AI agent can discover and use without an account, a token or a key. Three different discovery mechanisms point at the same six tools, so an in-browser agent, a remote agent framework and an agent-to-agent directory all find the same thing.
This is the opposite bargain from the Docs MCP, which verifies every reader before it serves a page. Here there is no gate, because everything on offer is already public HTML on your site. What the surface adds is structure: an agent gets titles, paths, publish dates and clean Markdown instead of whatever it can salvage from scraping your layout.
Three surfaces, one definition
All three surfaces are generated from a single capability module in Leed’s codebase. The tool names, descriptions, input schemas and output schemas are written once and rendered three ways, which is why the in-page registration, the WebMCP manifest and the A2A card can never disagree with each other or with the endpoints behind them.
| Discovery | Format | Built as | Audience |
|---|---|---|---|
In-page navigator.modelContext | WebMCP tool registration | A script tag on every published page | An AI agent running inside the reader’s browser |
https://YOUR-DOMAIN/.well-known/webmcp | WebMCP manifest | _services/webmcp.json, written at site build | Remote agents and agent frameworks |
https://YOUR-DOMAIN/.well-known/agent.json | A2A AgentCard | _services/agent.json, written at site build | Agent-to-agent discovery and directories |
In-page registration
Every published page loads a small tracker that installs the WebMCP polyfill and registers the six tools on navigator.modelContext. Each is annotated readOnlyHint: true and openWorldHint: false — it reads, and it reads only from this site.
The script is emitted into the page head only when MCP is enabled for your site, and it is defensive about where it runs: it is a no-op during server-side rendering, a no-op in a browser with no navigator, and a no-op if the polyfill fails to install. A tool handler is a same-origin fetch against the matching /api/agent/* endpoint, returning the JSON as text.
The practical consequence is that an agent already looking at your page in a browser does not have to fetch a manifest, resolve a base URL or authenticate. The tools are already there.
/.well-known/webmcp
The manifest is rendered at site build time into your site’s asset bundle and served from the well-known path by your edge worker. It names the site, describes each tool, and binds each one to a plain GET endpoint on your own domain with JSON Schema for both its inputs and its outputs.
curl -s https://YOUR-DOMAIN/.well-known/webmcp | head -40It is built as a static asset rather than injected as a worker variable for a mundane reason — at roughly 10 kB it exceeds the size a text binding can hold — but the consequence is useful: the file only exists when MCP is entitled, so the discovery path and the underlying document appear and disappear together.
/.well-known/agent.json
The A2A AgentCard describes the same six tools as skills, for agent-to-agent discovery. It is discovery-only: there are no task-execution or delegation endpoints in it, and its capability block says so explicitly.
{
"protocolVersion": "0.2.0",
"name": "Example Docs",
"description": "Product news and engineering articles",
"url": "https://YOUR-DOMAIN",
"version": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0",
"capabilities": {
"streaming": false,
"pushNotifications": false,
"stateTransitionHistory": false
},
"defaultInputModes": ["text/plain"],
"defaultOutputModes": ["application/json", "text/plain"],
"skills": [
{
"id": "search_site",
"name": "search_site",
"description": "Search the site's published posts…",
"tags": ["read-only", "content"]
}
]
}The card’s version is the commit your site was built from, so an agent that caches the card can tell when your site has been rebuilt.
The six tools
Every tool is a GET, every tool is read-only, and every tool sees published content only.
| Tool | Endpoint | Parameters | Returns |
|---|---|---|---|
search_site | GET /api/agent/search | query (sent as ?q=), limit 1–25, default 10 | The mode that ran, plus ranked hits with pageId, title, path, url, snippet and score |
get_site_overview | GET /api/agent/overview | None | Site name, description, origin, published post-type count, published post count, and the protected-content pointer |
list_post_types | GET /api/agent/post-types | None | Each published post type with its pageTypeId, name, base path and published count |
get_pages_by_id | GET /api/agent/pages-by-id | ids, up to 25 (sent as repeated ?id=) | One resource per id, in input order |
get_pages_by_path | GET /api/agent/pages-by-path | paths, up to 25 (sent as repeated ?path=) | One resource per path, in input order |
list_recent_posts | GET /api/agent/recent-posts | limit 1–50, default 10 | Newest published posts first, as lightweight pointers with no content |
These return your Markdown
On a hit, get_pages_by_id and get_pages_by_path return the post’s content as Leed Markdown — specifically the cleaned, publicly published Markdown your build emitted, not your editorial source. It arrives in the content field alongside pageId, title, path, url and publishedAt.
That content is filled in by your site’s edge worker on the way out, from the same published files that render your site. The API emits the envelope; the edge fills it. The distinction matters only if a page has been published but its file is not where the edge expects it, in which case that one item carries an error and the rest of the batch is unaffected.
search_site and list_recent_posts never return content. They return pointers, and the agent follows up with a batch fetch for the handful it actually wants. The syntax of what comes back is documented in the Leed Markdown cheat sheet.
Scope: posts only
The surface is hard-scoped to your post content. Documentation and API reference pages belong to the Docs MCP and never appear here — not in search results, not in listings, not by direct fetch, not by page id.
A request for one is not silently dropped. It comes back as a marker telling the agent exactly where that content lives.
{
"resources": [
{
"pageId": "3d1c9a80-2f77-4e6a-9a1b-77b0f2c4e001",
"title": "Why we rebuilt search",
"path": "/blog/why-we-rebuilt-search/",
"url": "https://YOUR-DOMAIN/blog/why-we-rebuilt-search/",
"publishedAt": "2026-04-11T09:00:00.000Z",
"content": "Search had one job and it was doing it badly…"
},
{
"path": "/docs/getting-started/authentication/",
"error": "protected_resource",
"accessVia": "/mcp",
"discovery": "/.well-known/oauth-protected-resource"
},
{
"path": "/blog/never-published/",
"error": "not_found"
}
]
}| Marker | When | What the agent should do next |
|---|---|---|
protected_resource | The page is published documentation or API reference | Follow accessVia to /mcp and authenticate — see the Docs MCP Tool Reference |
not_found | The page is missing, unpublished, or of a kind this surface does not serve | Nothing — the item does not exist here |
get_site_overview advertises the same pointer up front, in a protectedContent block naming the two protected kinds, whether your site actually has any published content of that kind, and where to reach it. An agent that starts with the overview learns about the second surface before it ever trips over a marker.
flowchart LR
A["Agent arrives at your domain"] --> B{"How did it find you?"}
B -->|"AgentCard well-known"| T["Six GET endpoints<br/>under /api/agent"]
B -->|"WebMCP well-known"| T
B -->|"in-page registration"| T
T --> K{"What kind of page<br/>was requested?"}
K -->|posts| C["Content returned<br/>Leed Markdown + metadata"]
K -->|documentation / api| P["protected_resource<br/>accessVia: /mcp"]
K -->|missing or unpublished| N["not_found"]
P --> M["Docs MCP at /mcp<br/>email verification + OAuth"]
M --> C2["Documentation returned"]
No sign-in, no tracking
There is no token, no OAuth and no email verification on this surface. There is also, deliberately, almost no trace of the agent afterwards.
The /api/agent/* endpoints run as a separate application from your site’s main API, with the visitor-tracking chain removed rather than skipped. That is the whole point of the separation: on the main app every cookie-less request would mint a session and a visitor id, write session, user, browser and location rows, emit a page-view event and set tracking cookies. An agent hitting six endpoints would look like six new visitors. Here it mints none of that and sets no cookies at all, so agent traffic never inflates your visitor analytics or contaminates your lead data.
Two honest qualifications:
- An in-page call carries the reader’s own cookies, because it is a same-origin
fetchfrom a page they are already on. The request log reads those cookies to resolve the existing visitor — read-only, creating nothing — so an agent acting on a human’s behalf is attributed to that human rather than to nobody. - A cold external call records a coarse location hash, and writes at most one deduplicated row to the locations table for it. This is the weakest available stand-in for identity on a surface where the protocol carries none, and it is recorded only when no visitor could be resolved. A row carries one or the other, never both.
A per-IP burst limit caps volume at 60 requests a minute. Over it, the response is 429 with {"error":"rate_limited"}.
Where it shows up in your numbers
Every call writes one row to the same request log the Docs MCP uses, tagged with source: "agent" rather than mcp. That tag is what separates the buckets on your dashboard: humans come from page views, AI agents from this surface, and MCP clients from the Docs MCP. A bucket with no rows reports zero honestly rather than hiding.
So the question “is anything actually reading my blog through an agent?” has an answer, and it is separate from both your human traffic and your documentation traffic. Analytics for AI and MCP covers how to read it.
Turning it on and off
There is no separate toggle for the site agent. It rides the same per-company MCP entitlement as the Docs MCP, and the two switch together.
The Docs MCP’s two access toggles — open access and capture leads — do not apply here. There is no identity on this surface for them to govern. Preview builds of your site never expose the agent surface either, regardless of entitlement; discovery and endpoints answer 404 there by design, so a staged site is not quietly agent-readable before you publish it.
Your own team’s workspace access is a third and entirely separate service with its own authentication and its own tool set: the Operator MCP. Nothing on this page grants write access to anything.
What else your published site emits at build time — sitemaps, feeds, structured data — is covered in What Your Visitors Get.
Pre-standard, on purpose
WebMCP and A2A are young specifications. Leed emits WebMCP draft 0.1-draft and A2A protocol version 0.2.0, both pinned, both declared in the documents themselves.