I asked a simple question about one of my sites: how does it respond to MCP? The answer was mostly good. The server card was where it should be, the endpoint answered every protocol revision it claimed, and every tool returned an answer with a link to cite. Two things looked off. One of them was my mistake. The other was a header that disagreed with itself, and it turned out to be on every site I run that carries an AI agent endpoint: 28 live sites and one more that deploys on a schedule.
The fix was one line of code. Getting that line onto 28 production sites, two at a time, without shipping a single other change by accident, is what this post is about. The method transfers to any small fleet of static sites with serverless functions, whatever the change is.
Check your own endpoints. The Agent Endpoint Inspector runs every check in this post against any site, read-only: it lists each MCP tool with its declared behavior and each A2A skill, then tests the cards, the preflight, the GET answer and both MCP protocol generations. The fix guide (save) walks through each finding: diagnose, decide, fix, verify. Free, no signup. The Mega Analyzer runs the same checks whenever a site publishes an MCP or A2A card.
The header that said two things
An MCP server over Streamable HTTP takes JSON-RPC by POST. A client may also try an HTTP GET to open a server-to-client stream, and the spec gives the server exactly two choices: answer with an event stream, or answer 405 Method Not Allowed to say it offers no stream (MCP). Mine are read-only and stateless, so GET gets the 405, with Allow: POST, OPTIONS, as RFC 9110 requires of every 405 (RFC 9110).
The browser preflight told a different story. OPTIONS /mcp answered Access-Control-Allow-Methods: POST, GET, OPTIONS. The same was true of /a2a, the A2A agent endpoint that sits next to it on the same sites. So one response said "GET works" and the next one said "GET is not allowed."
Why it broke nothing, and why I fixed it anyway
Here is the part I had to look up before I trusted it. A browser's preflight check only fails a method that is missing from Access-Control-Allow-Methods and is also not a "CORS-safelisted method" (Fetch Standard). The safelisted methods are GET, HEAD and POST (Fetch Standard). So listing GET never changed what a browser would do. Strictly, listing POST doesn't either. The header only matters for methods like PUT, PATCH and DELETE.
I fixed it for two reasons. First, people and scanners read headers literally. An agent-readiness checker, a security review or the next developer will see "GET allowed," try it, get a 405, and spend time on a contradiction that exists only on paper. Second, Allow is the header with a defined meaning: it lists "the set of methods advertised as supported by the target resource" (RFC 9110). Making the preflight say the same thing costs nothing and removes a question.
The false alarm first
The other thing that looked off was my own test. The server card listed five protocol revisions, newest 2026-07-28. I sent an initialize asking for 2026-07-28 and the server answered with 2025-11-25. That looked like the card claiming something the server wouldn't do, and I reported it as a bug.
It wasn't. The 2026-07-28 revision dropped the initialize handshake altogether. Every request now carries its own protocol version and client capabilities in its body, mirrored into HTTP headers for intermediaries (MCP 2026-07-28). An initialize request is by definition the older handshake, so the server correctly answered it with the newest older revision it speaks. When I tested the new revision the way it's actually spoken (a discovery request with the version in the body and the headers to match), it answered, listed its tools and ran them.
The lesson I took: when a server supports several protocol generations, test each generation in its own dialect. A probe written for one era will report the other era as broken.
Fix it once, at the source
None of these sites has its own MCP or A2A code. Each one copies two small "kits", one for MCP and one for A2A, and a few sites pin the copied file by its SHA-256 hash in their build checks so it can't drift. So the fix went into the kits, not the sites: one line in each, a version bump (MCP kit 1.1.1, A2A kit 1.2.1), and a new test assertion that the preflight no longer lists GET. Both kits' full test suites passed before any site saw the change.
That made every site's change identical and checkable: replace one file with a file whose hash is known, and nothing else. On the sites that pin the hash, the pin moved in the same commit.
The redeploy discipline
Each of these sites deploys from a folder on my machine, not from a repository that rebuilds on push. That is convenient and it is also the risk: a redeploy ships whatever is in the folder, including edits that were never meant to go live. Over 28 sites, some folder would have something in it. Here is the routine that kept the change to one file.
- Prove the folder equals what is live, and that it isn't behind the repository. Netlify will list every file in the published deploy with its SHA-1. I hashed every local file and compared; any difference stopped that site. That check has a blind spot, and one site found it for me: its local folder was missing posts published from another machine, and the live site had already lost them in an earlier deploy from the same stale folder. Folder and live matched, and both were wrong. The repository had the posts. Compare with the repository too, before you build.
- Check nobody else deployed in between. Each site's currently published deploy id had to equal the one I recorded at the start. Other work was in progress on one of my sites that night; it asked me to hold off, then told me when it was done, and the check would have caught a collision either way.
- Check the code the static comparison can't see. Function source isn't in the static file list. For the group of sites with a live booking system, the check refused any site where a function or edge function file had been modified after its last release. A Netlify deploy id begins with its creation time in hex, which made "after its last release" exact.
- Deploy two at a time. Small enough to watch every result, large enough to finish. A failure stops the run before the next pair.
- Prove the static files are unchanged after the deploy. The same hash comparison again, now against the new deploy. Every site came back identical, file for file, apart from the regenerated server card where the version moved.
- Test the behavior, not the status code. The preflight had to answer
POST, OPTIONS, GET had to stay 405, POST had to still answer, and each kit's own live checker had to pass (23 checks for A2A, 50 to 62 for MCP depending on the site's tools).
Two details from the run are worth knowing if you try this.
Netlify's edge function bundle includes a compressed archive alongside the code bundle. On sites whose deploy rebuilds, that archive gets a new hash every time even when the code is byte-identical, because archives carry timestamps. The code bundle itself (the .eszip) matched the old one exactly, and the edge functions answered the same. A rule that says "the edge bundle must not change" needs to look at the right file.
And right after one deploy, the A2A checker failed two of its 23 checks: the agent said it couldn't read the site's llms.txt or sitemap. Both files answered in a fifth of a second. The previous deploy, tested directly, behaved the same as the new one, and two minutes later the new one passed 23 of 23 twice in a row. The agent's first index fetch had failed during the switchover and was briefly cached. Retry before you roll back.
Different sites, different doors
Not every site deploys the same way, and the method had to bend without breaking:
- One site deploys whenever its main branch is pushed. There, the push was the deploy, and its own test suite ran first. Five of its 70 smoke gates failed, and I checked them against the code without my change: they failed identically. A gate that fails before and after a change is information about the gate, not about the change.
- One site deploys from a weekly scheduled job. That one got a commit on its main branch and no deploy; it ships with the next scheduled run.
- One site has a local file that differs from production on purpose, an edit that was never reviewed. Its deploy used a staging copy with the live bytes of that file pulled back from Netlify, so the unreviewed edit stayed home.
- One site ships through a "digest" deploy that sends Netlify a list of file hashes and uploads only what's new. Its dry run had to report exactly two replaced files (the server cards) before it went live.
Then the repositories
Every site has a private GitHub repository, created during a server migration a week earlier. I was asked to make sure each one actually holds the site's files. At first glance most looked badly out of date. Nearly every file differed.
They weren't. Git stores text with Unix line endings and my working folders use Windows line endings. Compared byte for byte, every text file "differed." Compared the way Git compares them, most repositories were complete, and the real gaps were small: a site's build helper scripts, two public pages, a blog post and its image, and one site's whole agent layer.
Two things made me glad I synced additively (add and update, never delete) and reviewed every list before writing:
- One source folder held saved copies of social media pages, kept as reference. They carried the session tokens those pages embed. They aren't site files, and they stayed out.
- On one site the repository was ahead of production in a way that mattered: the migration had made a security fix in the repository that was never deployed. Copying the local folder over it would have quietly undone the fix. That site got the kit file and nothing else, and the fix is now on my list to ship properly.
A secret scanner ran over every repository before each commit. It found nothing, which is the outcome you want from a scanner and not a reason to skip reading the list yourself.
What the inspector found on the first day
Pointing the new inspector at real servers, including my own, turned up things worth knowing:
- Some MCP servers answer GET with 405 but send no
Allowheader, which RFC 9110 requires on every 405. - Many tools declare nothing about their behavior. MCP lets a tool say it is read-only or destructive (
readOnlyHint,destructiveHint), and clients use those hints to decide what needs your confirmation. Without them, a careful client has to treat every tool as possibly destructive. - One well-known server refuses any request that carries an unfamiliar
Origin. That is correct: the spec asks servers to validateOriginagainst DNS rebinding. It also means a checker that sends a made-upOriginon every call will report a healthy server as broken. The inspector sends protocol calls without one, like desktop clients do, and tests browser access separately through the preflight. - Server cards come in at least three shapes: the current one with a
remoteslist, an older draft with a singleendpoint, and cards that declare only browser-side WebMCP tools with no HTTP endpoint at all. A checker that knows only one shape will call the other two broken.
What changed, in numbers
- 2 kits fixed, with 148 and 134 tests passing.
- 28 sites deployed and verified live, and 1 more queued for its scheduled deploy.
- 7 MCP endpoints and 28 A2A endpoints now say
POST, OPTIONSin both places. - 0 static files changed by any redeploy, apart from the regenerated server cards.
- 1 false alarm of my own, caught by testing the newer protocol the way it is actually spoken.
Fact-check notes and sources
- CORS-safelisted methods (GET, HEAD, POST): Fetch Standard, CORS-safelisted method.
- Preflight method check fails only for a method that is unlisted and not safelisted: Fetch Standard, CORS-preflight fetch.
- Allow lists the methods the resource supports; a 405 must carry Allow: RFC 9110 section 10.2.1 and section 15.5.6.
- MCP GET returns an event stream or 405; Origin validation; the MCP-Protocol-Version header: MCP 2025-11-25 transports.
- 2026-07-28 drops the initialize handshake; per-request version metadata: MCP 2026-07-28 transports overview.
Related reading
- Agent Endpoint Inspector, the free tool behind the checks above
- MCP servers for small business
- Netlify Pretty URLs: when to leave it on, and when it splits every page in two