Skip to content

Remote MCP server

Serve your documentation to agents over MCP from the site's own server, with list_pages and get_page.

AI-generated · awaiting reviewUpdated

With agents: { mcp: true }, the site serves a remote MCP server at /mcp. Any MCP client that speaks Streamable HTTP can connect, without a key, and read the documentation through two tools: list_pages and get_page. The server runs inside the site’s own server output, so there is nothing else to deploy.

The server reads what the build already publishes: the pages and Markdown versions behind llms.txt and site search. There is no search tool; agents list pages and read them, or fetch llms.txt and the .md URLs directly.

Turn it on

The server needs a server adapter, because /mcp answers requests at run time. If the site is static, add the adapter for the host you deploy to: cloudflare, netlify, node or vercel. For a Node server:

npx @cloudflare/nimbus-docs add adapter-node

On a site with no rendering policy, add adapter-cloudflare also sets rendering: { default: "request" }, so docs and API pages render on request. The server doesn’t need that: to keep them prerendered, set rendering: { default: "build" }. The server itself only needs /mcp and its card on request.

Install the MCP server SDK in the project. It is an optional dependency of Nimbus: sites without the server never install it.

npm i @modelcontextprotocol/server@2

Then turn the server on in the Nimbus config:

astro.config.tsts
const nimbusConfig = defineNimbusConfig({
  site: "https://docs.example.com",
  title: "Example",
  agents: { mcp: true },
});

A build with agents.mcp and no server adapter, or without the SDK installed, stops with a message that says what to add; nimbus-docs check reports both before a build.

To turn it off, set agents.mcp to false: /mcp, the card and the Copy MCP URL action go away. You can then remove @modelcontextprotocol/server, and the adapter if nothing else on the site renders on request.

Try it locally

astro dev serves /mcp, without an adapter, from the last content sync. List the tools with the MCP Inspector, at the address astro dev prints, with the base path, and as /mcp/ under trailingSlash: "always":

npx @modelcontextprotocol/inspector --cli http://localhost:4321/mcp --transport http --method tools/list

To try the production build, run it as the adapter does in production: node dist/server/entry.mjs on Node (set HOST and PORT), or wrangler dev on Cloudflare.

The URLs the server returns, and the Copy MCP URL action, use site, not the local address, so point your client at the local address yourself.

What the site serves

Path What it is
/mcp The server. POST a JSON-RPC message; each answer is one application/json body, and a notification gets 202; GET and DELETE answer 405, since the server is stateless and keeps no sessions. A POST whose Content-Type isn’t application/json, or a DELETE without a Content-Type, from anywhere but the site’s own Origin gets Astro’s origin-check 403 instead. OPTIONS answers the browser preflight.
/mcp/server-card The Server Card: the server’s name, the installed Nimbus version, a description and the endpoint URL, served on request as application/mcp-server-card+json with Access-Control-Allow-Origin: *, cacheable for five minutes (Cache-Control: public, max-age=300, s-maxage=300).

The card is listed in /.well-known/ai-catalog.json (unless agents.discovery is false), and llms.txt names the endpoint, so agents that start from either find the server. The server’s name is the site’s hostname reversed plus /docs: docs.example.com becomes com.example.docs/docs. A site on localhost or an IP address is local.nimbus/docs.

Under a base, both paths sit under it: https://docs.example.com/docs/mcp. With trailingSlash: "always", they’re /mcp/ and /mcp/server-card/.

Use exactly that form. The other one gets a 404 in astro dev, and a redirect in production that MCP clients can’t follow. The card, llms.txt and the Copy MCP URL action all give the right form.

A page, content entry or public file at either path fails the build, since the server would hide it.

The tools

list_pages returns pages 200 at a time, each with its title, url, markdownUrl and section, plus description and version when it has them. Pass nextCursor back as cursor for the next batch.

  • section filters by the first path segment: guide for /guide/install/, or index for a root page.
  • version filters by one of the site’s docs versions. Pages outside the docs versions, such as an API reference, appear only unfiltered.
{ "section": "guide" }

get_page returns one page as Markdown: { "title", "url", "markdownUrl", "content" }. Pass the url that list_pages returned, or a site-relative path such as /guide/; only the path of an absolute URL is used. Pages over about 100,000 characters are cut with "truncated": true; the markdownUrl serves the whole page.

{ "url": "https://docs.example.com/guide/" }

Both tools return JSON in a text content block. Errors come back in the result with isError set, as { "error": { "code", "message" } }:

Code Meaning
invalid_input An unknown version, or a cursor from other filters. Arguments that don’t match the schema get the MCP SDK’s message instead.
not_found No published page at that URL.
unavailable The site’s published files couldn’t be read.

Which pages

The tools see the pages that have a Markdown version: collections made with docsCollection() and apiCollection(), in every rendering mode. A collection defined by hand with Astro’s defineCollection has no Markdown version and is not listed, the same as in llms.txt.

list_pages matches site search: it leaves out noindex pages unless they set searchable: true, and deprecated versions unless you ask for one. Drafts and hidden versions are never listed or served.

get_page also reads noindex pages by URL. An API URL with ?version= resolves to the default version, the one with Markdown.

A homepage made in src/pages instead of a content entry isn’t a page here: get_page with / finds nothing, and /index.md serves the llms.txt index instead.

Connect a client

Give the client the endpoint URL using its own remote MCP setup instructions. Configuration formats differ between clients.

The starter’s docs layout shows a Copy MCP URL page action whenever the server is on. Clicking it copies the endpoint URL and briefly changes the label to Copied.

Then in src/layouts/DocsLayout.astro, pass it to PageActions, which renders it after its own actions:

src/layouts/DocsLayout.astroastro
---
import ConnectMcp from "@/components/ConnectMcp.astro";
import { capabilities } from "virtual:nimbus/agent-capabilities";
---

<PageActions markdownUrl={markdownUrl} lastUpdated={lastUpdated} class={description ? undefined : "mt-3"}>
  {capabilities.mcpUrl && <ConnectMcp />}
</PageActions>

The guard keeps the action’s script off sites without the server. A PageActions copied before the slot existed doesn’t render children: update it with nimbus-docs add page-actions --overwrite, or keep <ConnectMcp /> after it.

Limits and safety

The server is read-only and public. It serves what the site already publishes, with Access-Control-Allow-Origin: * so browser clients can connect. There’s no authentication, so keep private docs behind your own access control.

A malformed or null Origin header gets 403, and a body over 64 KB gets 413.

Rate limiting is the host’s job: add a rule for the /mcp path in its firewall, or in a proxy in front of the site. The server itself does none.

Adapters

Nimbus tests the server on the Node and Cloudflare adapters. Node reads the published Markdown from dist/client; Cloudflare reads it through the asset binding.

Other adapters work when the server can read dist/client or fetch it from the site’s URL; test a tools/call on the deployed site. A preview behind access protection answers that fetch with 401, and the tools report unavailable.

With the server on, the server bundle carries the MCP SDK, about 140 KB gzipped, loaded the first time /mcp is called.

Type to search…

↑↓ navigate↵ selectEsc close