Documentation
Publish a documentation site from MDX files in your repo — with search, an AI assistant, and an MCP endpoint.
On this page
Point the feature at a folder of MDX and it serves a documentation site: navigation, full-text search, an AI assistant that can read your pages, and an MCP endpoint so agents can query them directly.
The site you are reading is built with it.
Documentation is part of the app's ordinary source tree, build, worker, and release. There is no separate docs app, no separate runtime, and no separate deployment pipeline.
Install#
npx deepspace add --info documentation # inspect the feature first
npx deepspace add documentation
The installer creates documentation.json and starter pages under documentation/, wires the Vite plugin and the worker route shown below, adds the feature's rate-limit bindings to wrangler.toml, and gitignores the generated public/_documentation output. If a wired file has been customized past the insertion markers, the installer refuses with exact manual steps instead of overwriting.
From there, use the ordinary lifecycle - npx deepspace dev start, npx deepspace test run, npx deepspace deploy. There is no deepspace docs command group; don't invent commands for it.
What you write#
Two things — a config file and a folder of MDX.
documentation.json # name, domains, navigation
documentation/
index.mdx
guides/
getting-started.mdx
Every page is Markdown or MDX with frontmatter:
---
title: "File uploads"
description: "Let users upload files to R2."
---
Body content. Standard Markdown, plus the built-in components below.
Markdown is the inert default - prefer it. Reach for MDX only when trusted, same-repository React is genuinely useful on a page; MDX may import your app's components directly, so it carries your app's trust level. There is no plugin registry - local components and CSS are just app code.
Wire it up#
Two lines, and add documentation inserts both for you. The Vite plugin compiles the corpus at build time; the worker route serves it.
// vite.config.ts
import { deepSpaceDocumentation } from 'deepspace/documentation'
export default defineConfig({
plugins: [deepSpaceDocumentation(), react()],
})
// worker.ts
import { registerDeepSpaceDocumentation } from 'deepspace/worker'
import documentationConfig from './documentation.json'
registerDeepSpaceDocumentation(app, { resolveAuth, config: documentationConfig })
Configure#
documentation.json is the single configuration surface. Beyond identity and navigation it owns external links, redirects, assistant and MCP access (and their billing mode), contextual actions, SEO, and OpenAPI inputs. The native shape:
{
"name": "DeepSpace Documentation",
"description": "SDK reference and developer documentation.",
"domains": ["docs.deep.space"],
"theme": { "accent": "#635BFF", "logo": "/logo/light.svg", "logoDark": "/logo/dark.svg" },
"navigation": [
{ "group": "Get started", "pages": ["index", "quickstart"] }
],
"links": [{ "label": "GitHub", "href": "https://github.com/example/repo" }],
"redirects": { "/old-path": "/new-path" },
"assistant": { "access": "authenticated" },
"mcp": { "access": "public" },
"seo": { "noindex": false },
"openapi": [{ "source": "openapi.json", "playground": true }]
}
The installed DocumentationConfig types are the authority on every key and its exact shape - inspect them (and the feature's starter config) before adding keys rather than guessing from examples.
The build also accepts the Mintlify-compatible dialect - colors instead of theme.accent, logo: { "light", "dark" }, navigation.groups instead of the flat navigation array, navbar, footer.socials - and normalizes it into the native shape at build time, warning on any key it ignores. This site's own documentation.json is written in that dialect. Whichever dialect you write, it is still one file and one authority.
Navigation is validated at build time#
navigation entries are paths under documentation/ without the extension. When you declare explicit navigation, it must contain every public page:
- A public page missing from navigation is a build error -
N public page(s) are missing from navigation- not a hidden-but-reachable page. To keep a page out of the sidebar deliberately, sethidden: truein its frontmatter. Only omissions are policed: a page listed in more than one place is accepted without complaint, so keeping the sidebar duplicate-free is on you. - A navigation entry pointing at a page or internal route that doesn't exist is equally a build error, as are broken internal links between pages, redirect loops, a redirect source that shadows a real page, and two source files resolving to the same route. Validation is not total, though - a few behaviors stay silent: duplicate navigation entries (above), duplicated
redirectskeys (redirectsis a JSON object, so the same source route listed twice is last-write-wins at parse time, not an error), and link fragments (the#anchorpart is stripped before link validation, so a link to an existing page with a wrong anchor passes).
Treat every one of these as a defect in the corpus and fix it. Don't restructure config to sneak past validation - the checks exist so readers never hit a dead sidebar entry or an unlisted orphan.
Where it serves#
| Config | URL |
|---|---|
| Always | /docs on the app's own origin |
Each entry in domains | that hostname's root — documentation.deep.space/guides/… |
The domains form is how a documentation site gets its own hostname while
living inside an ordinary DeepSpace app. Attach the hostname to the app first
(see custom domains); listing it here only tells the
feature to serve the corpus at that host's root.
What you get for free#
Search across the corpus, built at compile time — no external index, no API key.
An AI assistant with two read-only tools, documentation_search and
documentation_read. It runs on the documentation agent profile, which is
capped at 20 tool calls and cannot reach your app's data — the tool list is
fixed by the SDK, not by the page.
An MCP endpoint, so an agent can query your documentation the same way it queries any other MCP server.
Machine surfaces#
Depending on config, the feature also emits a family of machine-readable surfaces: a per-page Markdown rendering of every page, /docs/llms.txt and /docs/llms-full.txt, /docs/skill.md, sitemap.xml, robots.txt, generated OpenAPI reference pages, the MCP endpoint (/docs/mcp, discoverable at /docs/.well-known/mcp), and the per-page contextual actions (copy as Markdown, open in assistant, and so on).
Consume these through the generated manifest or the runtime routes. Do not infer what is enabled by poking at output directories — the manifest is the contract, the directory layout is an implementation detail.
Assistant access and billing#
assistant.access and mcp.access are explicit choices — make them deliberately:
assistant.access | Who can ask | Who pays |
|---|---|---|
disabled | Nobody | — |
authenticated | Signed-in users | The asking user (their forwarded JWT) |
public | Anyone (rate-limited per IP) | The app owner, via APP_OWNER_JWT |
Never silently flip an assistant to public — that is an owner-billed endpoint exposed to the internet. If the owner wants a public assistant, that's a config change they should see.
The assistant runs on the shared DeepSpace agent runner and model catalog. Never copy that runner, its model list, or its provider policy into app code — a private copy goes stale and can't be fixed by an SDK upgrade.
Customize the reader#
Keep the documentation feature's files together as one app-owned unit: documentation.json, documentation/**/*.md(x), and — only if you need it — a root documentation.tsx.
- Omit
documentation.tsxfor the default reader. Add it only to wrap or replace the documented React surface; it is the single shell customization point, and it's ordinary app code that can import anything in the repo. - Never edit generated
public/_documentation*output. It is build output (the installer gitignores it); regenerate it by building, and keep every hand-authored asset out of it. - Keep media referenced by pages in app-owned source (alongside the pages or in your assets), so the build can validate and rewrite references. An image path that only exists in generated output will not survive the next build.
Components#
Beyond Markdown, pages can use:
| Component | For |
|---|---|
<Note> | An aside the reader should not skip |
<Steps> / <Step title="…"> | An ordered procedure |
Reserved paths#
The feature owns /_documentation and /_documentation-root (and everything
under them). They are on the platform's reserved list, so an app cannot claim
them and a request for one never reaches your routes.
Verify before deploying#
Run a local build and test pass before any deploy, and verify at minimum:
Routes serve the intended release
The ordinary app routes and /docs both serve the release you think they do.
The reader works
Navigation, search, Markdown/MDX rendering, and the branded not-found behavior all function.
Machine surfaces point at the right host
The configured machine surfaces and OpenAPI examples use the intended base URL — not localhost, not a stale domain.
Assistant and MCP match config
Access modes match documentation.json, and neither surface exposes any app-mutation tool.
The basics are clean
Console, keyboard navigation, mobile layout, focus, and accessibility checks pass.
Do not use production as a routine documentation test target. Local dev covers the loop; rehearse risky changes on a staging environment, and let production only ever see verified releases.