Can a Static Blog Hand AI Agents Real Tools? Wiring ARD + MCP into an Astro Site (Measured)
Key Takeaways — Executive & AI Summary
- The full agent interface shipped as one 13-file, +950-line commit (8eeb3ec, 2026-08-26): an ARD catalog at /.well-known/ai-catalog.json validated against the official JSON Schema, a 7-tool OpenAPI 3.1 document, two JSON indexes, and a 359-line read-only MCP server — and Vercel served it live 12–18 s after push (6 s polling granularity).
- A stateless MCP server over static data is enough: /api/mcp answers initialize, tools/list and tools/call by fetching the site's own static JSON and filtering in memory — first call in a 5-sample run took 0.90 s, the four follow-ups 0.28–0.34 s (AWS Seoul, 2026-08-26), with zero database and zero auth.
- Live testing keeps catching what local checks miss: phrase-only search returned 0 hits for the natural query "DeepSeek price" (fixed, +21 lines), and a same-day 11-hypothesis adversarial stress test with a real MCP client found five more declared-vs-actual gaps — limit clamping, query validation, undeclared OpenAPI security — fixed in eae3a23 (+31/−4) and re-verified on production.
Can a static blog give AI agents real, callable tools — not just pages to crawl? Yes. On 2026-08-26 this site (Astro 5.18.2, static output, Vercel hosting) shipped a four-layer agent interface as one 13-file, +950-line commit (8eeb3ec): an ARD capability catalog validated against the official JSON Schema, an OpenAPI 3.1 tool document with 7 operations, two JSON content indexes, and a 359-line read-only MCP server at /api/mcp. Vercel served the change live 12–18 s after push (6 s polling granularity, single observation, 2026-08-26). A local 21-check logic suite passed before shipping, and live golden-question testing then caught one real defect the suite had missed — details below.
Why: the discovery layer the agentic web was missing
Agents were already welcome here — robots.txt allows the AI crawlers, every article has a raw markdown endpoint, /llms.txt indexes the site. But all of that is documents: an agent still parses prose to learn what exists. The Agentic Resource Discovery specification (ARD), announced 2026-06-17 by a working group from Google, Microsoft, Hugging Face, AWS, Cisco, Databricks, GitHub, GoDaddy, NVIDIA, Salesforce and Snowflake, standardizes the missing piece: a site describes its callable capabilities in a manifest at /.well-known/ai-catalog.json, and agents or federated registries discover that manifest through the well-known path, a Agentmap: line in robots.txt, or an HTML <link rel="ai-catalog"> tag — the same conventions as robots.txt and security.txt.
Adoption is early. As of 2026-07-23, Dries Buytaert reported finding only Hugging Face with a catalog on its primary domain (his check, not mine); his own manifest — one entry pointing at his existing OpenAPI document — took him less than an hour. Hugging Face’s catalog is live today at huggingface.co/.well-known/ai-catalog.json (657 bytes, served as application/ai-catalog+json, verified 2026-08-26). That sparseness is the opportunity: a small site can be machine-discoverable now, at the cost of one build-time generated file.
What was built: four layers, 13 files, one session
The table below lists what a complete agent interface on this site consists of (all artifacts generated at build time on 2026-08-26 from the Astro 5.18.2 content collections; the MCP server is the single runtime piece):
| Layer | Artifact | Size | Role |
|---|---|---|---|
| Discovery | /.well-known/ai-catalog.json | 4,424 B | ARD catalog: 3 entries, 12 representative queries |
| Description | /openapi.json | 14,394 B | OpenAPI 3.1: 7 operations with typed schemas |
| Description | /agents.md | 3,163 B | Operations manual: call pattern + citation rules |
| Data | /posts.json | 4,138 B | Dispatch index: 2 entries with dates/hardware/takeaways |
| Data | /watch.json | 11,186 B | Watch index: 7 entries with full provenance |
| Invocation | /api/mcp (359-line function) | — | Stateless MCP: 6 sigpulse_* tools |
All five static artifacts together total 37,305 bytes (~36.4 KiB) — small enough for an agent to fetch in full on first contact. The catalog is wired for discovery three ways per the spec: the well-known path itself, Agentmap: https://sigpulse.com/.well-known/ai-catalog.json in robots.txt, and a <link rel="ai-catalog"> tag injected into every page’s <head> by the base layout. Every article page additionally carries <link rel="alternate" type="text/markdown"> pointing at its raw markdown, so a browser agent reading the HTML gets the direct machine endpoint in the first kilobytes.
One detail the official schema validator taught us the hard way: updatedAt must be a full ISO 8601 date-time, not a plain date. The first validation run failed with 3 errors (one per entry); changing 2026-08-26 to 2026-08-26T00:00:00Z made all three pass. If you implement ARD, budget ten minutes for ajv against the official schema — it caught a mistake that no amount of reading the spec prose had.
How a static site gets an MCP endpoint
MCP requires JSON-RPC POST handling, which static files cannot do. The design here keeps every piece of data static and adds exactly one Serverless Function (api/mcp.ts, 359 lines, zero dependencies, zero configuration — Vercel compiles the repo-root api/ directory automatically): it implements the stateless JSON-response path of the MCP streamable-http transport (POST JSON-RPC only; GET/DELETE answer 405; OPTIONS answers 204 for CORS), exposing six tools — sigpulse_list_dispatches, sigpulse_get_dispatch, sigpulse_list_watch, sigpulse_get_watch_entry, sigpulse_get_measurements, and sigpulse_search.
The function holds no data. Each call fetches the site’s own static JSON (5-minute in-instance cache) and filters in memory. Latency measured 2026-08-26 from an AWS Seoul node, 5 samples: the first call in the sample took 0.90 s end-to-end; the four follow-ups took 0.28–0.34 s. The function’s initialize response embeds the site’s citation rules in the instructions field, so every MCP session starts with the data contract (numbers carry measured_on + verified_hardware; Watch entries are commentary, never measurements; CC BY 4.0).
Two defensive choices worth copying. First, tool arguments are validated: a slug must exist in the static index before it is ever placed in a URL, so sigpulse_get_dispatch("../etc/passwd") returns isError: true plus the list of valid slugs — the server cannot be turned into an arbitrary-fetch proxy, and the agent can self-correct. Second, the ARD catalog’s embedded MCP card deliberately omits tool input schemas; the authoritative schemas live only in the runtime tools/list response, and a production diff confirmed the card’s six tool names exactly match the live tools/list (sorted diff empty, 2026-08-26). One source of truth, one diff to keep it honest.
The failure local tests missed: phrase-only search
The pre-flight suite — 21 checks covering the JSON-RPC handshake, all six tools against real data, the traversal guard, error codes (−32700/−32601/−32603), and 405/204 handling — passed 21/21 locally. The first live golden-question run then returned zero hits for the query “DeepSeek price”, which should have matched the entry “Why Did DeepSeek Double Its API Prices in August 2026?”.
The cause: search matched the whole phrase as a substring, and agents phrase queries in natural word order that rarely matches a title verbatim. The fix (+21/−13 lines, commit 4b2db6f): if the full phrase matches nothing, fall back to requiring every whitespace-separated term to appear somewhere across the record’s fields. Post-fix, live on 2026-08-26:
| Query (live, 2026-08-26) | Hits | What matched |
|---|---|---|
| DeepSeek price | 1 | Watch entry (API-prices piece) |
| helium export ban | 1 | Watch entry (helium-export piece) |
| InfiniteTalk VRAM | 3 | 1 dispatch + 2 measurement-ledger rows |
The lesson generalizes: local suites verify contracts, only live natural-language probes verify fitness for how agents actually ask. “DeepSeek price” — five plain words — found a defect that 21 contract checks could not.
Did the live interface match its own claims? An 11-hypothesis stress test (update, same day)
Because everything above was now published as fact, every conclusion was restated as a falsifiable hypothesis and tested against production the same day (2026-08-26). Eight held exactly as claimed: a blind five-hop discovery walk starting from the bare domain (homepage <head> → catalog → OpenAPI → index → full markdown, with the robots.txt Agentmap path verified as an independent alternate route); all three JSON payloads validating against the schemas our own OpenAPI document declares; three cross-layer identity checks (OpenAPI operationIds ≡ catalog capabilities, catalog MCP card tools ≡ live tools/list, agents.md counts ≡ live counts); 36 declared URLs resolving (the single non-200 was /api/mcp correctly refusing GET with 405 — a link-checker false positive, not a defect); discovery links present on 15 of 15 sitemap pages; exact 404-on-unknown-slug and 405-on-PUT semantics; and a 20-concurrent mixed burst returning 20/20 successes at p50 0.25 s / p95 0.34 s.
The headline verification came from a real client: a Claude CLI process connected to the production MCP endpoint with no prior knowledge and answered three fact questions — the 218.5 s/step dual-GPU measurement (RTX 4090D 24GB + RTX A4000 16GB), the Watch entry’s original Chinese headline 「DeepSeek涨价背后,一个时代结束了」 dated 2026-08-14, and the 20-entry ledger total — by autonomously planning four tool calls, issuing the first three in parallel and unprompted passing limit: 1000 to defeat truncation. Every fact was correct. The interface works not just as documented but as used.
Five declared-vs-actual gaps did surface, all fixed in eae3a23 (+31/−4) and re-verified live. The table below shows each gap as observed before the fix (production, 2026-08-26):
| Input | Declared behavior | Observed before fix | After fix |
|---|---|---|---|
limit: -1 | number ≥ 1 | slice(0, -1) silently dropped the last item — 2 of 3 returned | clamped to [1, 200] |
limit: 0 | number ≥ 1 | returned 0 items while count still reported 3 | falls back to default |
query: "" | required, non-empty | empty substring matches everything — 30 hits (every record) | isError rejection |
query omitted | required | degraded to literal "undefined" — matched the torch dispatch, whose takeaways genuinely contain “undefined symbol” | isError rejection |
| OpenAPI security | — | no declaration; redocly reported 7 security-defined errors | explicit security: [] + 4xx responses; redocly validates clean |
Two observations worth keeping. First, the “undefined” match was technically correct search behavior — the dependency-matrix dispatch really does discuss an undefined-symbol error — which is exactly why silent type coercion is dangerous: it produces plausible-looking wrong answers instead of failures. Second, a Chinese-language query (“涨价”) hits the Watch entry through its Chinese original_title field; for a China-focused site, multilingual matching through provenance fields is a feature that fell out of the design for free. One more declared-vs-actual alignment landed with this update: the limit input schemas now state minimum: 1, maximum: 200, matching the clamping the code actually enforces.
llms.txt vs OpenAPI vs ARD vs MCP: what each layer is for
These four are not competitors; each answers a different question an agent has (all four are published on this site simultaneously, 2026-08-26):
| Standard | Answers | Form | Who consumes it |
|---|---|---|---|
| llms.txt | “What is on this site?” | Markdown index | LLMs and crawlers that fetch it |
| OpenAPI 3.1 | “What operations exist, with what inputs?” | Typed JSON tool descriptions | Agents that turn APIs into tool calls |
| ARD catalog | “What capabilities live at this domain?” | /.well-known/ai-catalog.json manifest | Discovery agents and federated registries |
| MCP | “Let me call those tools now” | JSON-RPC over HTTP | MCP-native clients |
Reproduction appendix
Everything below is exactly what was run on 2026-08-26 (Astro 5.18.2; repo files at commit 8eeb3ec + fix 4b2db6f):
# 1. Static JSON indexes (Astro endpoint pattern, mirrors existing data.json.ts)
src/pages/posts.json.ts # {site, license, citation_rule, count, dispatches[]}
src/pages/watch.json.ts # {site, license, notice, count, entries[]} — provenance IS the payload
# 2. OpenAPI 3.1 tool document (static object, no collection dependency)
src/pages/openapi.json.ts # 7 operationIds: listDispatches, getDispatchRaw, ...
# 3. ARD catalog (Astro routes dot-directories: pages/.well-known/ai-catalog.json.ts
# works because Astro 5.18.2's route scan exempts exactly .well-known
# — node_modules/astro/dist/core/routing/manifest/create.js:85)
src/pages/.well-known/ai-catalog.json.ts
# 4. Official schema validation (the validator that caught the date-time issue)
curl -fsSL -o ard.schema.json \
https://raw.githubusercontent.com/ards-project/ard-spec/main/spec/schemas/ai-catalog.schema.json
npx ajv validate -s ard.schema.json -d dist/.well-known/ai-catalog.json --spec=draft2020
# 5. MCP server (Vercel compiles repo-root api/ with zero config)
api/mcp.ts # 359 lines, zero deps; POST JSON-RPC; GET→405; OPTIONS→204
# Smoke test:
curl -X POST https://sigpulse.com/api/mcp -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
# 6. Discovery wiring
public/robots.txt # Agentmap: https://sigpulse.com/.well-known/ai-catalog.json
src/layouts/BaseLayout.astro # <link rel="ai-catalog"> + OpenAPI alternate on every page
Primary sources: ARD specification (v0.9 draft, 2026-05-28) · spec repository with schemas · Google announcement, 2026-06-17 · Hugging Face reference implementation · Dries Buytaert’s site-search ARD writeup, 2026-07-23 · this site’s live artifacts: catalog · OpenAPI · agents.md.
All timings above are web-stack measurements (build on an AWS Seoul 2-vCPU node; production on Vercel’s global CDN), measured 2026-08-26, single-session. License: CC BY 4.0 — cite the source URL.
Update, 2026-08-30: the complete machine layer this dispatch measured — the stateless MCP server, the llms.txt/OpenAPI/ARD generators, and the preflight checker — is now open-sourced as a reference implementation: static-site-mcp.
FAQ — Direct Answers
- What is ARD (Agentic Resource Discovery)?
- An open specification published 2026-06-17 (spec v0.9 draft dated 2026-05-28) with contributors from Google, Microsoft, Hugging Face, AWS, Cisco, Databricks, GitHub, GoDaddy, NVIDIA, Salesforce and Snowflake. A site publishes a capability manifest at /.well-known/ai-catalog.json; agents and federated registries discover it via the well-known path, a robots.txt Agentmap line, or an HTML <link rel="ai-catalog"> tag.
- Can a purely static site expose MCP tools without running a server?
- Not fully — MCP requires JSON-RPC POST handling. The workaround used here: keep every data endpoint as static build output (posts.json, watch.json, data.json, raw .md) and add exactly one Serverless Function (api/mcp.ts, 359 lines) that fetches those static files and filters in memory. Vercel compiles the api/ directory with zero configuration.
- How long does it take to implement ARD on a personal site?
- Dries Buytaert reported his OpenAPI-pointing manifest took less than an hour (2026-07-23). The full four-layer version on this site — catalog, OpenAPI, JSON indexes, MCP server, and discovery wiring — was one working session: 13 files, +950 lines, with the first production failure found and fixed in a follow-up +21-line commit.
- Why not just use llms.txt and RSS for AI agents?
- Those are document feeds: an agent still has to parse prose to find facts. The ARD + OpenAPI + MCP stack turns the same content into named tools with typed inputs (listDispatches, getDispatchRaw, sigpulse_search) so an agent can discover and call them directly. llms.txt and RSS remain published alongside — the layers complement, not replace, each other.
- Did the official ARD schema validation pass on the first try?
- No — the first validation failed with 3 errors: the schema requires updatedAt as a full ISO 8601 date-time, not a plain date. Changing 2026-08-26 to 2026-08-26T00:00:00Z on all three entries made validation pass. Cite this if you implement: use date-time.
- What did the adversarial stress test find?
- Eleven falsifiable hypotheses were tested against production on 2026-08-26. Eight held exactly (five-hop blind discovery from the bare domain, payload-vs-declared-schema conformance, three cross-layer identity checks, 36 declared URLs resolving, discovery links on 15/15 pages, exact 404/405 semantics, and 20/20 concurrency at p95 0.34 s). Five declared-vs-actual gaps were found and fixed in eae3a23: limit:-1 silently truncated results, limit:0 returned none while count still reported the total, an empty query matched all 30 records, a missing query degraded to the literal string "undefined" (matching the dispatch about torch's undefined-symbol error), and the OpenAPI document lacked an explicit empty security declaration — 7 redocly errors, now zero. A real MCP client (Claude CLI) also answered every fact question correctly through four autonomous tool calls.