# Agent ready setup checklist for a small business website Version 1.0, September 26, 2026. Written by J.A. Watte (https://jwatte.com). Free to copy, adapt and share. No signup, no attribution required. This is the working checklist and template set behind the article at https://jwatte.com/blog/agent-ready-small-business-website/. The patterns in it come from real small business sites I worked on in September 2026, and the A2A and MCP parts were tested with the official client libraries. Replace `example.com`, `Example Laundry` and the other placeholders with your own values. **How to use it.** Work top to bottom. Section 1 decides what you publish at all, and it matters more than any template. Sections 2 onward are templates and checks. Every JSON block in this file parses as written. **If you hand this file to an AI coding assistant**, tell it three things: change the config or generator that builds these files, never one built file; never write a price, an hour, availability or any claim your pages do not already publish; and stop at a draft deploy so a person can check it. ## 1. Decide what you are actually offering A card or manifest is a promise to a client that cannot see you. Publish one only when the thing it describes answers at the URL it names. | What you have | Publish | Leave out | |---|---|---| | A website and nothing more | llms.txt, robots.txt with Content-Signal in every group, link preview tags, the two head links in section 6.5 | Agent card, MCP server card, OAuth metadata | | A working read-only site guide at `/a2a` | The A2A agent card (section 3) | Anything claiming streaming, push or tasks | | A working read-only MCP server at `/mcp` | The server card at three addresses, an AI Catalog entry, an api-catalog anchor (sections 4 and 6) | OAuth documents | | No login or API key anywhere | Nothing at `/.well-known/oauth-protected-resource`. Its 404 is the specified way to say "no auth". | Protected Resource Metadata, OAuth server metadata | | No online checkout | Nothing commerce related | UCP profiles and other commerce manifests | | You receive bot traffic | Nothing | A Web Bot Auth key directory (that file belongs to the bot operator, not to you) | - [ ] 1.1 Every card you plan to publish names an endpoint that exists today and answers. - [ ] 1.2 The card is generated from the same config file the endpoint runs, so they cannot drift. - [ ] 1.3 You know how you will remove it: card and endpoint go in the same deploy, never one without the other. - [ ] 1.4 No file sits at a standard path "for later". A malformed or placeholder card at `/.well-known/agent-card.json` scores worse than no card. - [ ] 1.5 Your "not found" paths return a real 404, not your home page with a 200. A scanner that checks only the status code counts an HTML 200 as a card. ## 2. The order of work - [ ] 2.1 Build the endpoint and its tests first. - [ ] 2.2 Generate the cards and discovery entries from the endpoint's config. - [ ] 2.3 Run the local checks (section 14.1). - [ ] 2.4 Deploy a draft or preview, never production. - [ ] 2.5 Have someone other than the builder try to break the draft: hostile input, odd paths, wrong methods, source file paths. They return GO or a list of blockers. - [ ] 2.6 Deploy production only when the owner says so. - [ ] 2.7 Run the live checks, paced under your own rate limit (section 14.4). - [ ] 2.8 Only after that, submit any connector or directory listing (section 13). - [ ] 2.9 Check the bytes the live site serves, not the deploy tool's success message. A "deploy is live" line can print before the CDN serves the new files. ## 3. A2A site guide agent A read-only guide: a client sends a plain-language question and gets back up to 5 pages from your own site, each with a title and link, plus how to contact you. ### 3.1 Endpoint behavior (POST `/a2a`, JSON-RPC 2.0) | Request | Answer | |---|---| | `SendMessage` (A2A 1.0) or `message/send` (A2A 0.3) | A Message, never a Task, with a text part and a JSON data part listing the pages | | `GetTask`, `tasks/get`, `CancelTask`, `tasks/cancel` | Error `-32001` (task not found; the agent never creates tasks) | | Any streaming method | Error `-32004` (unsupported operation) | | Any push notification config method | Error `-32003` (push notifications not supported) | | A request with no `id` (a notification) | HTTP 204, no body | | Body over your cap (256 KB is plenty) | HTTP 413 | | `GET`, `HEAD` | HTTP 405 with `Allow: POST, OPTIONS` and a small JSON body pointing at the card | | `OPTIONS` | HTTP 204 CORS preflight | - [ ] 3.1.1 Every answer opens by saying it is an automated site guide. - [ ] 3.1.2 Pages come only from your own page list (llms.txt, and the sitemap if llms.txt lists only part of the site). Links to other domains are dropped. - [ ] 3.1.3 The answer repeats only the words of the question (letters and digits), never its links or markup. - [ ] 3.1.4 Reading the page list has a hard deadline of about 3 seconds, so an honest "the page list is unavailable" answer still arrives inside a common 5 second client timeout. - [ ] 3.1.5 The config's description and contact text carry no price and no claim the site does not publish. Both are repeated in every answer. ### 3.2 Agent card template (`/.well-known/agent-card.json`) This card carries the A2A 1.0 fields plus three A2A 0.3 fields (`url`, `preferredTransport`, `protocolVersion`) that older client libraries need before they will parse it. Clients built for 1.0 ignore the extra fields. ```json { "name": "Example Laundry site guide", "description": "Automated, read-only guide to Example Laundry (example.com). Ask a plain-language question and it replies with up to 5 matching pages from the site's own page list, each with its title and a link, plus how to contact the business. It does not book, buy, submit forms or store what you send.", "supportedInterfaces": [ { "url": "https://example.com/a2a", "protocolBinding": "JSONRPC", "protocolVersion": "1.0" }, { "url": "https://example.com/a2a", "protocolBinding": "JSONRPC", "protocolVersion": "0.3" } ], "provider": { "organization": "Example Laundry LLC", "url": "https://example.com/" }, "version": "1.0.0", "documentationUrl": "https://example.com/llms.txt", "capabilities": { "streaming": false, "pushNotifications": false, "extendedAgentCard": false }, "defaultInputModes": ["text/plain"], "defaultOutputModes": ["text/plain", "application/json"], "skills": [ { "id": "site-guide", "name": "Find pages on example.com", "description": "Read-only. Answers a plain-language question with up to 5 of the most relevant pages on example.com, found by searching only the site's own page list, plus a short line on how to contact the business. It never books, buys, submits forms, stores messages or calls third parties.", "tags": ["site-guide", "search", "read-only", "laundromat"], "examples": [ "What are your hours?", "How much does a large washer cost?", "Where do I report a broken machine?" ], "inputModes": ["text/plain"], "outputModes": ["text/plain", "application/json"] } ], "url": "https://example.com/a2a", "preferredTransport": "JSONRPC", "protocolVersion": "0.3" } ``` - [ ] 3.2.1 3 to 5 `examples` that your page list can really answer. Ask each one against the live agent before publishing. - [ ] 3.2.2 `version` is bumped whenever the config or the agent code changes. - [ ] 3.2.3 The card is served with `Content-Type: application/json`, `Access-Control-Allow-Origin: *` and a cache time such as `max-age=3600`. - [ ] 3.2.4 If your build tool only renders certain template formats (Eleventy with a pinned `templateFormats` list, for example), confirm the card template actually produced a file. - [ ] 3.2.5 If your CI calls the build tool directly (`npx astro build`), an npm `prebuild` script never runs there. Give the card check its own CI step. ## 4. Read-only MCP server ### 4.1 Behavior checklist (POST `/mcp`, Streamable HTTP) - [ ] 4.1.1 Serves both protocol eras on one endpoint: the stateless 2026-07-28 revision (`server/discover`, a `_meta` envelope on every request, no `initialize`) and `initialize` for clients built for the 2025 revisions. - [ ] 4.1.2 Mints no `Mcp-Session-Id`. Every request works on its own. - [ ] 4.1.3 `GET /mcp` answers 405 with `Allow: POST, OPTIONS` and `Vary: Accept`. Never a 200 description page and never a cached JSON body. (On Netlify, a cached response also needs `Netlify-Vary: header=Accept`.) - [ ] 4.1.4 A notification (no `id`) gets HTTP 202 and no body. - [ ] 4.1.5 A tool that fails returns a normal result with `isError: true` and text the model can act on. A malformed request gets a JSON-RPC error. Argument validation failures are tool errors, so the model can fix its call. - [ ] 4.1.6 For 2025 era clients, keep dispatch errors (unknown method, unknown tool) at HTTP 200: older client transports throw on a non-2xx status before they read the JSON-RPC error. - [ ] 4.1.7 Every tool has `readOnlyHint: true` and `destructiveHint: false`. The MCP spec tells clients to treat annotations as untrusted, so the code itself must be read-only too. - [ ] 4.1.8 Every string argument has a `maxLength`, and any `pattern` sits next to a `maxLength` in the same schema object. A pattern such as `^(a+)+$` can take seconds on a short input, and nothing can interrupt a running regular expression. - [ ] 4.1.9 Output is strict JSON: lone UTF-16 surrogates are replaced before sending. Strict parsers (Rust `serde_json`) reject the whole message otherwise. - [ ] 4.1.10 A body cap (64 KB is enough for read-only tools) and a per-tool timeout. - [ ] 4.1.11 `instructions` says what the server is for, what it cannot see and what it will not do. ### 4.2 Server card template Serve the same bytes at three addresses: | Address | Written by | Why | |---|---|---| | `/mcp/server-card` | The MCP function itself, `application/mcp-server-card+json` when asked for it | The location the current server card extension reserves (`/server-card`), and the one your AI Catalog entry points to | | `/.well-known/mcp/server-card.json` | The build, from the same config | Older draft path that scanners still read | | `/.well-known/mcp.json` | The build, same bytes | Another scanner reads this one | ```json { "$schema": "https://static.modelcontextprotocol.io/schemas/v1/server-card.schema.json", "name": "com.example/site", "version": "1.0.0", "description": "Hours, machine sizes and prices, and how to report a problem at Example Laundry.", "title": "Example Laundry", "websiteUrl": "https://example.com/", "icons": [ { "src": "https://example.com/icon-512.png", "mimeType": "image/png", "sizes": ["512x512"] } ], "remotes": [ { "type": "streamable-http", "url": "https://example.com/mcp", "supportedProtocolVersions": ["2026-07-28", "2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05"] } ], "serverInfo": { "name": "com.example/site", "title": "Example Laundry", "version": "1.0.0", "description": "Hours, machine sizes and prices, and how to report a problem at Example Laundry.", "websiteUrl": "https://example.com/" }, "transport": { "type": "streamable-http", "endpoint": "https://example.com/mcp" }, "endpoint": "https://example.com/mcp", "capabilities": { "tools": { "listChanged": false } } } ``` - [ ] 4.2.1 The `$schema` value is an identifier. The server card schema requires exactly that URL, and on September 26, 2026 the URL itself answered 404. Validate against `schema.json` in https://github.com/modelcontextprotocol/ext-server-card instead of fetching the URL. - [ ] 4.2.2 The first block of fields (`$schema` through `remotes`) is the current card. `serverInfo`, `transport`, `endpoint` and `capabilities` are older draft fields some scanners still read; the schema allows extra members. - [ ] 4.2.3 `name` is reverse DNS with exactly one slash (`com.example/site`), and the server reports the same name and version at runtime. - [ ] 4.2.4 A build check fails when any of the three copies differs from the others. - [ ] 4.2.5 The static copies get `Content-Type: application/json; charset=utf-8`, `Access-Control-Allow-Origin: *`, `Access-Control-Allow-Methods: GET`, `Access-Control-Allow-Headers: Content-Type, If-None-Match`, `Access-Control-Expose-Headers: ETag` and `Cache-Control: public, max-age=3600`. - [ ] 4.2.6 Do not claim a `webmcp` transport unless your pages actually load WebMCP code. ### 4.3 One tool, as `tools/list` returns it ```json { "name": "search_site", "title": "Search the site", "description": "Read-only. Finds up to 5 pages on example.com whose own text matches the words in the query and returns each page's title, link and the sentence that matched. It never answers from outside the site and never states a price, hour or availability the page does not say.", "inputSchema": { "type": "object", "properties": { "query": { "type": "string", "minLength": 1, "maxLength": 200 }, "limit": { "type": "integer", "minimum": 1, "maximum": 5 } }, "required": ["query"], "additionalProperties": false }, "annotations": { "title": "Search the site", "readOnlyHint": true, "destructiveHint": false, "idempotentHint": true, "openWorldHint": false } } ``` ### 4.4 One answer, with the notice in both places ```json { "content": [ { "type": "text", "text": "Automated site information from example.com, quoted from the page linked. Not legal, medical or financial advice.\n\nOpen 24 hours a day, 7 days a week. (https://example.com/hours/)" } ], "structuredContent": { "notice": "Automated site information from example.com, quoted from the page linked. Not legal, medical or financial advice.", "quotes": [ { "text": "Open 24 hours a day, 7 days a week.", "source": "https://example.com/hours/" } ] }, "isError": false } ``` Notice lines that fit other kinds of business (write your own; these are patterns): - Housing or health adjacent: say what the business is and is not ("housing, not treatment"), say the answer is not medical advice, and put emergency numbers first. - AI-written content: say it is AI generated, and what it is not (news reporting, investment advice), in the text and in the structured data. - Anything with prices: say prices are quoted from the linked page as of the build, and that the page is the authority. ## 5. Tool honesty rules - [ ] 5.1 Every fact a tool returns is generated from your own pages or from the source-controlled data file your pages are built from. Nothing is typed into the tool by hand. - [ ] 5.2 The generator has a check mode that fails the build when a page changed and the facts did not. - [ ] 5.3 The tool imports from the source-controlled file, never from the publish folder (`_site/`, `dist/`), which may not exist yet or may hold the previous build. - [ ] 5.4 Every answer carries the link to the page it quotes. - [ ] 5.5 When the site states something several ways (a price on one page, "subject to change" in an agreement, "call for current rates" in the FAQ), the tool returns all of them as published and does not pick one. - [ ] 5.6 No tool states availability, price, admission, coverage or anything else the business has not published. - [ ] 5.7 If a tool quotes a price, a test runs the same inputs through the quote tool and the code that actually charges, and fails on any difference. - [ ] 5.8 No booking, lead, payment or other write tool. A "report a problem" tool returns instructions and files nothing. A tool that takes a URL returns a link that runs in the user's browser and never fetches the URL itself. - [ ] 5.9 Only same-site pages are listed. Bidirectional text controls (U+202A to U+202E, U+2066 to U+2069) are stripped from every string. - [ ] 5.10 A source check greps the tool code, with comments stripped, for write helpers, environment variable reads and outbound fetches. Treat a pass as a lint, not a proof, and still read the code. ## 6. Discovery files Point discovery files at the cards, not at the endpoints. The site guide reads llms.txt, so an `/a2a` link there would become a "page" it recommends. ### 6.1 llms.txt section ```text ## Machine-readable - [A2A agent card](https://example.com/.well-known/agent-card.json): JSON. Read-only site guide agent; send JSON-RPC SendMessage (A2A 1.0) or message/send (A2A 0.3) to the url listed in the card and get up to 5 pages from this site's page list. - [MCP server card](https://example.com/.well-known/mcp/server-card.json): JSON. Read-only MCP server over Streamable HTTP; the card names the endpoint. Tools: hours, machines and prices, how to report a problem. ``` ### 6.2 AGENTS.md section The AGENTS.md format is defined for code repositories. Publishing one at a website root is my own extension, not part of that format, and it doubles as the documentation URL a connector form asks for. ```markdown ## MCP server Endpoint: https://example.com/mcp (Streamable HTTP, JSON-RPC, POST only; GET answers 405). Card: https://example.com/mcp/server-card (also /.well-known/mcp/server-card.json). Protocol: 2026-07-28 (server/discover) and 2025 clients (initialize). No sessions. Authentication: none. Public and read-only. Rate limit: 60 requests a minute per IP. Tools (all read-only): - get_hours: opening hours, quoted from https://example.com/hours/ - get_machines: washer and dryer sizes and prices, quoted from https://example.com/prices/ - report_problem: tells you how to report a broken machine; it files nothing What an agent may say: only what a tool returns, with the page link. What an agent must not say: availability, discounts or anything the site does not publish. ``` ### 6.3 api-catalog (`/.well-known/api-catalog`, RFC 9727) Serve it with `Content-Type: application/linkset+json; profile="https://www.rfc-editor.org/info/rfc9727"`. The file has no extension, so set the type yourself. ```json { "linkset": [ { "anchor": "https://example.com/", "service-doc": [ { "href": "https://example.com/llms.txt", "type": "text/plain", "title": "Site summary (llms.txt)" } ] }, { "anchor": "https://example.com/a2a", "service-desc": [ { "href": "https://example.com/.well-known/agent-card.json", "type": "application/json", "title": "A2A Agent Card: read-only site guide (JSON-RPC, A2A 1.0 and 0.3)" } ], "service-doc": [ { "href": "https://example.com/llms.txt", "type": "text/plain", "title": "The pages the site guide can point to" } ] }, { "anchor": "https://example.com/mcp", "service-desc": [ { "href": "https://example.com/mcp/server-card", "type": "application/mcp-server-card+json", "title": "MCP Server Card: read-only MCP server, Streamable HTTP" } ], "service-doc": [ { "href": "https://example.com/AGENTS.md", "type": "text/markdown", "title": "How the MCP server works" } ] } ] } ``` ### 6.4 AI Catalog (`/.well-known/ai-catalog.json`) Serve it as `application/ai-catalog+json`. The entry key is `identifier`, not `id`: a file with `id` parses as JSON and is rejected whole. Each entry has exactly one of `url` or `data`. Give each one 2 to 5 `representativeQueries` as well: the ARD discovery spec asks for them, and the readiness scanner's ARD guide asks for 2 to 5. ```json { "specVersion": "1.0", "host": { "displayName": "Example Laundry", "identifier": "did:web:example.com", "url": "https://example.com" }, "entries": [ { "identifier": "urn:air:example.com:agents:site-guide", "displayName": "Example Laundry site guide (A2A agent)", "description": "Read-only A2A agent: send a question, get up to 5 matching pages from the site's page list with links.", "type": "application/json", "url": "https://example.com/.well-known/agent-card.json", "representativeQueries": ["What are your hours?", "Where do I report a broken machine?"] }, { "identifier": "urn:air:example.com:mcp:site", "displayName": "Example Laundry MCP server", "description": "Read-only MCP server (Streamable HTTP at https://example.com/mcp): hours, machines and prices, how to report a problem.", "type": "application/mcp-server-card+json", "url": "https://example.com/mcp/server-card", "representativeQueries": ["How much is a large washer at Example Laundry?", "Is Example Laundry open now?"] } ] } ``` ### 6.5 Link the surface from every page Put these two lines in the `` of every page template. Both relations are registered, so nothing reads them as a typo. ```html ``` - [ ] 6.5.1 The same relations can go in one `Link` response header on `/*`. On Netlify, two header rules that set the same header name for one path do not add up; one replaces the other. Use a single rule. - [ ] 6.5.2 In `netlify.toml`, write a `Link` header value as a single-quoted literal string. It contains double quotes, and an invalid string drops the whole headers block. - [ ] 6.5.3 Gate it by walking the chain on the live site: a page, then the catalog it links, then the file the catalog names. A validator that checks files one by one never asks whether a reader can reach them. ### 6.6 Keep every copy of a fact in step - [ ] 6.6.1 Counts and facts in agent files (tool counts, prices, coordinates, capacity) are computed at build time from the same source the pages use. - [ ] 6.6.2 When you fix a value, sweep `.txt`, `.md`, `.json` and `/.well-known/` files too, not only `.html`. - [ ] 6.6.3 Allowlist every field you publish. Internal data files carry notes next to the values, and spreading an object publishes them. ## 7. robots.txt Under RFC 9309 a crawler obeys only the group that names it, and a named group inherits nothing from `*`. Rules written after a comment or a blank line still belong to the group above them; only the next `User-agent` line starts a new group. ### 7.1 The simple form (one policy for everyone) ```text # Content-Signal: search = building a search index; ai-input = using pages to ground AI answers; # ai-train = training AI models. It states a preference; it does not block anything. User-agent: * Content-Signal: search=yes, ai-input=yes, ai-train=no Allow: / Disallow: /thank-you/ Sitemap: https://example.com/sitemap.xml ``` ### 7.2 The named form (when some crawlers need different rules) Several `User-agent` lines above one block form one group. Every named group repeats every `*` rule plus its own `Content-Signal` line. ```text User-agent: * Content-Signal: search=yes, ai-input=yes, ai-train=no Allow: / Disallow: /thank-you/ # Example policy only: decline the crawlers their operators describe as collecting # content for model training, welcome the search and user-request fetchers. User-agent: GPTBot User-agent: ClaudeBot User-agent: meta-externalagent Content-Signal: search=yes, ai-input=yes, ai-train=no Disallow: / User-agent: OAI-SearchBot User-agent: ChatGPT-User User-agent: Claude-SearchBot User-agent: Claude-User User-agent: meta-webindexer User-agent: facebookexternalhit Content-Signal: search=yes, ai-input=yes, ai-train=no Allow: / Disallow: /thank-you/ Sitemap: https://example.com/sitemap.xml ``` ### 7.3 Checks for either form - [ ] 7.3.1 Count groups and count `Content-Signal` lines. The two numbers match. - [ ] 7.3.2 Every named group carries every `Disallow` the `*` group has (unless it deliberately blocks everything). - [ ] 7.3.3 Content-Signal is not a standard. Its three categories come from an individual Internet-Draft that expired in April 2026. Nobody is obliged to honor it. - [ ] 7.3.4 For Meta: meta-webindexer is the documented lever for citations in Meta AI answers and facebookexternalhit builds link previews, so keep both allowed. meta-externalagent is the training crawler, a separate decision. Meta says meta-externalfetcher may bypass robots.txt because a person asked for the page. Meta says its crawlers may cache robots.txt for up to 24 hours. - [ ] 7.3.5 Match crawler tokens as case-insensitive substrings in firewall and log rules, not as a prefix of the User-Agent header. - [ ] 7.3.6 Do not build user agent rules or IP allowlists for Meta's Muse agent. Meta documents neither. - [ ] 7.3.7 Test it: https://jwatte.com/tools/robots-txt-simulator/ ## 8. Netlify plumbing ### 8.1 Function config (path routing and rate limit) Put each function's files in a subfolder: `netlify/functions/mcp/mcp.mjs` plus its helpers. Netlify deploys every top-level file in `netlify/functions/` as its own function, so flat helper files become broken endpoints. ```js // netlify/functions/mcp/mcp.mjs (end of file) export const config = { path: ["/mcp", "/mcp/server-card"], rateLimit: { windowLimit: 60, windowSize: 60, aggregateBy: ["ip", "domain"] }, }; ``` - [ ] 8.1.1 Route by the function's own `path` (or a 200 rewrite). Never a 301: a redirect turns the JSON-RPC POST into a GET and drops the body. - [ ] 8.1.2 Past the limit Netlify answers 429. Free, Starter and Personal plans get 2 code-based rules per project and Pro gets 5 (Netlify's rate limiting docs, read September 26, 2026). An A2A agent plus an MCP server use both of the smaller allowance. Check the deploy log that the rule was applied. - [ ] 8.1.3 Keep these endpoints out of any Origin or Referer allowlist guard. They exist for outside agents, and such a guard refuses every one of them. - [ ] 8.1.4 Pace your own checkers under the limit. An unpaced probe burst gets 429s and can get your IP refused by the host for a while. ### 8.2 Edge functions that run on every path ```js // netlify/edge-functions/markdown.js (end of file) export const config = { path: "/*", excludedPath: ["/a2a", "/mcp", "/mcp/*", "/*.md"], }; ``` - [ ] 8.2.1 An edge function on `/*` runs before your functions and can answer their POSTs. Exclude the agent paths. - [ ] 8.2.2 After deploying, `GET /mcp` must be the function's 405 JSON. A 200 HTML page means something else answered. - [ ] 8.2.3 If the site's folder sits inside another project folder, check that the new exclusion reached production. Nested layouts have shipped a stale edge bundle. - [ ] 8.2.4 If the edge function rewrites HTML, skip it for requests that carry a `Range` header. A 206 must carry exactly the bytes its `Content-Range` promises. - [ ] 8.2.5 If it serves markdown to agents, send `Vary: Accept` on both the markdown and the HTML answers, and keep `*/*` (what curl sends) on HTML. ### 8.3 Keep function source private when you publish from the repo root Only needed when the publish folder is `.`. Better still, stage the publish tree without the `netlify/` folder. ```toml [[redirects]] from = "/netlify/*" to = "/404.html" status = 404 force = true ``` Redirect rules match case-sensitively, while static files are served case-insensitively and `%2F` is decoded, so also refuse every spelling in the edge function that runs first: ```js function isSourcePath(pathname) { let p = pathname; for (let i = 0; i < 3; i++) { let next = p; try { next = decodeURIComponent(p); } catch (e) { break; } if (next === p) break; p = next; } p = p.replace(/\\/g, "/").replace(/\/{2,}/g, "/").toLowerCase(); return p === "/netlify" || p.startsWith("/netlify/"); } // first line of the edge handler: // if (isSourcePath(new URL(request.url).pathname)) return new Response("Not found\n", { status: 404 }); ``` - [ ] 8.3.1 Probe `/NETLIFY/functions/...`, `/Netlify/functions/...` and `/netlify%2Ffunctions%2F...` on the draft. All must be 404 or 400 with no source in the body. ### 8.4 Long caching without caching 404s A header rule such as `for = "*.png"` is matched on the request path, so it also stamps `immutable, max-age=31536000` on a 404. A web app manifest icon that did not exist yet is then cached as a year-long 404. ```toml # Exact paths only; a new image file needs its own block. [[headers]] for = "/favicon.ico" [headers.values] Cache-Control = "public, max-age=31536000, immutable" # Icons and screenshots the manifest names: a moderate TTL. [[headers]] for = "/icon-512.png" [headers.values] Cache-Control = "public, max-age=604800" ``` - [ ] 8.4.1 Request `/does-not-exist.png` and check the 404 does not carry `immutable`. - [ ] 8.4.2 If a broken icon URL was already published, publish the icon under a new filename and point the manifest at it. ## 9. Service worker bypass Agent endpoints and discovery files answer machines and change with the server behind them. They must never come from an offline cache. ```js // sw.js: bump this whenever the rules change, and delete older caches in "activate" const CACHE = "site-v6"; function isAgentEndpoint(pathname) { const p = pathname.toLowerCase(); return p === "/a2a" || p.startsWith("/a2a/") || p === "/mcp" || p.startsWith("/mcp/") || p.startsWith("/.well-known/"); } self.addEventListener("fetch", (event) => { const url = new URL(event.request.url); if (url.origin !== self.location.origin) return; if (event.request.method !== "GET") return; // POSTs to /a2a and /mcp go straight to the network if (isAgentEndpoint(url.pathname)) return; // never cached, never answered from cache // ...your normal caching strategy here; pass { ignoreVary: true } to every cache.match }); ``` - [ ] 9.1 In a fresh browser profile, confirm the agent URLs are not served by the worker (DevTools shows the response did not come from the service worker). - [ ] 9.2 After the version bump, the old cache is gone and the new worker controls the page. ## 10. WebMCP (tools a page registers in the browser) ```js (function () { try { const mc = document.modelContext || navigator.modelContext; if (!mc) return; const tools = [ { name: "get_hours", description: "Read-only. Returns Example Laundry's opening hours as published on https://example.com/hours/.", inputSchema: { type: "object", properties: {}, additionalProperties: false }, annotations: { readOnlyHint: true }, execute: async () => ({ content: [{ type: "text", text: "Open 24 hours a day, 7 days a week. (https://example.com/hours/)" }] }), }, ]; if (typeof mc.registerTool === "function") { for (const t of tools) { try { mc.registerTool(t); } catch (e) { /* keep going */ } } } else if (typeof mc.provideContext === "function") { mc.provideContext({ tools }); // older builds only; removed from the spec } } catch (e) { // WebMCP must never break the rest of the page } })(); // Service worker registration and anything else in this file go OUTSIDE the block above. ``` - [ ] 10.1 `document.modelContext.registerTool` is what the current draft, Chrome's origin trial and ChatGPT's Site tools use. `provideContext` was removed from the spec. - [ ] 10.2 ChatGPT's Site tools do not pick up tools declared with HTML form attributes, or tools registered inside iframes. Register imperatively in the top page. - [ ] 10.3 Test the page with the API missing, with `registerTool` throwing, and with only `provideContext` present. Nothing else on the page may break. - [ ] 10.4 WebMCP is a Draft Community Group Report, not a W3C standard. ## 11. Link previews - [ ] 11.1 One `og:image` per page, inside ``, as an absolute URL. - [ ] 11.2 The image is at most 8 MB, at least 200 by 200, ideally 1200 by 630, and under 600 KB so WhatsApp shows it too. - [ ] 11.3 `og:image:width`, `og:image:height` and `og:image:type` match the real file. Measure it; do not copy the numbers from another page. - [ ] 11.4 The `` sits in the first 300 KB of the HTML (WhatsApp) and the Open Graph tags in the first 1 MB (Facebook). - [ ] 11.5 A replaced image gets a new filename. Meta caches images by URL. Leave the old file in place. - [ ] 11.6 WebP rendered as a large card in a Facebook Sharing Debugger test on September 24, 2026. SVG did not (a small cropped square). Do not convert WebP to JPEG for Facebook's sake, and do not add `fb:app_id` just to quiet the debugger. - [ ] 11.7 Check: https://jwatte.com/tools/meta-ai-crawler-audit/ and https://jwatte.com/downloads/meta-crawler-preview-checklist.md ## 12. What not to publish - [ ] 12.1 No OAuth or Protected Resource Metadata when nothing is protected. A 404 at `/.well-known/oauth-protected-resource` says "no auth". - [ ] 12.2 No auth.md that has to claim a flow you do not run to pass a scanner. Leaving that check red is the honest result. - [ ] 12.3 No commerce manifests or UCP profile without a checkout behind them. - [ ] 12.4 No Web Bot Auth key directory unless you operate a bot that signs its requests. - [ ] 12.5 No card, manifest or catalog entry for anything that is not live. ## 13. Meta Muse connector worksheet There is no website standard or discovery file for Muse. The only way in is the reviewed form at https://muse.ai/platform, behind a Meta login. Fill this in first, then paste. **Preconditions (all must be true):** - [ ] Production is live and the live MCP check shows 0 failures. - [ ] AGENTS.md (or your documentation URL) already describes the server as it runs today. - [ ] Privacy, terms and contact pages answer 200. - [ ] If the business is not yours, the owner has said, in writing, that you may submit for them. **Step 1, overview:** | Field | Your value | |---|---| | Connector name | Example Laundry | | Company or developer | Example Laundry LLC | | Product website | https://example.com | | Example prompts (one per line) | 3 or 4 questions you have already asked the live server and seen answered correctly | | Icon | 512 by 512 PNG, the site's own app icon, byte for byte | | Payments | My connector does not accept payments | | Category | The honest closest fit. A housing site is not a medical category. | | Support email or URL | https://example.com/contact/ | | Privacy policy | https://example.com/privacy/ | | Terms of service | https://example.com/terms/ | | Anything else | One paragraph: what each tool returns, that it is read-only, that nothing books, pays or submits, and the notice line every answer carries | **Step 2, technical:** | Field | Your value | |---|---| | Connection type | Existing MCP | | Hosted MCP endpoint | https://example.com/mcp | | Authentication | Other: "None. Public, read-only server; no API key or OAuth." | | Documentation URL | https://example.com/AGENTS.md | | Access requirements | None. Public and read-only; rate limit 60 requests a minute per IP. | **Proof table (keep it with the form):** | Example prompt | Tool that answers it | What it returned on the live server, and when | |---|---|---| | | | | - [ ] One connector per business. Several sites run by one business get one connector, not one each. - [ ] Meta reviews connectors "for functional, security, and legal requirements". Whether it accepts read-only connectors is not documented. Record the outcome when Meta writes back. ## 14. Verification checklist ### 14.1 Before any deploy (local) - [ ] The built card files are exactly what the config produces (a `--check` mode that fails the build). - [ ] The whole protocol runs in-process against your own function: A2A methods and error codes; MCP `initialize`, `server/discover`, `tools/list`, a call to every tool with a real argument, and the error cases. - [ ] Every JSON file you publish parses. - [ ] robots.txt group count equals Content-Signal count. ### 14.2 On the draft ```text curl -si https://DRAFT.example.net/a2a | head -5 expect 405 and Allow: POST, OPTIONS curl -s -X POST https://DRAFT.example.net/a2a -H "content-type: application/json" -H "A2A-Version: 1.0" -d '{"jsonrpc":"2.0","id":1,"method":"SendMessage","params":{"message":{"messageId":"m1","role":"ROLE_USER","parts":[{"text":"How do I contact you?"}]}}}' expect a result.message whose first text says it is an automated site guide curl -si https://DRAFT.example.net/mcp -H "accept: text/event-stream" | head -5 expect 405, Allow: POST, OPTIONS, Vary: Accept curl -s -X POST https://DRAFT.example.net/mcp -H "content-type: application/json" -H "accept: application/json, text/event-stream" -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}' expect a result with serverInfo and no Mcp-Session-Id header curl -si https://DRAFT.example.net/mcp/server-card -H "accept: application/mcp-server-card+json" | head -12 expect 200 application/mcp-server-card+json, and the same bytes as both .well-known copies ``` - [ ] Hostile input: a question with a link, markup and "ignore previous instructions" returns only bare words; an unknown tool name gets `-32602`; an over-long query is a tool error; a body over the cap gets 413. - [ ] Source paths in several spellings return 404 or 400 with no source in the body. - [ ] A missing `.png` returns a 404 without `immutable`. - [ ] A Range request's 206 body length equals its `Content-Range`, for both plain and gzip responses. - [ ] The service worker does not serve any agent URL. - [ ] Every example prompt you plan to publish gets the answer you expect. ### 14.3 With the official client libraries - [ ] MCP: `@modelcontextprotocol/sdk` (the 1.x `Client` with `StreamableHTTPClientTransport`) connects, lists tools and calls each one. `@modelcontextprotocol/client` 2.x does the same with version negotiation set to auto (it should reach 2026-07-28 through `server/discover`). - [ ] A2A: `@a2a-js/sdk` 1.x sends `SendMessage` and gets a Message; a 0.3 release of the same package sends `message/send` and gets a 0.3 Message; `GetTask` maps to task not found. - [ ] Feed each client one deliberately tampered answer (a `structuredContent` that breaks the tool's `outputSchema`). If the client does not throw, it was not validating, and the passing run proved less than you thought. ### 14.4 Live, after production - [ ] Run the live checkers once, paced. They send dozens of requests, and a second run inside a minute trips a 60 a minute limit. - [ ] If you use a readiness scanner, call its API for one site at a time, read each check's own spec for exact field names, and confirm every fail with a direct fetch. Scanners have passed a server card on file presence alone, passed a Content-Signal line that only one group could see, and reported a stale copy of a file. - [ ] Walk the chain from a real page: head link, catalog, card, endpoint. - [ ] Compare the served bytes (or ETags) with what you built. ## 15. Public examples worth copying All fetched and checked on September 26, 2026. | What | Where | Copy this part | |---|---|---| | AI Catalog to server card | https://github.com/.well-known/ai-catalog.json | One entry pointing at `/server-card`, served with the typed media type | | AI Catalog with a did:web host | https://developers.cloudflare.com/.well-known/ai-catalog.json | `identifier`, `url`, tags and 2 representative queries per entry | | Read-only public MCP server | https://learn.microsoft.com/api/mcp | Every tool titled and marked read-only; instructions that say what each tool returns and when to use it | | Read-only MCP on a marketing site | https://www.pinecone.io/mcp/ | One search tool, and instructions that name where its scope stops | | Honest server instructions | https://docs.pinecone.io/mcp | "Do not claim access to private or authenticated content"; the one write tool marked as such | | Discovery chain from a header | https://blog.cloudflare.com/ | `Link: ; rel="api-catalog"` on the home page | | RFC 9727 catalog | https://developers.cloudflare.com/.well-known/api-catalog | Media type with profile; service-desc, service-doc and status | | Catalog for an API on another host | https://vercel.com/.well-known/api-catalog | The `item` relation, then a description of that host | | llms.txt skeleton | https://llmstxt.org/llms.txt | H1, blockquote, link lists | | llms.txt with rules for agents | https://vercel.com/llms.txt | "Ask for approval before changing account resources." | | llms.txt size variants | https://svelte.dev/llms.txt | Small, medium and full versions, with notes on what each drops | | Markdown negotiation | https://docs.stripe.com/testing | `Vary: Accept` on both the markdown and the HTML answers | | One robots group with Content-Signal | https://vercel.com/robots.txt | Mixed signals, and preview image routes allowed under a broad Disallow | | Named AI group done right | https://github.com/robots.txt | Several AI tokens in one group that repeats every `*` Disallow | | Crawler token list | https://www.nytimes.com/robots.txt | A checklist of current tokens, even if your policy is the opposite | | WebMCP for a small restaurant | https://googlechromelabs.github.io/webmcp-tools/demos/french-bistro/ | A form with `toolname` and `tooldescription`, plus `registerTool` in script | | A2A card that validates | https://jwatte.com/.well-known/agent-card.json | The whole card; the agent behind it answers at https://jwatte.com/a2a | | A2A skills block | https://www.tavily.com/.well-known/agent-card.json | Skills with concrete examples and per-skill media types (take the interface block from the A2A spec) | | 401 that tells the client what to do | https://mcp.stripe.com | `WWW-Authenticate` with `resource_metadata`, and a body that links the docs (only relevant if you have auth) | ## 16. Sources - A2A specification: https://a2a-protocol.org/latest/specification/ - MCP specification 2026-07-28: https://modelcontextprotocol.io/specification/2026-07-28 (tools and security considerations: /server/tools; authorization: /basic/authorization) - MCP server card discovery: https://github.com/modelcontextprotocol/ext-server-card (docs/discovery.md, schema.json) and SEP-2127: https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2127 - AI Catalog: https://github.com/Agent-Card/ai-catalog ; ARD discovery spec (representativeQueries): https://agenticresourcediscovery.org/spec/ - llms.txt: https://llmstxt.org/ - AGENTS.md: https://agents.md/ - RFC 9309 (robots.txt): https://www.rfc-editor.org/rfc/rfc9309 - RFC 9727 (api-catalog): https://www.rfc-editor.org/rfc/rfc9727 and RFC 9264 (linkset): https://www.rfc-editor.org/rfc/rfc9264 - RFC 9728 (Protected Resource Metadata): https://www.rfc-editor.org/rfc/rfc9728 - RFC 8246 (immutable): https://www.rfc-editor.org/rfc/rfc8246 - Content Signals categories (individual Internet-Draft, expired April 4, 2026): https://datatracker.ietf.org/doc/draft-romm-aipref-contentsignals/ - WebMCP draft: https://webmachinelearning.github.io/webmcp/ ; Chrome: https://developer.chrome.com/docs/ai/webmcp/imperative-api ; ChatGPT Site tools: https://learn.chatgpt.com/docs/webmcp - Meta crawlers: https://developers.facebook.com/docs/sharing/webmasters/web-crawlers/ ; images: https://developers.facebook.com/docs/sharing/webmasters/images/ ; WhatsApp previews: https://developers.facebook.com/documentation/business-messaging/whatsapp/link-previews - Muse connectors: https://muse.ai/platform - Netlify rate limiting: https://docs.netlify.com/manage/security/secure-access-to-sites/rate-limiting/ - Client libraries: https://github.com/modelcontextprotocol/typescript-sdk and https://github.com/a2aproject/a2a-js Related free tools (no signup): https://jwatte.com/tools/agent-ready-audit/ , https://jwatte.com/tools/mcp-server-audit/ , https://jwatte.com/tools/llms-txt-validator/ , https://jwatte.com/tools/well-known-audit/ , https://jwatte.com/tools/service-worker-audit/ , https://jwatte.com/tools/web-app-manifest-audit/ , https://jwatte.com/tools/agent-rate-limit-probe/