Skip to content
duxt

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.

PathWhat it is
/llms.txtAn index: site name, summary, then every page's .md link
/llms-full.txtA duxt extension: the same pages in one Markdown file
<page>.mdAny page's own Markdown source
/mcpAn 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.

Was this page helpful?