Machine readers
What a model gets — llms.txt, the raw Markdown of any page, and a real MCP server.
Documentation is read by models as much as by people, and all four answers below are generated from the same collections the pages render from — so none of them can drift from what is published.
| Path | What it is |
|---|---|
/llms.txt | An index: site name, summary, then every page's .md link |
/llms-full.txt | A duxt extension: the same pages in one Markdown file |
<page>.md | Any page's own Markdown source |
/mcp | An MCP server — four tools, from list_versions to read_page |
Markdown, not HTML
Everything here hands over the Markdown as it was written, which is why the
collection schema asks Content to store rawbody. A model given rendered HTML
has to undo a layout to find a heading; one given the source reads what the
author wrote, with the code fences still fences and an MDC block still a
component call.
<page>.md is served by a middleware rather than a route, because .md is a
suffix on the last path segment and not a segment of its own — there is no Nitro
route pattern meaning "any path ending in .md".
How a page announces itself
None of this has to be guessed at. Every documentation page carries two link relations in its head — the first names its own Markdown, the second the index that covers it:
<link rel="alternate" type="text/markdown" href="/duxt/getting-started.md" />
<link rel="describedby" href="/llms.txt" />
The alternate points at the page being rendered rather than at its canonical, so
an old version hands over its own source instead of the current one's. Both are
<link> elements and not HTTP Link headers deliberately: a prerendered page
keeps them when an edge serves it as a static file, with no header configuration
anywhere in the deployment.
Why both llms files
The llmstxt.org convention defines the index. duxt adds
/llms-full.txt as an extension for a model that cannot fetch the .md links
page by page and needs the same default-version documentation at once.
Why a real MCP server
/mcp speaks the protocol through the official SDK rather than being a JSON
endpoint of duxt's own design. A client connects to it the way it connects to
anything else, instead of someone writing an adapter around a shape only this
theme has.
The server exposes four bounded tools: list_versions, list_pages,
search_docs and read_page. Search covers titles, descriptions and the
authored Markdown body, with source, version and path prefixes available to
narrow it. It deliberately does not reuse the browser's client-side index: the
tool has the deployed Content database and returns protocol-sized pages of
results instead. MCP endpoint is the lookup reference
for arguments, pagination and result shapes.
The one Nitro flag duxt turns on for you
nitro.experimental.asyncContext is set by the layer, and /mcp is why. An MCP
tool is called by the SDK, not by Nitro, so it is handed the protocol's own
request object and nothing of the HTTP request behind it — useEvent() is the
only way back to that, and it needs Nitro's async context to exist.
Nothing is asked of you and nothing else changes. A consumer who sets
nitro: { experimental: { asyncContext: false } } turns the MCP server off and
leaves the rest of the site exactly as it was.