Skip to content
SIGPULSE
AI & Compute 9 min read raw .md ↗

Can a Static Blog Hand AI Agents Real Tools? Wiring ARD + MCP into an Astro Site (Measured)

● PROOF OF EXECUTION Astro 5.18.2 static build on an AWS Seoul node (2 vCPU) · Vercel global CDN + one Serverless Function · no GPU involved — all numbers are web-stack timings · Tested 2026-08-26 · Configs published for replication

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):

LayerArtifactSizeRole
Discovery/.well-known/ai-catalog.json4,424 BARD catalog: 3 entries, 12 representative queries
Description/openapi.json14,394 BOpenAPI 3.1: 7 operations with typed schemas
Description/agents.md3,163 BOperations manual: call pattern + citation rules
Data/posts.json4,138 BDispatch index: 2 entries with dates/hardware/takeaways
Data/watch.json11,186 BWatch 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 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)HitsWhat matched
DeepSeek price1Watch entry (API-prices piece)
helium export ban1Watch entry (helium-export piece)
InfiniteTalk VRAM31 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):

InputDeclared behaviorObserved before fixAfter fix
limit: -1number ≥ 1slice(0, -1) silently dropped the last item — 2 of 3 returnedclamped to [1, 200]
limit: 0number ≥ 1returned 0 items while count still reported 3falls back to default
query: ""required, non-emptyempty substring matches everything — 30 hits (every record)isError rejection
query omittedrequireddegraded to literal "undefined" — matched the torch dispatch, whose takeaways genuinely contain “undefined symbol”isError rejection
OpenAPI securityno declaration; redocly reported 7 security-defined errorsexplicit 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):

StandardAnswersFormWho consumes it
llms.txt“What is on this site?”Markdown indexLLMs and crawlers that fetch it
OpenAPI 3.1“What operations exist, with what inputs?”Typed JSON tool descriptionsAgents that turn APIs into tool calls
ARD catalog“What capabilities live at this domain?”/.well-known/ai-catalog.json manifestDiscovery agents and federated registries
MCP“Let me call those tools now”JSON-RPC over HTTPMCP-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.