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>
<fenced ts block: the page's ONE defining lead code block, REAL verified API, with a first-line comment: // draft - API verified against packages//src/.ts>
... (4-9 H2 sections total; each one job)
Recap
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 ../):
```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):
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-internalin a guide example.
5. The sync command (HARD RULE)
Writers run ONLY the read-only check, never the mutating form:
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 indocs/,--checkexits 1 with1 file(s) out of sync: .../docs/_THROWAWAY-proof.md (1 snippet(s)). - With no drift,
--checkexits 0 withAll snippets are up to date.
6. Other verified ground truth
registerToollives onMcpServer(packages/server/src/server/mcp.ts). The raw-shapeinputSchemaoverload is deprecated — ALWAYS passz.object({...}).- Blessed entry points:
serveStdio(@modelcontextprotocol/server/stdio) andcreateMcpHandler(@modelcontextprotocol/server). - Verify every API you reference against
packages/*/srcat 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.
Source: docs/_meta/CONVENTIONS.md