One application definition, several ways for an agent to use it
Generating discovery files, OpenAPI and MCP tools from one declared surface keeps the formats agreeing. It can't make a flag true, write a recovery procedure or count as extra test coverage. A skills pilot and a live byte comparison show where the line falls.

On this page
An agent that wants the Quickstart from a documentation site on Ferrite has more routes to it than it could ever need. It can start from llms.txt, ask for the page with a Markdown Accept header or fetch its .md twin, or find the MCP server at /mcp, call search_docs and then call get_doc on the result. A browser asks for the same page and gets HTML. Those routes run through different code and speak different protocols, and each of them makes a claim about the same application, so anyone exposing an application to agents has to decide how to keep those claims from drifting apart, and how they would find out if they had.
On Ferrite the starting point is a source package I developed and directed with agents. An application declares its surface once, its routes, pages and operations, and the package derives the rest from that declaration: robots and sitemap files, llms.txt, Markdown twins, RFC 9457 errors, an OpenAPI catalog, an A2A agent card, an MCP server and a self-report of how ready the application is for agents. “Two ways a tool description goes stale” covers how that derivation stops a rename from leaving one format behind. The derivation leaves three questions open: what a declaration is entitled to say, what an application should write by hand instead, and what counts as evidence once many applications carry the same code.
What a declaration can say
Follow one operation, get_doc. Its declaration is a small record: a machine name, a method and a path, a one-line description written for an LLM reader, optional input and output schemas, and flags for read-only and idempotent. The name becomes both the MCP tool name and the OpenAPI operationId, it appears on the agent card as what A2A calls a skill, {param} segments in the path turn up in the OpenAPI document and the tool’s input schema, and the flags become the hints an MCP client sees. None of this takes judgment, because each format is restating the same fact, and validation flags a declaration no projection could express, such as two operations sharing a method and a path.
The flags are where a fact turns into a promise. The idempotent flag means the operation returns its first result when a request repeats the platform’s idempotency key. Every request carries such a key, but deduplicating on it is the handler’s job, and the flag defaults to true. The package will advertise whatever it is told in llms.txt, the catalog and MCP’s idempotentHint, and it has no way to check. A class-booking application on Ferrite got this wrong: its begin_booking operation issues a new recovery secret on every call, and its metadata had to be corrected to say it wasn’t idempotent. Generation guarantees that every format repeats the declaration, and it can’t guarantee that the declaration is true.
The same discipline has to cover what an application doesn’t have. /.well-known/change-password is a discovery address for a password-change page. An application that has such a page names it, and one that doesn’t leaves the default, so the endpoint answers 404 rather than advertising a page that can’t work. A generator is very good at producing a complete-looking set of files, and a readiness checklist rewards completeness, so it matters that the default is the one that says no.
A declaration can also simply be wrong about where the application is running. The agent card’s A2A URL, the MCP manifest’s remote and the sitemap all name an absolute origin, and an application deployed on a domain other than the one it declared would send agents to a host it doesn’t serve. So the package can re-project the surface for the host the gateway attests on each request, and it never takes that host from a header the caller supplied. The protocol moves as well. At the end of August the MCP face of this package still spoke the 2025-11-25 revision, while another package in the same repository targeted 2026-07-28. By November a single stateless /mcp endpoint served both, with the body of each request deciding which revision’s rules it is held to.
Where generation stops
The generated skill on Ferrite, use-this-app, stays a general orientation to the API, and it deliberately doesn’t try to infer a procedure from those flags. HTTP idempotency says that repeating a request has the same effect as sending it once. It has nothing to say about what the booking application’s authored skill, book-class, spends most of its length on. Discover the class, then read its slots. Hold a place for five minutes if the customer is still choosing, and release the hold if they change their mind. Call begin_booking once before the first create_booking and keep its recovery secret. If the reply is lost, replay the identical arguments, because the same key prevents a second booking and the same secret recovers the management link. If a session is full, read availability again and ask the customer rather than moving them silently. A hold or an accepted request isn’t a confirmed booking, and a 403 on a token that a cancellation already spent doesn’t reverse the cancellation. That sequence and that recovery argument are knowledge about the business, and a generator inferring them from flags would be publishing a guess under the application’s name.
So an application hands the package its authored skills through one method, PageSource::skills(). The default returns nothing and leaves the MCP server with tools alone, so an application with no procedures to describe sees no change. A nonempty catalog adds resources and the skills extension to the MCP server, under both supported revisions, and publishes the same text through an HTTP index at /.well-known/agent-skills/index.json. Each manifest carries the original frontmatter along with the exact byte size and SHA-256 digest of every file. In the booking application the catalog is compiled from the authored skill at release, so the running application needs no filesystem and no Node.js.
That keeps the useful half of the single-source idea after generation has stopped. The text isn’t derived from anything, but it’s written once and carried unchanged through both channels, and a client on either can check that it has the bytes the application published. The digest only goes that far. If the hold operation changes and nobody revises the skill, the digests will certify an out-of-date procedure perfectly. And because these files are static text that can be served under a public cache scope, they have to be guidance that suits any caller, which rules out anything specific to one customer.
Whether the skill earned its place
It would be satisfying to report that agents did much better with these skills, and the first comparison says no such thing. A pilot in October 2026 gave fresh Codex CLI sessions on the default model the same tools, with and without a verified skill loader, across five workflows in this booking application and a writing assistant, including recovery of a lost booking receipt. Both conditions completed all five. The skills condition made more workflow calls, 51 against 33, on one valid pair per workflow with the tools always run first, so I wouldn’t read a cost or a speed difference into it either way. The conclusion recorded at the time was to keep the feature optional and claim no improvement, because the tool definitions had already told the model enough.
A harder follow-up found one real difference. After a dropped reply to a reschedule, an agent had to verify the moved booking and hand back a working link. Agents with tools alone invented a plausible /manage/<id>?token=… route, and so did agents using the first version of the skill, but the application actually reads the booking id and capability from the fragment of /book/manage. Once the skill documented that contract, the link opened the moved reservation. In that cohort tools alone passed three of four workflows and the revised skill passed all four, which establishes those outcomes and nothing as general as a completion rate. The record of that run also points out that returning the complete management path in the operation’s receipt could close the same gap. That is a useful way to think about what a skill is for. It holds what the declared interface doesn’t carry, and sometimes what it reveals is that the interface should carry more.
Many copies, one test
A package on Ferrite isn’t a deployed service. Its source is composed into each application that mounts it and runs under that application’s authority, and that makes coverage easy to overcount. The skills change was composed into 22 consumers and a check verified 1,248 composed files, but those copies are distribution, and none of them is extra testing. The booking application’s own run shows the proportion: 505 tests passed, of which 414 belong to shared source packages and 91 to the application. So the package’s canonical tests run once, against its own source, through a single fixture harness, a copy installed with ferrite add may omit its test modules altogether, and each application owns the integration and user-flow tests that only it can run.
Those cover what the package can’t see from where it sits: whether the declared surface matches the application’s routes, whether the well-known handler comes before catch-all routing and every header it returns is emitted, and whether a real password change works through the platform’s gateway. The booking application’s test reads the same authored bytes through HTTP and both MCP revisions, checks the manifest and confirms that customer discovery grants no staff operations. A copy synchronized in the repository doesn’t tell you that a deployed application has picked it up, either.
The self-report at /.well-known/agent-readiness calls for the same caution. Its findings carry the exact fix for each gap, which makes it very useful to whoever is integrating the package, and it remains an application’s account of itself. An application can earn a clean report without any agent having used it.
Reading it back
The evidence I give more weight to is a call through the front door, compared with something obtained another way. On 31 October 2026, as part of a wider expansion of the platform’s release verification, a read-only journey ran against a live documentation application. It started at discovery, called search_docs over MCP and then get_doc, and received the Quickstart, 6,157 bytes of Markdown. Those bytes were then compared with an independent HTTP response for the same document’s Markdown, and they were equal.
That oracle is deliberately simple. It has no view on whether the Quickstart is any good and doesn’t need a model to judge it. It tests the one property the design claims, that two routes describe one application, and a single stray byte would fail it. Inside the application the design makes that property plausible, since the HTML, the search text and the Markdown twin all come from one Markdown string through one renderer, and a local gate checks the twin against the page payload. The live journey asks the same question after the protocol code, the deployment and the gateway have all had their chance to interfere. The recipe’s first fixture failed for a duller reason: it pointed at a sample application that doesn’t mount the package at all. The maintained recipe now selects a consumer from the package’s manifest and uses that consumer’s recorded serving URL, so the check can’t quietly wander off to an application the package has nothing to do with.
Agreement between routes is all that comparison shows. On the same day a different check caught the platform’s production documentation sending its setup and MCP links to the wrong installation, on pages that were otherwise being served perfectly well, and two routes can agree perfectly about a wrong address.
The journey was also read-only, over one document, so it had no write side effects to undo. An agent holding a place in a class through one of these routes is relying on an authored skill’s recovery argument and on the application’s own journey tests, and byte equality has nothing to say about either of them.
Chris Chabot, November 2026