docs: rebuild the v2 documentation around a task-based page tree (#2397)

This commit is contained in:
Felix Weinberger 2026-06-30 21:18:42 +01:00 • committed by GitHub
commit b153505428
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

249
docs/_meta/CONVENTIONS.md Normal file
View file

@ -0,0 +1,249 @@
# Docs page conventions
Single source of truth for form. Three blocks: REGISTER (voice), SCAFFOLD FORMAT
(page shape), WIRING (snippet mechanics, verified in this worktree). Every writer
agent reads this file before touching a page.
---
## REGISTER
THE REGISTER — "restrained tiangolo". Felix chose this against live samples. Every fully
written page must pass EVERY rule; the voice reviewer fails pages per-rule.
R1. First screenful: a code block, or one lead-in sentence then a code block. Zero preamble.
Carve-out (Felix, 2026-06-30): the LANDING PAGE (index.md) leads with a short "what is
MCP" orientation paragraph + a spec link BEFORE the code. Every other page: code first.
R2. No main-flow paragraph exceeds 3 sentences; most are 1-2.
R3. Second person, imperative, present tense. Never "we", never "the user", never "one can".
R4. Every code block has a one-line lead-in naming the ONE thing it adds or changes.
Never two code blocks adjacent.
R5. After a capability lands, state the observable result; when there is output, quote it
verbatim in its own fenced block — and it must be REAL (produced by the companion example).
R6. The schema-payoff sentence appears at most ONCE per page, as a plain sentence
("From that one schema the SDK derives the JSON Schema the model sees, validates arguments
before your handler runs, and infers the handler's argument types."). Never a refrain,
never a bullet ceremony.
R7. Asides leave the main column: tips, version notes, "coming from X" -> ::: tip / ::: info /
::: warning containers (VitePress syntax). The page read WITHOUT its asides is still a
complete working path.
R8. Era caveats are ONE line linking /protocol-versions — never inline era prose. Deprecation
(SEP-2577: sampling, roots) is the opposite: a loud ::: warning banner at the TOP of that
page, migration target named first.
R9. Headings on doing-pages are imperative micro-steps ("Add a tool", "Validate the input"),
not noun labels.
R10. Bold a key term once on first introduction. Backtick every identifier. Never {@linkcode}.
Define terms inline; link for depth, never instead of defining.
R11. End with "## Recap": 3-6 flat bullets, each one claim already made on the page, zero code,
zero new information. No "Next up" handoff line after it.
R12. BANNED: theatrical stingers ("Look at what you passed in", "You didn't write any of them",
"That's it.", "That's the whole API."); exclamation marks; rhetorical questions; "Let's";
"we will"; "In this section"; "simply"; "basically"; "It is important to note"; emoji;
passive voice for SDK behavior (write "the SDK generates", not "is generated by").
R13. No option/parameter tables on narrative pages. Two carve-outs only: the error-code table
at the bottom of servers/errors.md, and the one-line install command at the top of every
serving/ recipe page.
R14. No hedging ("you might want to", "consider"). State what to do.
R15. Never write DEFENSIVELY. No prose that justifies a choice to an imagined reviewer:
no "this page is structured this way because", no "we use X rather than Y since",
no pre-empting objections, no apologizing for an API. Teach what the reader does;
rationale belongs in PR descriptions and ADRs, never on the page. If a sentence
answers "why did the AUTHOR do it this way" instead of "what does the READER do
next", delete it. (Source: Felix's #2390 review pattern, 2026-06-29 — both of his
flags that round were defensive prose; the fix each time was contract + the one
sharp edge + minimal usage, justification moved out.)
The target feel — confident, plain, code-first, not performed. Calibration sample (Felix-approved):
## Add a tool
registerTool takes a name, a config, and a handler.
inputSchema is a Zod schema — the only schema you write.
server.registerTool('search', {
description: 'Search the product catalog',
inputSchema: z.object({
query: z.string(),
limit: z.number().int().max(50).optional(),
}),
}, async ({ query, limit }) => { ... })
From that one schema the SDK derives the JSON Schema the
model sees, validates arguments before your handler runs,
and infers the handler's argument types.
::: tip
Call the tool with limit: 999 and the SDK rejects it
before your function runs.
:::
## Recap
* registerTool(name, config, handler) registers a tool.
* One Zod schema: wire schema, validation, handler types.
---
## SCAFFOLD FORMAT
SCAFFOLD FORMAT — every non-calibration page is a SKELETON, not prose. Exact shape:
---
status: scaffold
shape: <tutorial|how-to|explanation|reference>
---
# <Title>
<!-- SCAFFOLD - structure only; prose comes in a later tranche.
scope: <the one-line scope from the approved tree, verbatim>
teaches: <symbolA>, <symbolB>, ...
source: mined from docs/<file>.md "<heading>" | net-new
-->
## <Imperative micro-step heading>
<!-- teaches: X | salvage: docs/servers/tools.md "<heading>" -->
<fenced ts block: the page's ONE defining lead code block, REAL verified API, with a
first-line comment: // draft - API verified against packages/<pkg>/src/<file>.ts>
<!-- result: one line, what the reader observes -->
## <Imperative micro-step heading>
<!-- teaches: Y -->
<!-- code: one line describing the block that goes here -->
... (4-9 H2 sections total; each one job)
## Recap
<!-- the 3-6 claims this page will prove -->
Only the FIRST code block is real (and source-verified). Later blocks are comment placeholders.
H2 count and ordering ARE the deliverable — they get structure-reviewed.
Special cases: sampling.md and roots.md open with a ::: warning sunset banner placeholder
(SEP-2577; name the migration target). Each serving/ recipe page's first body line is the
install one-liner. real-host.md: VS Code leads the main flow; Claude Code and Cursor are
labeled H3 subsections after it (Felix ruling).
---
## WIRING
How a code fence in a docs page is wired to a typechecked example file. All of this
was verified live in this worktree on 2026-06-29 (see steps below); the mechanics are
implemented by `scripts/sync-snippets.ts`.
### 1. Region syntax (in `examples/guides/**/*.examples.ts`)
A region is a named block delimited by line comments. The name MUST be identical on the
open and close marker:
```
//#region instructions_basic
const server = new McpServer(
{ name: 'db-server', version: '1.0.0' },
{ instructions: '...' }
);
//#endregion instructions_basic
```
Regions live at the top level of the companion file, or nested inside a helper function
when the snippet needs surrounding setup (see `examples/guides/serving/stdio.examples.ts`
for the former and `examples/guides/clients/machine-auth.examples.ts` for the latter).
The sync script dedents the region body to the indentation of the `//#region` line, so
any wrapper indentation is stripped in the rendered fence. Region extraction only works
for `.ts` files. Region names follow `exportedName_variant` (e.g. `registerTool_search`).
### 2. Fence attribute syntax (in the `.md` page)
The opening fence carries a `source="<relative-path>#<region>"` attribute. The body
between the fences is OWNED by the sync script — it is overwritten on every mutating
run, and `--check` reports drift if it does not already match the region byte-for-byte.
Real, working example (this exact fence is live in this file and is verified by
`pnpm sync:snippets --check`; this file lives in `_meta/`, one level deeper than a
top-level page, hence the extra `../` — a page at `docs/<page>.md` uses one `../`):
````md
```ts source="../../examples/guides/serving/stdio.examples.ts#serveStdio_basic"
import { McpServer } from '@modelcontextprotocol/server';
import { serveStdio } from '@modelcontextprotocol/server/stdio';
const handle = serveStdio(() => {
const server = new McpServer({ name: 'notes', version: '1.0.0' });
// server.registerTool(...) — one factory builds the instance that serves the connection
return server;
});
```
````
Notes on the exact form (regex `MARKDOWN_LABELED_FENCE_PATTERN` in sync-snippets.ts):
- The opening fence must start at column 0: `` ```ts source="<path>#<region>" ``.
- An optional display filename may appear BEFORE `source=`:
`` ```ts my-app.ts source="./x.examples.ts#foo" ``.
- Omit `#<region>` to inline an entire file (any file type, e.g. `json`, `sh`).
- The closing fence must be a bare ```` ``` ```` on its own line.
### 3. Path resolution rule
The path in `source="..."` is resolved RELATIVE TO THE MARKDOWN FILE's own directory
(`resolve(dirname(mdFile), examplePath)` in the script). It is NOT relative to the repo
root and NOT relative to `docs/`.
From the standard locations, the prefixes are:
| page location | companion example location | `source=` prefix |
|--------------------------------|------------------------------------------|------------------|
| `docs/<page>.md` | `examples/guides/<file>.examples.ts` | `../examples/guides/` |
| `docs/<section>/<page>.md` | `examples/guides/<section>/<file>.examples.ts` | `../../examples/guides/<section>/` |
So a page at `docs/get-started/first-server.md` whose companion is
`examples/guides/get-started/firstServer.examples.ts` uses:
```
source="../../examples/guides/get-started/firstServer.examples.ts#<region>"
```
The sync script scans `docs/**/*.md`.
### 4. How example files are typechecked
`examples/guides/` has NO package.json or tsconfig of its own. It is part of the
`@modelcontextprotocol/examples` workspace package (`examples/package.json`), whose
`examples/tsconfig.json` includes `"./"` recursively — so new files under
`examples/guides/<section>/*.examples.ts` are picked up with ZERO config changes.
The typecheck command (verified passing in this worktree):
```sh
pnpm --filter @modelcontextprotocol/examples typecheck
```
(it runs `tsgo -p tsconfig.json --noEmit` inside `examples/`.)
Import rules for example files (enforced by that tsconfig's `paths` mapping):
- `import { McpServer } from '@modelcontextprotocol/server'`
- `import { serveStdio, StdioServerTransport } from '@modelcontextprotocol/server/stdio'`
- `import * as z from 'zod/v4'` ← the ONLY zod import form used in this repo.
- Never import from `@modelcontextprotocol/core-internal` in a guide example.
### 5. The sync command (HARD RULE)
Writers run ONLY the read-only check, never the mutating form:
```sh
pnpm sync:snippets --check
```
NEVER run bare `pnpm sync:snippets` — it rewrites every fenced block in `docs/` and
`docs/` in place, and concurrent mutating runs from parallel agents race and clobber
each other. Hand-write the fence body to match the region exactly; `--check` confirms.
Verified outputs from prep (2026-06-29):
- With a stale `source=` fence present in `docs/`, `--check` exits 1 with
`1 file(s) out of sync: .../docs/_THROWAWAY-proof.md (1 snippet(s))`.
- With no drift, `--check` exits 0 with `All snippets are up to date`.
### 6. Other verified ground truth
- `registerTool` lives on `McpServer` (`packages/server/src/server/mcp.ts`). The
raw-shape `inputSchema` overload is deprecated — ALWAYS pass `z.object({...})`.
- Blessed entry points: `serveStdio` (`@modelcontextprotocol/server/stdio`) and
`createMcpHandler` (`@modelcontextprotocol/server`).
- Verify every API you reference against `packages/*/src` at this commit before writing
it into a lead code block; annotate the block's first line with
`// draft - API verified against packages/<pkg>/src/<file>.ts`.