# Documentation

Publish a documentation site from MDX files in your repo — with search, an AI assistant, and an MCP endpoint.

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

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

```mdx
---
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.

```ts
// vite.config.ts
import { deepSpaceDocumentation } from 'deepspace/documentation'

export default defineConfig({
  plugins: [deepSpaceDocumentation(), react()],
})
```

```ts
// worker.ts
import { registerDeepSpaceDocumentation } from 'deepspace/worker'
import documentationConfig from './documentation.json'

registerDeepSpaceDocumentation(app, { resolveAuth, config: documentationConfig })
```

**Order matters.** Register it *after* your own `/api` routes — so auth, actions, and AI keep working on the documentation hostname — and *before* the platform proxy and the static/SPA fallback. That is the order the SDK expects.

## 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:

```json
{
  "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.

**Migrating from Mintlify: `documentation.json` becomes the only authority.** Do not keep a `docs.json` around, and do not carry Mintlify aliases or config conventions as a second source of truth - port what you need into `documentation.json` and delete the rest. Two configs describing one site always diverge.

### 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, set `hidden: true` in 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 `redirects` keys (`redirects` is a JSON object, so the same source route listed twice is last-write-wins at parse time, not an error), and link fragments (the `#anchor` part 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](/guides/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.tsx` for 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.

Private paths inside the corpus are refused before routing: a page under a
private path answers 404 rather than being served, so drafts kept alongside
published pages do not leak.

## 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.
