Versioned Docs for Coding Agents: How We Built FrontMCP's Docs MCP Server
A coding agent writes FrontMCP code from what it remembers, which is whatever release it was trained on, if any. FrontMCP ships often, so that memory is usually out of date. We wanted an agent to be able to read the docs for the release the project in front of it actually uses.
The result is a docs MCP server at https://mcp.frontmcp.dev. It's built with FrontMCP, runs on a Cloudflare Worker, and is listed in the MCP Registry as dev.frontmcp.mcp/frontmcp. This post covers how it works and what we learned building it.
What it replaced
FrontMCP's docs used to live on a hosted docs platform, and its MCP server was the one every new FrontMCP Nx workspace wrote into .mcp.json. It worked, but it couldn't answer the questions an agent upgrading a project actually asks:
- It searched everything. Every answer mixed four products and every archived minor release.
- It didn't know the project's release. An agent on 1.9.4 got answers written for whichever release ranked first.
- It couldn't look up an API by name. An agent had to search for
createFetchHandlerthe same way it searched for "rate limits". - It couldn't say what changed between two releases. That is the question an upgrade starts with.
When the docs moved to frontmcp.dev, we wrote our own server rather than move the old one along with them.
One snapshot per release, from git
The site's docs are MDX in a git repository. Each time the site moves to a new FrontMCP release, the release pass tags the outgoing release's docs as docs/<version>. That tag is the snapshot.
The server's build script takes each tag with git archive: the content, the sidebar and the release notes. It turns each page into Markdown with the same function the site uses for its .md copies, so the server and the site never disagree. Then it writes a folder per release:
| What | For 1.10.5 |
|---|---|
| Pages, as Markdown | 178 pages, 6.7 MB |
| Sections, with their anchors | 3,662 |
| API entries (decorators, options, error codes, CLI flags) | 4,737 |
| Version notes ("New in", "Changed in") | 709 |
| Pages with runnable examples | 143 |
| Full-text search index | 3.3 MB, 0.87 MB gzipped |
Eleven releases are served today, from 1.9.2 to 1.10.5. The folders are the Worker's static assets: a release's index loads on its first request and stays in memory, and pages load one at a time. A tag never changes, so neither does its snapshot.
Finding the right release
An agent can pin a release in the URL: https://mcp.frontmcp.dev/1.9.4, or /1.9 for the newest patch of 1.9. Usually it doesn't know which release to pin, so the server's instructions tell it to call resolve_version first, with the project's package.json or lockfile. A lockfile gives the installed version; package.json only gives a range, so the lockfile wins when it's there. The answer names the documented release to read and the endpoint that pins it. When the project's release isn't documented, the answer names the nearest one that is, and what changed between them.
The pin is handled before FrontMCP sees the request. The Worker takes the version off the path and passes it to the tools in the request's environment:
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
const pinnedVersion = url.pathname.match(/^\/(\d+(?:\.\d+){1,2})\/?$/)?.[1];
if (pinnedVersion) url.pathname = "/";
return (await handler)(new Request(url, request), ctx, { ...env, DOCS_VERSION: pinnedVersion });
},
} satisfies ExportedHandler<DocsWorkerEnv>;
Every content tool also takes an optional version, so one connection can read two releases during an upgrade. Every answer names the release it comes from.
Tools for what agents do with a shell
The old server exposed a virtual file system and let agents run rg, tree and head over it. We looked at what agents used those commands for, and made each use a tool:
| Tool | What it does |
|---|---|
search_docs | Full-text search by section, returning the page, the heading, path#anchor and a snippet |
get_page | A page or one section as Markdown. It also takes links to the old docs and maps them through the same redirect table the old host uses |
list_pages | The docs as the sidebar has them |
lookup_api | An API by name: @Tool, createFetchHandler, an option like throttle.ipFilter, an error code like TOOL_NOT_FOUND, or a CLI flag |
get_example | A page's runnable example: its files, its tests, and how to run them in a project |
whats_changed | Between two releases: the release notes, every "Changed in" note with its page, and the pages that changed |
resolve_version, get_versions | Which release to read, and which ones are served |
get_skill | FrontMCP's skills catalog for the release |
There's one prompt too, upgrade_frontmcp, which runs whats_changed and asks the agent for an upgrade plan against the user's code.
The whole server is one createFetchHandler() call. Without its metadata:
import { app, FrontMcpInstance } from "@frontmcp/sdk";
const handler = FrontMcpInstance.createFetchHandler({
info: { name: "frontmcp-docs", version: serverJson.version },
instructions: INSTRUCTIONS,
apps: [
app({
id: "docs",
name: "FrontMCP docs",
tools: [...docsTools, ...versionTools, ...referenceTools],
prompts: [upgradePrompt],
}),
],
registry: { name: serverJson.name, description: serverJson.description, publicKey: REGISTRY_PUBLIC_KEY },
tasks: { enabled: false },
});
What we learned
You can't redirect an MCP endpoint
Every old page on the docs host answers with a 301 to its new page. We couldn't do the same for the MCP endpoint, because MCP clients don't follow a redirect on a POST. The new URL has to be written into each client's configuration, so the Nx plugin's template will point at mcp.frontmcp.dev from an upcoming release. The server's first URL, frontmcp.dev/mcp, still answers too: the Worker drops the prefix rather than redirecting.
Index what agents search for, not everything
The first search index was 5.5 MB per release. We dropped code blocks (lookup_api covers names), link URLs and stored fields, and indexed headings and prose only. That halved the index, and a fixed set of test queries returned the same results. Tools that don't search never load it.
Send each page once
A tool that returns a string has it sent as JSON text, {"value":"…"}. With outputSchema: "string", FrontMCP also repeats it as structuredContent, so every page went out twice. The tools that return Markdown now return a finished result, { content: [{ type: "text", text }] }, which FrontMCP sends as it is.
Measure the cold start before choosing a plan
Measured in Node, building the server takes 27 ms, loading a release's search index 130 ms, and a warm search 7 ms. The Workers Free plan allows 10 ms of CPU per request, which can't even build the server, so it runs on Workers Paid. The handler is built once per isolate, and the pinned release reaches the tools with each request.
Rate limit at the edge
FrontMCP's own throttle counts per isolate, and a Worker has many isolates. So the server uses Cloudflare's Rate Limiting binding, by client address: 120 requests a minute, then a 429 with Retry-After: 60 and a JSON-RPC error that tells the agent to wait. FrontMCP's throttle stays as a second line.
Treat the tools as an API
Agents and their prompts depend on tool names and input shapes. A snapshot of tools/list is part of the tests, so a breaking change fails the build; changes are additions only. After every deploy, a script connects as a real client: initialize and tools/list over MCP 2025-06-18, and every tool over 2026-07-28, at the latest endpoint and a pinned one.
In the MCP Registry
The server has its own host, which lets it serve its own /.well-known documents. FrontMCP's registry option, in the code above, serves the Registry's proof that the server owns the dev.frontmcp.mcp namespace, plus a server card and an AI catalog. Publishing is two CLI commands:
npx frontmcp registry keygen
npx frontmcp registry publish --file server.json --key-file ~/.frontmcp/keys/mcp-registry.keykeygen writes the private key outside the repository and prints the public key that goes into the registry option. publish signs the listing with the private key.
Connecting your agent
With Claude Code:
claude mcp add --transport http frontmcp-docs https://mcp.frontmcp.devClients that keep their servers in a JSON file take the URL:
{ "mcpServers": { "frontmcp-docs": { "url": "https://mcp.frontmcp.dev" } } }
It needs no key and no sign-in, only reads, and speaks MCP from 2024-11-05 to 2026-07-28. Docs for Your Agent has the pins, every tool, and what its errors mean.
What's next
Search is full-text today. Semantic search is the next step we're looking at, possibly with VectoriaDB, AgentFront's own vector database. If your agent gets a wrong or missing answer from the server, open an issue with the question it asked.
Code in this post targets the FrontMCP version current when it was written; see Versions for what changed since.