docs: rebuild the v2 documentation around a task-based page tree (#2397)
This commit is contained in:
parent
708d5455d9
commit
b153505428
1 changed files with 11884 additions and 4971 deletions
249
docs/_meta/CONVENTIONS.md
Normal file
249
docs/_meta/CONVENTIONS.md
Normal 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`.
|
||||
Loading…
Reference in a new issue