How to Make Your Site Usable by AI Agents: llms.txt, MCP, and GEO
An increasing share of the traffic evaluating your business isn't a person. It's a model acting for one — deciding whether you can do the job, and increasingly whether to call you directly rather than hand a link back to a human.
Most websites are close to unusable by that reader, and you can't tell from looking at yours. Mine had an llms.txt — the file you publish specifically so AI models understand your business — that documented a POST /api/hire endpoint which has never existed. I'd been advertising a broken contract for months to precisely the audience least able to recover from it. No human ever noticed, because no human ever tries to use llms.txt as a contract.
I found that by running my site through an agent-readiness scanner, fixing everything it surfaced, and re-running it. This article is the whole thing: what actually makes a site usable by an AI agent, how that differs from GEO and AEO, the single change that mattered most, every fix in order, the three checks that score zero without telling you why, and the measurement trap that cost me a full deploy cycle. All of it verifiable — the scan is public.
What makes a site usable by AI agents
A human on a landing page tolerates ambiguity. They'll infer that "let's talk" means there's a contact form, scroll to find your rates, read a testimonial for reassurance. An agent does none of that. It wants a machine-readable answer at a predictable URL, and if it can't get one it moves to a site that gives it one. So the criteria aren't about design or copy. They're whether the following are true:
- Can an agent discover what you offer without parsing marketing prose?
- Can it call you — an actual endpoint, not a form for a human to fill in?
- Can it do that without credentials, or at minimum sign up for them itself?
- When something goes wrong, does it get a parseable error or an HTML page about a sad robot?
- Does a
200mean the page is real, and a404mean it genuinely isn't?
That last one sounds trivial and isn't. A site that returns 200 with a soft "page not found" body teaches an agent that your status codes are noise, which means it can't trust any of them.
GEO, AEO, and agent-readiness are three different problems
These get mashed together, and the distinction is the most useful thing in this article.
Generative engine optimization (GEO) is about being cited — appearing inside an answer that ChatGPT, Claude, Perplexity or Google's AI Overviews generates about your market. Answer engine optimization (AEO) is the near-synonym most people use for being the answer to a direct question, including featured snippets and voice assistants. In practice the tactics overlap almost entirely: answer real questions plainly, publish checkable facts, use structured data, keep it current.
Agent-readiness is a different axis entirely. GEO and AEO ask will an AI mention me when describing my market? Agent-readiness asks can an AI actually use me? — call an endpoint, retrieve my terms, start the engagement.
You can win the first completely and fail the second. A site with excellent content marketing gets cited constantly and still hands the agent nothing to act on, at which point the agent recommends whoever it can transact with. As models move from answering questions to completing tasks, the second axis is the one that decides whether you're in the consideration set or just in the summary.
Both are worth doing. But GEO gets all the attention right now because it looks like SEO, and agent-readiness is where the actual scarcity is.
One scan, two very different verdicts
Something worth knowing before you optimize for any score: is-agentic.com and ora.ai render the same underlying scan record, and grade it completely differently.
I confirmed this by comparing timestamps — identical scannedAt on both. On the pre-fix record, is-agentic showed 72/100 across 29 eligible checks grouped as Essential / Recommended / Bonus, while ora.ai showed 43/100, grade D across roughly 127 checks grouped into Discovery / Access / Usability / Payments.
Same site, same moment, same crawl. A 29-point spread, purely from rubric design.
The takeaway isn't that one is wrong. It's that the score is a rendering choice and the findings are the real artifact. Fix a finding and both numbers move. Optimize for a number and you'll thrash. Read the underlying check list on both, deduplicate it, and work that.
The single biggest lever: a public MCP server
If you do one thing from this article, do this one.
I already had two MCP servers running on this domain. Both were behind authentication, because both do real work for actual clients. From an agent's point of view that is identical to having none — it arrives, gets a 401, and leaves.
So I built a third: public, read-only, no credentials at all.
curl -s -X POST https://eduardocruz.com/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize",
"params":{"protocolVersion":"2025-06-18","capabilities":{},
"clientInfo":{"name":"demo","version":"1"}}}'
That returns a real handshake — serverInfo.name: eduardocruz, protocol 2025-06-18 — and instructions written for the agent rather than for you. Three tools (get-profile, get-services, get-engagement) and five resources, including the llms.txt corpus itself.
On Laravel this is genuinely small. With laravel/mcp, a server is a class with two arrays:
#[Name('eduardocruz')]
#[Version('1.0.0')]
#[Instructions(/* what an agent should use this for */)]
class PublicServer extends Server
{
protected array $tools = [GetProfile::class, GetServices::class, GetEngagement::class];
protected array $resources = [ProfileResource::class, ServicesResource::class, /* ... */];
}
and one route:
Mcp::web('/mcp', PublicServer::class)->middleware('throttle:120,1');
Two details that cost me time. Tool names resolve to kebab-case — my GetProfile class is exposed as get-profile, and my hand-written instructions told agents to call getProfile, which doesn't exist. And resources default to an ugly derived name (ProfileResource became profile-resource) unless you set #[Name('profile')] explicitly. Both work fine in a test suite and read as broken to a real agent.
The deeper question is what to expose. The temptation is to gate the good stuff. But an agent evaluating whether to recommend you will not fill in a form to find out. Everything on that server is what I'd tell any prospect on a first call — what I do, what it costs, whether I'm available. There is no competitive advantage in making a machine work to learn my hourly rate.
Your llms.txt is probably lying
This is the cheapest fix in this article and the one I'd bet most sites need.
llms.txt is a convention, not a standard: a plain-text file at your domain root describing what you are and where the machine-readable versions live. Google has said it isn't a ranking factor, so it does nothing for classic SEO. Its value is GEO — when a model builds an answer about you, it works from a source you control instead of inferring from page copy.
Which makes it dangerous when it drifts. Mine had accumulated months of rot:
POST /api/hire— documented, never shipped. The real route isPOST /api/hire-leads./api/availability— didn't exist.- A
/legalpage and a logo URL, both 404. - An availability claim that contradicted my own landing page.
A human hitting any of these shrugs and finds the right page. An agent fails. Go curl every URL your llms.txt advertises right now — it takes two minutes and I'd be surprised if you're clean.
I also fixed it at the source rather than by editing text. Everything — the REST API, the MCP tools, the OpenAPI spec, the llms.txt content — now reads from one PHP class, so the surfaces are structurally incapable of disagreeing:
final class PublicProfile
{
public const RATE_USD_PER_HOUR = 60;
public const AVAILABILITY_HOURS_PER_WEEK = 20;
// profile(), services(), engagement(), whenToUse()
}
If you take one architectural idea from this article: agent surfaces multiply, and hand-maintained duplicates of the same facts will drift. One source, many renderings.
Every fix, in order of return
| # | Fix | Why it counts |
|---|---|---|
| 1 | Public MCP server at /mcp, zero auth |
Turns "reads about you" into "can call you" |
| 2 | Versioned REST API — /api/v1/{profile,services,engagement,when-to-use} |
Not every agent speaks MCP. JSON over HTTP is the universal floor |
| 3 | OpenAPI 3.1 spec at /openapi.json |
What makes your API function-callable. Needs operationId + descriptions on every operation |
| 4 | RFC 9457 problem+json on every /api/* error |
An agent can parse {"type","title","status","detail"}. It cannot parse your styled 500 page |
| 5 | Markdown 404 with recovery links, Vary: Accept |
A wrong guess becomes a redirect instead of a dead end |
| 6 | Plain pages — /about, /contact, /docs — no JS required |
Trust signals, and the fix for a check explained below |
| 7 | /.well-known/ai-catalog.json + an MCP server card |
The machine-readable index of every agent surface you have |
| 8 | /auth.md stating plainly that no auth is required |
"No credentials needed" is itself an answer agents look for |
| 9 | contactPoint + address on the Organization JSON-LD |
Cheap structured data, and real humans use it too |
| 10 | Truthful llms.txt — every advertised URL resolving |
See above. Free, and probably your biggest current defect |
Three checks that score zero and never tell you why
These cost me the most time, and I suspect they're where most sites quietly lose points.
1. "Public API docs" means linked from your homepage. I had /docs, /openapi.json and /auth.md. All live, all correct, all returning 200. The check still failed, because the crawler starts at your homepage and follows links — and my homepage linked to none of them. The fix was four <a> tags in the footer. Nothing about the docs changed.
2. "Developer portal" reads the status code before following the redirect. I pointed /developers at /docs with a 301, which is correct web practice and reads to the scanner as absent. It must answer 200 at the URL an agent guesses. I now serve the same view at three paths.
3. "Name in titles and headings." My layout appended "Eduardo Cruz" to every <title>, and I counted that as done. But the check wants the name in the heading structure too, and every <h1> on my new plain pages said something generic like "Docs." Titles are not headings.
The pattern in all three: the check name describes the outcome, not the mechanism. When something you're sure you implemented still fails, assume you've satisfied a different mechanism than the one being tested.
The measurement trap that will waste your day
This is the part I most wish someone had told me: the scanners cache aggressively, and you cannot force a refresh.
After deploying every fix above and verifying each against production with curl, I re-ran the scan. Same score, same scannedAt timestamp to the millisecond. Here is everything that did not work:
- The CLI, six times. It returns the stored report and, per its own README, "never forces a rescan when a completed report is already available."
- The streaming endpoint with
force,refresh,rescan,fresh,noCache,cache=false. All served cache. - The Rescan button, twice. It visibly re-ran the agent task; the score didn't move within several minutes.
- Re-submitting through the homepage form. Same record.
- Trying
www.as an alias. It normalizes to the same registrable-domain key.
The record stayed frozen for over nine hours, then refreshed on its own overnight — the work had been live and correct the entire time. If I'd trusted the number I'd have concluded the fixes failed and started undoing good work.
Verify each fix against production yourself and treat the score as a lagging indicator. A one-line loop is enough:
for u in /mcp /api/v1/profile /openapi.json /.well-known/ai-catalog.json /auth.md /docs; do
printf "%-34s %s\n" "$u" "$(curl -s -o /dev/null -w '%{http_code}' https://eduardocruz.com$u)"
done
A 405 on /mcp is correct, by the way — the endpoint is there, telling you GET is the wrong method.
What still doesn't pass, and why I'm leaving most of it
The scan now returns 100/100, but that number is a raw 102.1 clipped to the cap, and the report still lists one failure and five partials. Here they are, because an article claiming perfection against a public report that shows otherwise would be exactly the failure mode this whole exercise is meant to prevent.
| Item | Status | Why it's open |
|---|---|---|
| Developer resource discoverability | FAIL | An agent searched the web for my dev resources and found nothing. The pages shipped hours earlier and weren't indexed. External latency, not a site defect |
| Brand name discoverability | Partial | I rank #6 of 10 for my own name. It's a common name and the namespace is crowded. No markup fixes this |
| Agent onboarding friction | Partial | It wants a free tier, self-serve API keys, a sandbox. My API is public and read-only — nothing to key, nothing to sandbox. The evidence literally reads "zero-auth access" |
| Function calling compatibility | Partial | 5/5 operationIds but only 1/5 typed response schemas. Real, and I'm fixing it |
| REST versioning / deprecation | Partial | /api/v1 is detected; no Sunset/Deprecation policy. Also real, also cheap, also next |
| CLI tool | Partial | Wants a published npm/PyPI/Homebrew package. Deliberately skipped — shipping a package to satisfy a scorer means maintaining it forever |
Two are real work. Two are external. Two assume a business model I don't have.
That distinction is the actual skill. A rubric encodes assumptions about what a website is for — usually a SaaS product with an authenticated API and a paid tier. Where those assumptions match you, the checks are excellent guidance. Where they don't, implementing them means adding complexity to satisfy a measurement, which is how sites end up with OAuth flows protecting information they'd happily print on a business card.
The highest-value items here were worth doing with no scanner in the picture. The MCP server is genuinely useful. The dead-URL cleanup fixed a real broken promise. The single-source refactor prevents a class of bug. The plain pages are faster for everyone. That's the filter for any check: if the score didn't exist, would this still be worth an afternoon?
What I'd do first if this were your site
In order, stopping whenever the return stops justifying the time:
- Curl every URL your
llms.txtadvertises. Fix or delete the dead ones. Free, fast, and you are probably lying to agents right now without knowing it. - Ship one public, unauthenticated MCP server. Even three read-only tools. Largest single lever, not close.
- Publish an OpenAPI 3.1 spec with
operationIdand typed schemas on every operation — what makes you function-callable to everything that isn't MCP. - Make errors parseable —
problem+jsonunder/api/*, and a real404that says so. - Link your docs from your homepage, and make sure the URL an agent guesses returns
200, not a redirect.
Steps 1 and 2 are most of the value. Everything after is a slow-afternoon job.
Work with me
I did this to my own site in a day, and the scan report is public, so you can check every claim here rather than take my word for it. If you want the same for yours — the MCP server, the API surface, the OpenAPI spec, a truthful llms.txt, and an audit of what you're currently advertising that doesn't work — that's a normal engagement for me.
I'm a senior Laravel engineer and fractional CTO. $60/hr, no retainer, no contract, cancel any week. I work US-overlapping hours in English, you talk to me rather than an account manager, and for most sites this is days, not months.
Tell me what you're building at /hire-me, or see the exact packages at /pricing. If you'd rather see how I think about AI-native engineering first, start with Laravel + Claude integration or what fractional CTOs actually cost in 2026.
Scores cited are from the is-agentic report for eduardocruz.com as of 22 August 2026, 04:04 UTC. Third-party scores move as rubrics and indexes change; the report URL above is always current.
FAQ
What is llms.txt and do I need one?
llms.txt is a plain-text file at the root of your domain that tells large language models what your site is, what you offer, and where the machine-readable versions live. It's a convention, not a standard — no search engine requires it. You need one if you want an LLM summarizing your business to work from a source you control rather than from your marketing copy. The important part is that it stays true: mine documented a POST /api/hire endpoint that never shipped, which is worse than having no file at all, because an agent trusts it as a contract and fails when the endpoint doesn't answer.
Does llms.txt help SEO? Not directly. Google has said it doesn't use llms.txt for ranking, and it's not a substitute for a sitemap, structured data, or crawlable HTML. Where it helps is generative engine optimization — when a model builds an answer about you, a clear, current llms.txt gives it accurate material instead of leaving it to infer from page copy. Treat it as source control for how AI describes you, not as a ranking lever.
What is generative engine optimization (GEO)? Generative engine optimization is the practice of getting your content surfaced and cited inside AI-generated answers — ChatGPT, Claude, Google's AI Overviews, Perplexity — rather than in a list of blue links. In practice it rewards content that answers a specific question directly, states checkable facts, and is structured enough to be quoted out of context. It overlaps with SEO but optimizes for being the source of an answer rather than the destination of a click.
Is GEO the same as answer engine optimization (AEO)? They're used almost interchangeably, and most of the tactics are identical: answer real questions plainly, use structured data, keep facts current and verifiable. If people draw a line, AEO usually refers to being the answer to a direct question — including featured snippets and voice assistants — while GEO refers to being cited inside longer AI-generated responses. Both are distinct from agent-readiness, which is about whether an AI can actually transact with your site rather than merely describe it.
How do I build an MCP server for my website?
MCP (Model Context Protocol) servers expose tools and data to AI agents over a standard interface. On Laravel, the laravel/mcp package makes it a class with two arrays — one listing your tools, one listing your resources — plus a single route registering it over Streamable HTTP. Start read-only and unauthenticated with two or three tools covering what you'd tell any prospect on a first call. Two gotchas: tool names resolve to kebab-case, so a GetProfile class is exposed as get-profile, and resources need an explicit name attribute or you get an ugly derived one.
How do I make my website usable by AI agents? In order of return: publish a public MCP server that needs no credentials, expose a versioned REST API with an OpenAPI 3.1 spec including operationIds and typed schemas, return RFC 9457 problem+json errors instead of styled HTML error pages, serve plain HTML pages for about/contact/docs and link them from your homepage, and keep llms.txt truthful. Then verify every URL you advertise actually resolves — broken documented endpoints are the most common and most damaging defect, because an agent can't improvise around them.