---
title: "Can a Static Blog Hand AI Agents Real Tools? Wiring ARD + MCP into an Astro Site (Measured)"
date: 2026-08-26
description: "A +950-line commit gives a static blog an ARD catalog, OpenAPI tools, JSON indexes and a read-only MCP server. Live in 12–18 s; stress-tested, 5 gaps fixed."
category: "AI & Compute"
tags:
  - "ARD"
  - "MCP"
  - "OpenAPI"
  - "Astro"
  - "Vercel"
  - "Agents"
verifiedHardware: "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"
keyTakeaways:
  - "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."
measuredOn: 2026-08-26
faq:
  - q: "What is ARD (Agentic Resource Discovery)?"
    a: "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."
  - q: "Can a purely static site expose MCP tools without running a server?"
    a: "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."
  - q: "How long does it take to implement ARD on a personal site?"
    a: "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."
  - q: "Why not just use llms.txt and RSS for AI agents?"
    a: "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."
  - q: "Did the official ARD schema validation pass on the first try?"
    a: "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."
  - q: "What did the adversarial stress test find?"
    a: "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."
---
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`):

```text
# 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](https://agenticresourcediscovery.org/spec/) (v0.9 draft, 2026-05-28) · [spec repository with schemas](https://github.com/ards-project/ard-spec) · [Google announcement, 2026-06-17](https://developers.googleblog.com/announcing-the-agentic-resource-discovery-specification/) · [Hugging Face reference implementation](https://huggingface.co/.well-known/ai-catalog.json) · [Dries Buytaert's site-search ARD writeup, 2026-07-23](https://dri.es/helping-agents-discover-my-site-search-with-agentic-resource-discovery) · this site's live artifacts: [catalog](https://sigpulse.com/.well-known/ai-catalog.json) · [OpenAPI](https://sigpulse.com/openapi.json) · [agents.md](https://sigpulse.com/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](https://github.com/xiong1984/static-site-mcp).
