# Versioned Docs for Coding Agents: How We Built FrontMCP's Docs MCP Server

> Coding agents can now read FrontMCP's docs for the release a project uses, from an MCP server built with FrontMCP on a Cloudflare Worker. How it finds the version, what its tools do, and what we learned building it.

Source: https://frontmcp.dev/blog/docs-mcp-server

*[Illustration: A project's package-lock.json names @frontmcp/sdk 1.9.4. resolve_version picks 1.9.4 out of the documented releases, the newest of which is 1.10.5, and the agent reads from the pinned endpoint mcp.frontmcp.dev/1.9.4.]*
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 `createFetchHandler` the 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.

*[Illustration: One snapshot per release. The docs' git tags, docs/1.9.2 to docs/1.10.4, and main for the latest release, 1.10.5, each go through the same build: a git archive of the tag, each page turned into Markdown as the site's .md copies are, then pages/*.md, index.json with the sections, search.json for full-text search, api.json with the APIs by name, and notes.json with the version notes. The Worker serves them as static assets, one folder per release, at mcp.frontmcp.dev/<version>.]*
## 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.

*[Illustration: How an agent reads the docs server. The coding agent calls resolve_version on mcp.frontmcp.dev with the project's lockfile and gets back version 1.9.4 and its endpoint. On the pinned endpoint it calls search_docs for rate limit and gets sections with path#anchor, get_page for one of them and gets that section as Markdown, and whats_changed from 1.9.4 to 1.10.5 and gets the release notes and every Changed in note.]*
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:

```ts
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:

```ts
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:

```bash
npx frontmcp registry keygen
npx frontmcp registry publish --file server.json --key-file ~/.frontmcp/keys/mcp-registry.key
```

`keygen` 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:

```bash
claude mcp add --transport http frontmcp-docs https://mcp.frontmcp.dev
```

Clients that keep their servers in a JSON file take the URL:

```json
{ "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](https://frontmcp.dev/learn/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](https://github.com/agentfront/frontmcp/issues) with the question it asked.
