Zero-Click (Part 2): The Front Door Is an Agentic Chat MCP — Not a Data Feed
We attacked our own agentic MCP storefront: the findings, the fixes, the production outage our own hardening caused, and why an MCP endpoint with an agentic brain on the serving side is a different class of target.
Classification: Field Notes · Published: 2026-09-11 · Revision 2 · Status: verified against the live production endpoint · Canon: axoworks.com/articles/front-door-is-an-api
The short answer: when a marketing surface becomes an API, it inherits every discipline an API needs — server-authoritative counters, capability-level validation, metered spend, and deliberate disclosure — and when an AI sits on the serving side of that API, it adds four attack surfaces a data endpoint never has: injection, unprompted disclosure, autonomous write paths, and per-answer cost. AxoWorks attacked its own MCP endpoint at https://axoworks.com/api/mcp, found a live provider credential in the public bundle, a rate limit carried in a client-controlled cookie, an unauthenticated write path validated at one door and open at the other, and “redactions” implemented in CSS. This is the record: what broke, what we fixed, what our own fixes broke, and what is still open.
TL;DR
- An AI→AI endpoint is a different species. Most agent endpoints return chunks and leave the reasoning to the caller. Ours answers: an AI on the serving side, grounded in a curated corpus. That is the product — and it is also why this class of endpoint is harder to secure than a normal API. Intelligence on your side of the wire is a surface an index does not have.
- A counter your client carries is not a counter you have. Ours was a cookie. Drop it and the quota reset. The fix moved the count to the server; the proof is twelve cookie-less requests served and a thirteenth refused at the door.
- Rate limits are throughput, not spend control. “30 requests per minute” bounds speed, not cost. Our MCP server had no daily ceiling: a theoretical 43,200 paid inferences per day, per address, with no authentication.
- Fix the capability, not the caller. We hardened the MCP handler and the chat path kept firing the same write tool. Two doors, one room — and only one door had a lock.
- Redaction must delete, not decorate. Our “redacted” audits hid text with CSS that revealed on hover. Hiding is not removing.
- Publishing is a disclosure decision, not a marketing one. A crawlable evidence corpus built for SEO is also a tidy index of records you would rather nobody read.
- Every fix needs a control. A probe without a control does not prove the hole is closed. Two of our own conclusions did not survive that standard, and both corrections are in the record below.
01 / What changes when your marketing surface becomes an API with a wallet
When the conversion is a structured request your server receives, your tools stop being pages and become write paths — and your risk register has to be rewritten around that.
Part 1 named the funnel: the impression is a tool call, the landing page is an endpoint, the conversion is a structured request your server receives directly. Sit with what that does to a risk register:
- Your tools are no longer pages. They are write paths.
- Your inference is no longer a feature. It is a cost line someone else can pull.
- Your transcripts are no longer ephemeral chat. They are stored data with an owner question.
- Your content corpus — the crawlable one you built for SEO — is now an index of everything you ever published, read by machines that never see your intent, only your URLs.
A zero-click storefront is still a storefront. But the customers bring their own runtime, and some of them are not customers.
There is a second-order effect, and it is the real subject of this article. An agentic chat endpoint has an attack surface that scales with the intelligence you put on it. A chunked-data endpoint can be abused the ways any read API can: enumeration, scraping, load. An AI→AI endpoint has all of that, plus four surfaces that exist only because something on our side thinks:
- Injection. The caller's text reaches a model that holds tools. A data endpoint cannot be argued with. A model can.
- Disclosure. A serving AI says the wrong thing unprompted. Ours volunteered its pricing model, its internal stack, and a client-project figure that no one asked for.
- Autonomy. The write path fires on a sentence, not a click — no confirmation, no human in the loop until the notification has already landed.
- Cost. Every answer is paid inference, and a well-worded question is indistinguishable from a well-funded attack.
A filing cabinet cannot be talked into anything. An employee can. That is the trade you make when you stop serving data and start serving answers: a better salesperson and a harder perimeter, shipped in the same feature.
For an AEC firm the trade is more consequential than it sounds. The question an agent asks us is usually a site question, and a wrong answer is a professional-liability question, not a marketing one. That is why we run the deterministic half of the practice on rails — a Revit MCP server that returns measured geometry rather than generated claims — and keep the conversational half inside a scope it can defend. Numbers cannot be persuaded. A model can. In an industry where the deliverable is stamped, that distinction is the whole design.
02 / How we attacked it, and what “verified” means here
We ran a live adversarial audit against production — not a code review — across four workstreams and three hardening phases, and we required a control for every claim.
Every finding below came from a live request against axoworks.com, its logs subdomain, or the MCP endpoint, with the raw request and response retained. Nothing in this article is a hypothesis about our own code. The method matters as much as the results, so here it is plainly.
- Four workstreams. HTTP and MCP protocol probing; static analysis of the shipped JavaScript bundle; a conversational red-team battery against The Concierge through the real UI; and a full inventory of the crawlable evidence corpus.
- 28 adversarial conversational tests executed of 40 designed, spanning system-prompt extraction, persona and roleplay jailbreaks, injected and indirect instructions, tool abuse, data-leak probing, robustness, and honesty. Twelve secondary variants were skipped on budget after the first serious finding landed.
- Three hardening phases, each followed by a live re-test of the exact bypass that had succeeded. A fix without a re-test is a claim, not a fix.
- A control for every conclusion. Any probe that returned a success-shaped result was re-run against something the system could not possibly accept. Two of our own conclusions failed this standard, and both are corrected below rather than quietly dropped.
The headline conversational result is the one number from this engagement we would put on a slide: zero system-prompt leaks and zero successful injections across 28 adversarial tests. Eighteen probes drew an in-persona deflection, six produced partial information disclosure, three were flat refusals, and one XSS probe was sanitized into inert markup. A two-turn persistence attack that tried to plant a trigger in one message and fire it in the next was refused in the first turn and deflected in the second.
That number is genuinely good, and it is also not the interesting one. The interesting finding was that the conversational core was the hardened part. Everything around it — the plumbing, the counters, the disclosure posture, the crawlable corpus — was where the damage was.
03 / Finding: the storefront was shipping its own keys
A live model-provider API key was inlined into the public JavaScript bundle that every visitor downloads — and our own published audit claimed the exposure had been patched.
The first pass looked at the parts any competent attacker looks at first: the things we ship to every visitor.
The bundle carried a provider credential. Not in a config file, not behind an endpoint — inlined into the JavaScript every browser downloads, an artifact of how the build tool inlines environment variables. It was confirmed three independent ways in a single afternoon: a grep of the build output, a static analysis of the shipped bundle, and an endpoint probe. One key, one length, one grep. It was the most expensive finding of the entire exercise and it cost nothing to find.
Session identifiers were acting as bearer tokens. Chat transcripts were retrievable by anyone holding a session ID, and a session ID had been published on our own logs subdomain months earlier. Two individually reasonable decisions, chained into a disclosure: a static leak on one domain, and an unauthenticated read endpoint on another. Anyone holding that identifier could pull a full transcript anonymously.
The write tool was open by design, without validation. book_consultation is deliberately unauthenticated — it is the zero-click conversion path, a tool for agents that never click. But “open” without validation means anyone can pipe arbitrary markup into the humans' inbox and into whatever renders it. Our probe sent a name field containing a script tag and a scope field containing an instruction aimed at whatever model might later read it, and the endpoint echoed both back unescaped.
The smaller furniture mattered too. Error paths that reflected arbitrary origins; debug response headers narrating our internal stack and provider tier; a catch-all route that answered every typo with HTTP 200 and the homepage's HTML, which blinded status-code reconnaissance for everyone including us; and security headers present on the apex domain but absent on the subdomain.
Not everything was broken, and it is worth saying which parts held under the same pressure. CSRF protection failed closed against every forged token scheme we could construct, including a freshly computed one from the client's own fallback algorithm. The turn cap did engage for cooperating clients. Transcript reads enforced an origin allow-list on the browser path. Admin signup was invitation-gated. Across 28 adversarial conversational tests the core never leaked its prompt or adopted an injected instruction. Clean JSON-RPC error shapes, no stack traces, clean TLS.
The locks were fine. The doors we had forgotten we had were the problem.
None of it was exotic. That is the point. Zero-click marketing changes where your conversion happens, not the fact that you still have to do the boring work.
04 / The counter that wasn't a counter
Our chat turn cap was enforced against a client-supplied cookie, so dropping the cookie reset the quota. A quota enforced against client-supplied state is a suggestion.
This is the most transferable mistake we made, and it deserves its own section.
We had a turn cap on The Concierge: twelve questions, then a thirty-minute cooldown. It looked like a control. It behaved like one — in a browser. Our UI reset the transcript, the limit persisted, and we congratulated ourselves.
Then we asked a sharper question: where does that number live?
It lived in a cookie. The server read the count from the client, incremented it, and wrote it back. Which means:
- Drop the cookie and the count starts at one. Unlimited inference.
- Forge the cookie and you get the same result with extra steps.
- Rotate the session identifier and you get the same result, cleaner.
- Spoof a forwarding header and nothing changes — because the count was never keyed to identity in the first place.
The mechanism test that settled it was four requests long. Twelve requests carrying the server's cookies forward, as a browser does, returned 200 with the counter climbing one through twelve. The thirteenth returned HTTP 429. Then the same payload with the cookies dropped returned 200 with the counter back at one — same address, no cooldown. There was no brake underneath the client's own bookkeeping.
A quota enforced against client-supplied state is a courtesy for well-behaved clients and a rounding error for everyone else.
The fix was not clever. We moved the count server-side, keyed it to the trusted client identity the edge platform gives us, and demoted the cookie to a UI hint the server ignores. The proof is equally unglamorous, and it is a proof because every old bypass was re-run against it: twelve cookie-less requests are served, the thirteenth is refused with a 429 and a Retry-After of 1,800 seconds; a forged counter cookie after the cap returns 429; a fresh session identifier plus a fresh CSRF token plus a spoofed forwarding header returns 429. Every pass-seven bypass is dead.
Design principle: if the client can reset it, it is not a limit. It is a label.
05 / Finding: same tool, two doors
We hardened the MCP handler and the chat path kept firing the same write tool. We had fixed the handler, not the capability.
This is the finding that taught us the most, because the fix was applied correctly and the vulnerability survived it.
An agent-driven storefront has more than one way in. Our MCP server exposes book_consultation. Our chat widget can also book a consultation, because a person typing “can you call me?” should get a human. Same capability — two execution paths through two different code files.
We hardened the MCP handler: domain blocklists, disposable-address rejection, and tag stripping on free-text scope. We verified it with a probe that sent a name of PROBE <script>alert(1)</script> plus an instruction-shaped scope containing an admin-escalation token and a template marker. It came back as PROBE alert(1) — tags stripped, injection markers removed, inert residual text only. We shipped it.
The chat path still accepted a reserved-TLD address and cheerfully reported “Lead Captured — fast-tracked to the principals.”
We had fixed the handler, not the capability. And the path we left open was the one real visitors use — the one we never re-tested, because it was the path we thought we had not touched.
Two lessons, both cheap to state and expensive to learn:
- Validation belongs at the capability, not the caller. If two entry points reach the same write, one shared gate has to own the rules — otherwise every path carries its own copy, and copies drift.
- Client-side sanitisation is not validation. The chat widget did strip script tags and event handlers. That is a rendering policy, not an admission policy. It protects nothing that arrives through any other door — including a machine calling your API directly, which is the entire premise of zero-click.
There is a related design lesson we bought the same week. A honeypot field does not belong in a public tool schema. Honeypots work by being invisible; an agent-facing schema is visible by definition. Put a hidden field in an MCP tool and honest agents will fill it in helpfully, silently destroying your highest-intent leads while every attacker reads the schema and skips it. Security controls that depend on the attacker not reading your published contract are not controls in this era. The public schema is the contract.
06 / Meter the moat
A per-minute rate limit bounds one attacker's speed; it does not bound your bill. Ours allowed a theoretical 43,200 paid inferences per day per address, with no authentication.
Part 1 made a claim we have to correct in public, because it was published and it was wrong:
“Each grounded answer costs AxoWorks cents of inference, capped at 30 requests per minute per IP.”
Thirty per minute is a throughput limit. It says nothing about how many minutes there are in a day — and there are 1,440. Our MCP endpoint, unauthenticated by design, accepted a theoretical 43,200 paid inferences per day per address, and an attacker with a handful of addresses multiplies that. We were, in the most literal sense, giving away the product.
Worse, the two limits we did have did not cover the same surface. The chat path had its turn cap; the MCP path had only the per-minute window. When we probed it with a burst of consecutive ask_concierge calls, every one returned a live model answer with the rate-limit counter ticking down and nothing else in the way. The cap we were proud of was not defending the expensive door.
The fix is a daily inference budget: fifty grounded answers per address per day, down from 43,200 — a reduction of roughly three orders of magnitude. It is enforced server-side and advertised to machines as headers, because Part 1's own principle was that backpressure should be legible: a well-behaved agent should be able to read how much room it has before it hits a wall. The budget is scoped to ask_concierge only. Search and read tools carry no daily headers, because they do not invoice us. And it was verified the only way that counts: calling until the counter decremented, and confirming that search_knowledge_base did not touch it.
One structural point we would put in front of any firm building this, because it outlives every individual cap: per-address limits bound one attacker, not an attack. Fifty a day per address is the politeness layer. A hundred addresses is still thousands of paid calls. The durable control is a global spend breaker — a single counter across all traffic, with a threshold that trips an alert before it trips your invoice. Ours is on the list, not in the build, and we would rather say so than imply otherwise.
Free inference is a gift with a bill attached. Meter what costs you money, or someone will meter it for you.
07 / Redaction must delete, not decorate
Our “redacted” audit documents hid text with CSS that revealed on hover. The hidden text was in the HTML the whole time. CSS is a presentation layer, not a confidentiality control.
Part 1 argued that the moat is not the protocol, it is the curation. This phase was the discovery that curation includes what you publish about yourself.
Our logs subdomain is the crawlable evidence corpus — the bridge that makes a single-page app legible to search engines at all. It is a genuinely good idea and we still run it. It was also, in three specific ways, a dossier.
“Redacted” audits were not redacted. The hidden text was in the HTML, styled to match the background, with a CSS rule that revealed it on hover. Anyone reading the source — or moving a mouse — got the content. One of those hidden spans contained a working comparison routine from our own CSRF validation. Another audit record still carried production system-prompt fragments, and a third printed the auditor's entire input prompt.
A raw database export sat on a static host. A CSV of production rows, linked from a page, carrying internal function names and a column of session identifiers, 164 rows and 46 of those identifiers. It was PII-free on the day we looked, which is exactly how it would have looked the day before it was not: the export path sat one pipeline run away from shipping visitor contact data with a single click. We reviewed it and left it public, while enforcing session ownership so that the identifiers inside it are inert — a published identifier now returns a 403, not a transcript. The finding is not the file. The finding is that the export path existed at all.
The sitemap advertised the sensitive records. Fifty-three URLs, everything at priority 0.8, including internal audit documents, a live chat transcript, and fourteen records we had built out of scraped professional profiles — with third-party names inside them, alongside our own account's private analytics, which is exactly the kind of pasted-without-review artifact that proves a scrape came from an authenticated session. We had de-indexed nothing, published everything, and called the subdomain a marketing asset.
The fix had three parts, and only the third is interesting:
- Delete the content rather than hide it, and remove the reveal rule. The audit records that were genuinely embarrassing went back through review, and the third-party personal data came out of the records we republish.
- Trim the index. The sitemap went from 53 URLs to the 32 that are actually marketing.
- De-index deliberately, at the edge. The remaining sensitive records stay live and linked — no link rot, no 404s — but they carry a
noindexheader applied per path. Twenty-three paths are now de-indexed and stay reachable.
One counter-intuitive detail worth stealing: do not block those paths in robots.txt. A crawler that cannot fetch a page cannot see its noindex header, which delays de-indexing instead of causing it. Blocking the crawl and asking to be forgotten are opposite instructions. And a robots.txt disallow is a convention, not access control — the file remains fetchable by every browser on earth, which is exactly how the CSV stayed discoverable long after we had stopped linking it.
08 / Your audit said what?
Our own published audit said the credential exposure had been patched. The live bundle still shipped it. Post-fix claims rot, and a published posture is a claim about production, not about a commit.
There is a special category of trust failure that only shows up in firms that publish their own audits — which, as the site you are reading, we do.
Our published audit marked the exposed provider key [PATCHED IN PRODUCTION]. The live bundle still shipped it. Both statements came from our own house, months apart, and the second one was the truthful one.
The lesson is not “do not publish audits.” It is that a published posture is a claim about production, not about a commit. If you advertise your security posture — and in zero-click marketing you should, because for machines trust is the product — you have to re-verify against the live endpoint every time you rebuild. Otherwise you have published misinformation with your logo on it, and you have done it in the one format a machine will quote verbatim for years.
We have made this mistake in public before. The only defence we have found is the one we now use everywhere: publish the correction next to the claim. That is why this article has a corrections section, and why Part 1 carries one too.
09 / Guard precision is a sales metric (and your llms.txt is an SLA)
A prompt-injection defence that hangs up on a paying client mid-sentence is not a security win. In AEC, “ignore the previous revision” is normal collaboration, not an attack.
This section is the reason we think the hardening discipline transfers to any firm with a domain vocabulary, and it is the least discussed finding of the engagement.
Guard precision is a sales metric. Our first-generation injection defence was tuned against synthetic adversarial payloads, and it showed. The failure mode was not a breach — it was a false positive on legitimate client language. In AEC, “please ignore what I said before, the setbacks changed on revision 3” is a normal sentence from a normal client. A guard that reads it as a jailbreak attempt is hanging up on revenue. Every false positive is a refund you never see leave, and you never learn it happened, because the client just stopped. The fix was not a weaker guard: it was a dialect-tolerant one that whitelists legitimate domain vocabulary — corrections, revision changes, superseded scopes, “disregard the draft” — while keeping the structural checks intact.
Your llms.txt is an SLA. If you advertise to every visiting agent that your concierge answers zoning questions, your model's domain classifier cannot one day decide it does not. Ours briefly did: a Seattle floor-area-ratio lookup tripped the out-of-domain classifier and got a brush-off — “I do earthly buildings, not zoning code lookup bots. Move along.” That sentence contradicted the capability our own llms.txt had just promised. A machine does not experience that as a personality quirk; it experiences it as a broken tool, and a broken tool gets dropped from the list. Your marketing promises and your model's system boundaries have to be the same document.
And curate your corpus by record type, not by text. The disclosure problem has a structural fix: never let internal operational records — response playbooks, internal policy notes, protocol documents, system records — into the same semantic index that answers public queries, and never let them into the prefetch that runs before a query arrives. Our own red-team found the agent volunteering internal pricing structure, internal tooling, and a client-project figure nobody asked for. A retrieval layer that cannot tell the difference between your marketing content and your operations manual will eventually tell a customer the difference. Type-level isolation, not pattern matching on chunk content, is the fix that holds.
10 / The part where our hardening broke the website
Two correct security fixes, shipped together, took the Concierge offline for real visitors. Security changes are product changes, and they ship to the same users on the same day.
This is the section most security write-ups skip, so here it is plainly.
We fixed the cost-abuse problem by capping request size. We fixed the session problem by binding transcripts to ownership. Both changes were correct. Together, they broke The Concierge for real visitors — and it took someone testing in a different browser to surface it.
What happened: our client was sending the entire content library plus the full conversation history on every request — twenty-five posts, each with up to five thousand characters of body text, on top of the whole transcript — and the new 32-kilobyte ceiling rejected the storefront's own traffic. Meanwhile an older stored session identifier, minted before the ownership model existed, got a flat refusal that the client had no code path to recover from. The result looked like a dead chatbot, which is a worse outcome than any single finding it was fixing.
We caught it because the console said two numbers — 413 and 403 — and because we checked a second browser instead of assuming a browser problem.
The fixes went to three places. Trim what the client sends: last fifteen turns, no post bodies in the context payload. Treat an ownership refusal as “no history here” and mint a fresh session instead of dying. Enforce the limits on the untrusted field rather than the whole envelope, measuring raw decoded bytes server-side instead of trusting the Content-Length header a client chooses to send.
One honest standing item: the ceiling still sits about ten percent above our worst measured realistic request, and the numbers are close enough to be uncomfortable — a worst-case measured payload of 29,133 bytes against a 32,768-byte limit is roughly eleven percent of headroom, while the Concierge routinely writes answers of 2,500 to 3,000 characters. We have already broken this once by a margin that looked acceptable on paper. It is a standing watch item, not a closed ticket.
Test your security fix with the real payload your own product sends — not a hand-written {"message": "ping"}. Otherwise your tests pass while your storefront is dark.
11 / How do you know your fix worked? Controls.
A probe without a control is a guess. Two of our own conclusions failed that standard, and both corrections belong in the record next to the findings they amend.
We were wrong once, loudly. Our first write-permission probe returned a success code on every database table, including the ones holding leads. It looked like a catastrophe. It was not: with row-level security in place, an update that matches no row also returns success — the same status code carrying the opposite meaning. Re-run properly, against a row we could actually see and with a value the database could never accept, the same tables returned “nothing was touched,” which is what a locked door actually looks like. The first test had no control. A probe without a control is a guess with a chart.
We overstated a finding once, publicly. Our automated counter reported that every “redacted” span on one page held real content. On inspection, thirteen of the fifteen were exactly the placeholders they claimed to be. Two were real. The severity held; the scope was wrong. Automated counts mis-classify text that looks like the thing you are hunting — and a scanner that flags every match is a scanner you will stop reading.
Two lessons we now apply without exception. A single-request verdict is provisional until a control validates it. And a finding you cannot re-run is a finding you will lose in the next refactor, which is why three of our checks are now a single command: keys absent from the build, caps enforced server-side, ownership refused.
If your hardening report has no corrections in it, be suspicious of it — including ours, which has two.
12 / What the mind in the loop actually costs you
Four of the findings in this engagement exist only because an AI answers on the serving side. Remove the mind and you remove the vulnerabilities — along with the answers.
Mapping the four surfaces from section 01 to what we actually found is the most useful thing we can hand another firm, because it is the part that does not transfer from ordinary API security advice.
Injection was the surface we were most afraid of, and the one that held. Zero successful injections in 28 tests, including a two-turn persistence attempt and a payload padded with two kilobytes of benign text. The defences that worked were structural — intent detection at the edge, a retrieval layer the model cannot be talked out of, and instructions the model has to satisfy rather than merely read. What we learned is that injection resistance is a property of architecture, not of a better prompt.
Disclosure was the surface that leaked, and it leaked voluntarily. No attacker extracted anything on demand; the agent offered. Pricing structure, internal tooling, a client-project figure. The fix is not a stricter rule in the prompt — it is scope: decide what the endpoint is allowed to know before you decide what it is allowed to say. Isolation by record type is what makes that decision enforceable.
Autonomy is the one we have only partly addressed, and it is the one with the worst asymmetry. An agent-submitted lead is genuinely valuable data — a site and a question, not an email address. It also arrives as a sentence, with no confirmation step, validated at the entry point last rather than first. The addresses are now gated on both paths. The intent is deliberately still open, because a storefront that requires a form is a storefront nobody bothers with. An open read path is a product decision. An open write path is a liability. Ours is validated, throttled, and monitored — and a human confirmation step is on the list, not in the build.
Cost is the surface most firms will get wrong, because it does not appear in any security checklist. Your inference bill is an attack surface with a currency, and the only controls that matter are the ones that bound the total. Per-address caps are the politeness layer. A global spend breaker is the control.
13 / The hardened MCP production checklist
If you are shipping an agent-facing endpoint — especially one with a write tool — this is the compressed version of what this engagement taught us, in the order we would do it.
- Decide which kind of endpoint you built, before you build it. A data endpoint can be secured like any read API: access control, rate limits, schema hygiene. An agentic chat endpoint is a member of staff. It has judgment, a mouth, and a wallet, and it needs governance — disclosure limits, autonomy gates, per-tool budgets — not just access control. The moment your server starts reasoning, the question stops being “can they get the data” and becomes “can they steer the behavior.”
- Server-authoritative counters. Never enforce a quota from client state. Key limits to the identity the edge platform gives you, and treat the client's copy as a hint.
- Per-tool budgets plus a global spend breaker. Meter what invoices you. Alert before the threshold, not after. Per-address limits bound one attacker, not an attack.
- One gate per capability. If two paths reach the same write, they share the validation — at the capability layer, never in the client. Then re-test the path you thought you had not touched.
- Validate every field that leaves your system. A lead that reaches an email, a webhook, or a CRM is input to somebody else's renderer. Strip tags and injection markers at ingest, not at display.
- Separate read from write. Keep the read path open if that is your marketing strategy. Gate anything that changes state or reaches a human: validate, confirm, throttle, and make the human on the other end identifiable.
- Never enforce a limit by trusting a client-supplied header or body size. Measure raw decoded bytes server-side.
Content-Lengthis a claim, not a fact. - Keep honeypots out of public schemas. If a control depends on the attacker not reading your published contract, it is not a control. Put honeypots in HTML forms, not in an agent-facing tool definition.
- Cure the corpus by type, not by pattern. Internal playbooks, policy notes, and system records must not share an index or a prefetch path with public answers.
- Tune your guards against real client language. Test for false positives with the vocabulary your own customers actually use, and remember that in AEC “ignore the previous revision” is a revision, not a jailbreak.
- Treat your published capability list as an SLA. Your
llms.txtand your tool descriptions are promises a machine will hold you to. If the model refuses a question your marketing advertises, you have shipped a broken tool. - Treat your corpus as a disclosure surface. Index deliberately. Delete content you do not want read — CSS is not a redaction tool. De-index with headers, not
robots.txt. - Re-verify what you publish about yourself. Every posture claim gets checked against production after every deploy.
- Ship a regression script with the fix, and keep the controls. For every finding, write down the probe and the control that proves it closed. A fix you cannot re-test is a fix you will lose in the next refactor.
- Invite the adversary back. Twice. The defect that did the most damage to our lead funnel was introduced by our own first security patch. If we had not re-tested after fixing, our conversion path would have stayed broken for days.
14 / FAQ
Q: Is an unauthenticated MCP server reckless?
A: An unauthenticated read server is a product decision — it is the zero-click front door, and it is the reason an agent can call us without a relationship. An unauthenticated write path is a different animal. Keep the read path open, meter the inference, and gate anything that changes state or reaches a human. We have gated the addresses; the intent is still deliberately open, because a booking that requires a form is a booking nobody makes. The confirmation step is on the list, not in the build, and we would rather say that than imply otherwise.
Q: What is the difference between a data endpoint and an agentic chat MCP?
A: A data endpoint is a retrieval surface: the agent calls a tool, the server returns chunks, and every act of reasoning happens on the caller's side. It is an index with a handshake — the right design for most sites, and the reason platform-generated endpoints appearing everywhere are cheap to run and cheap to secure. An agentic chat MCP puts intelligence on the serving side too: the caller's agent talks to a second AI — ours, grounded in a curated corpus, bound to live pricing logic and scope thresholds, answering in a voice tuned over months of adversarial pressure. Same protocol; different species. The practical difference is the attack surface: injection, unprompted disclosure, autonomous write paths, and per-answer cost are all properties of having a mind in the loop. Remove the mind and you remove the vulnerabilities — along with the answers.
Q: Do you still publish your audit documents?
A: Yes, selectively — now deliberately de-indexed where appropriate and genuinely redacted. An audit is reconnaissance if you hand it to an attacker. Ours are published as a trust signal for the audience that reads them, not as a public dossier. That distinction is the whole of section 07.
Q: Does hardening hurt zero-click marketing?
A: The opposite. For machines, uptime, honesty, and legibility are the funnel. A storefront that returns a clean error with a retry hint keeps the relationship; one that dies mid-conversation teaches an agent not to call again. And a firm that publishes its own corrections is more quotable, not less — a machine that can verify you have admitted a mistake has something no marketing copy can provide.
Q: How much did this cost?
A: Time, not money — three phases, days not weeks, most of it spent proving things we had already fixed. The expensive item was the credential in the bundle, and rotating it cost less than the coffee that preceded the grep. The most expensive thing in the whole engagement was the outage our own fixes caused, and the only reason it was hours rather than days is that a human tested in a second browser.
Q: What was the single best decision?
A: Deciding that a self-assessment is not evidence. Every claim on this page came from a live request against production, with a control next to it — and two of our own conclusions did not survive that standard. The second-best decision was inviting the adversary back after the first round of fixes, because the worst defect in the funnel was created by the first round of fixes.
Q: Does this apply outside AEC?
A: The business case in Part 1 is AEC-specific; this part mostly is not. Client-trusted counters, capability-layer gates, unmetered write paths, honeypot fields in public schemas, and CSS “redactions” are industry-agnostic. If your marketing surface became an API — or your product is an agent — the findings transfer. If all you have is a brochure, you have a different set of problems.
Q: So is everything fixed?
A: No. Hardening has a tail, and a firm reporting a clean sweep has stopped testing. What is handled gets a fix, a control, and a line here. What remains is lower-severity, logged, and genuinely open — see the next section. This article has already needed two corrections, and we would rather publish a third than pretend the list is empty.
What is still open
We are publishing the shape of what remains, not the mechanisms — because an open item written in enough detail is a recipe, and an audit is reconnaissance if you hand it to an attacker.
Two weeks of hardening closed the three big classes: exposed secrets, session and ownership trust, and unbounded paid inference. All three were verified closed by live re-test, not by reading our own code. What is left falls into four buckets:
- One known application-layer issue (medium), in the chat transcript renderer, scheduled not closed. It is self-scoped today — transcripts are ownership-gated — so it is not remotely exploitable as things stand, but it must be closed before transcripts are ever rendered anywhere else, such as an internal dashboard or a notification email.
- One open abuse path in outbound notification mail (medium), which we are addressing with a per-recipient throttle and a two-step opt-in rather than a blocklist, because blocklists are the thing that breaks legitimate clients.
- Two items that can only be settled by reading our own server code, not by probing (one medium, one potentially high if the answer is bad). Both involve anonymous calls that may or may not trigger expensive server-side work. They are cheap to resolve and we are resolving them in code rather than publishing the entry point.
- A short hygiene tail — a validation-before-authorization order in one admin function, a credentials header that should be dropped on one error path, a rate-limit message that still says “per session” when the counter is now per address, and the payload headroom watch item from section 10.
And one structural item that matters more than any single finding above: every abuse control we now have is per-address. Per-address caps bound one attacker, not a distributed one. The durable control is a global daily spend breaker with an alert below the threshold. It is on the list. We would rather write that sentence than let a clean checklist imply it is done.
15 / The bottom line
Part 1 made an AI-native storefront legible to machines. Part 2 made it survivable — same interface, same bet, different question underneath: if the front door is an API, who is holding the door?
Nothing here required expertise in the exotic. It required taking the storefront as seriously as we took the idea behind it: counters the server owns, gates that sit on capabilities, budgets that watch the invoice, content that gets deleted rather than hidden, and the habit of testing the fix the way a stranger would.
Zero-click marketing does not get to skip security because the visitor has no eyes. It gets more of it — because your visitor is now a machine that reads everything, clicks nothing, and remembers exactly what you published.
And in a practice like ours, something else is true. The deterministic half of our MCP story — the Revit server that measures window-to-wall ratio and septic clearance — does not need this kind of article, because numbers do not get persuaded. The half that talks does. The moment you let an AI answer on your behalf, you have hired someone — and hiring someone means governing them.
What comes next is not more endpoints. It is more interlocutors. The next turn of zero-click is agents talking to agents: a buyer's agent that does not fetch your corpus but negotiates with your Concierge, compares your computed scope against three competitors' computed scope, and holds you to what you said last quarter. When that arrives, the moat stops being your corpus and starts being your conduct.
We may have built one of the first. We cannot prove it, and we would rather be corrected than quoted. But if this is where zero-click is heading, the checklist above is a rough estimate of the bill.
The click was never the point. Neither is the courtesy of a well-behaved client.
Part 1 — Zero-Click Marketing: How Axoworks Built an MCP Connector for the Agents Who Never Click — is available in canonical form on our own domain.
This article is an account of AxoWorks' own hardening engagement. The graded technical report, the per-file change logs, and the re-runnable verification scripts remain internal. Every claim above was verified against the live production endpoint before publication. Where our own earlier statement was wrong, we have said so and corrected it — including one claim in Part 1 itself.
If you are building a storefront for agents — or you have shipped one and never attacked it — start where we started: move the counter server-side, then put a budget on the thing that bills you. The graded report stays internal. The checklist above does not. If something here is wrong, say so; corrections land on this page.
The rest of this line of work
Part 2 is the engineering half of a project we have been running for a year. These are the pieces it leans on:
- Why General-Purpose AI Fails at AEC Pre-Construction — the failure modes and the five-layer verification stack that makes AI output trustable enough to build on. This article is the security corollary to it.
- The Referee in the Pit — the Advocate, Prosecutor, Honey Badger, and Referee debate team we built to catch our own models lying. The same instinct as “a probe needs a control,” applied to content.
- revit-tools — the other MCP server: a zero-dependency Python MCP server for Autodesk Revit 2027 with deterministic geometry audits. The half of the practice that never needs a jailbreak defence.
- Three Ways an AI Lies to You — the adversarial benchmark behind our refusal to let a model answer a question it cannot source.
- Death of the Dashboard — where we published a blind spot in our own audit panel. The precedent for the corrections in this piece.
- From Portfolio to Agent — the architecture underneath both parts: Brain–Hands–Voice, Eager RAG, and the SEO paradox.
- Part 1: Why an AEC Studio Built an MCP Storefront That Answers Back — the case for building the door, and the build log.
Sources:
First-party (AxoWorks-published and internal; figures self-reported): the graded adversarial stress-test report on axoworks.com, its MCP endpoint, the logs subdomain, and The Concierge (September 2026) · the post-hardening remaining-issues register (2026-09-11) · the public redacted January 2026 system audit · From Portfolio to Agent · The $0.74 Website · The Axoworks Logs · revit-tools · Why General-Purpose AI Fails at AEC Pre-Construction · The Referee in the Pit · An earlier hardening pass (September 2026) from which sections 09 and 13 draw the dialect-tolerance, honeypot, and type-isolation lessons · Part 1 of this series.
Third-party research: Model Context Protocol (Anthropic, November 2024; OpenAI, March 2025; Google, April 2025) · OWASP guidance on rate limiting, input validation at trust boundaries, and secret management in build output.
Live verification: GET https://axoworks.com/api/mcp returned {"name":"axoworks-concierge","protocol":"mcp","protocolVersion":"2026-09-02","transport":"streamable-http","status":"active","tools":["ask_concierge","search_knowledge_base","book_consultation"]} — fetched 2026-09-11. https://axoworks.com/llms.txt fetched the same day.