TTypeScript SDKSource ↗
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.

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-internal in 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 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.

Source: docs/_meta/CONVENTIONS.md

Generated by smolify with deterministic-repository-import-v1 · 1 source file