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
5 changed files with 11884 additions and 4971 deletions

View file

@ -0,0 +1,146 @@
/**
* Runnable, type-checked companion for `docs/get-started/first-client.md`.
*
* Each `//#region` block is synced byte-for-byte into that page's code fences by
* `pnpm sync:snippets` (`pnpm sync:snippets --check` reports drift). The
* top-level regions are one linear program — the `src/client.ts` the tutorial
* builds — and the file runs for real: `StdioClientTransport` spawns
* `./src/index.ts` (the tutorial's weather server, checked in next to this
* file) over stdio, prints every output the page quotes verbatim, asserts each
* one in the harness blocks, and exits non-zero on any drift.
*
* pnpm --filter @modelcontextprotocol/examples typecheck
* npx tsx firstClient.examples.ts # from examples/guides/get-started/
*
* Two regions live in never-invoked wrappers because the program cannot
* execute them hermetically: `firstClient_callTool` reaches the live NWS API
* (the page describes its weather-dependent output in prose instead of
* quoting it), and `firstClient_registerResource` is the server-side line the
* page has the reader add to `src/index.ts` — `./src/index.ts` already carries
* it, and the `listResources` assertion below fails if the two drift apart.
*
* @module
*/
/* eslint-disable no-console */
import { dirname } from 'node:path';
import { chdir } from 'node:process';
import { fileURLToPath } from 'node:url';
import type { McpServer } from '@modelcontextprotocol/server';
// Harness: anchor the working directory so the transport below resolves
// `src/index.ts` against this directory no matter where the file is launched
// from. The tutorial reader runs `npx tsx src/client.ts` from the project root,
// where the same relative path holds.
chdir(dirname(fileURLToPath(import.meta.url)));
// ---------------------------------------------------------------------------
// "Connect to a server"
// ---------------------------------------------------------------------------
//#region firstClient_connect
import { Client } from '@modelcontextprotocol/client';
import { StdioClientTransport } from '@modelcontextprotocol/client/stdio';
const client = new Client({ name: 'my-first-client', version: '1.0.0' });
const transport = new StdioClientTransport({
command: 'npx',
args: ['tsx', 'src/index.ts']
});
await client.connect(transport);
//#endregion firstClient_connect
// ---------------------------------------------------------------------------
// "List the server's tools"
// ---------------------------------------------------------------------------
//#region firstClient_listTools
const { tools } = await client.listTools();
for (const tool of tools) {
console.log(tool.name, '—', tool.description);
}
//#endregion firstClient_listTools
// Harness: the page quotes the loop's one line verbatim.
if (tools.length !== 1 || tools[0]?.name !== 'get-alerts' || tools[0].description !== 'Get the active weather alerts for a US state') {
throw new Error(`first-client.md listTools output drifted: ${JSON.stringify(tools)}`);
}
// ---------------------------------------------------------------------------
// "Call a tool" — typecheck-only. The happy path hits the live NWS API, so the
// page describes that output in prose; nothing here is quoted verbatim.
// ---------------------------------------------------------------------------
async function firstClient_callTool() {
//#region firstClient_callTool
const result = await client.callTool({ name: 'get-alerts', arguments: { state: 'CA' } });
for (const block of result.content) {
if (block.type === 'text') console.log(block.text);
}
//#endregion firstClient_callTool
}
void firstClient_callTool;
// Harness: the page's "Call a tool" tip quotes the SDK's rejection of
// `{ state: 'California' }` verbatim. The rejection happens before the handler
// (and therefore before any network call), so it runs here for real.
const rejected = await client.callTool({ name: 'get-alerts', arguments: { state: 'California' } });
const rejectedBlock = rejected.content[0];
const quotedRejection =
'Input validation error: Invalid arguments for tool get-alerts: state: Too big: expected string to have <=2 characters';
if (rejected.isError !== true || rejectedBlock?.type !== 'text' || rejectedBlock.text !== quotedRejection) {
throw new Error(`first-client.md tip output drifted from the SDK: ${JSON.stringify(rejected)}`);
}
console.log(rejectedBlock.text);
// ---------------------------------------------------------------------------
// "Add a resource and read it" — the server-side half is the one line the page
// has the reader add to the weather project's `src/index.ts`. It is
// typecheck-only here; `./src/index.ts` (the server this program spawned)
// already registers it, and the assertions below prove the two agree.
// ---------------------------------------------------------------------------
function firstClient_registerResource(server: McpServer) {
//#region firstClient_registerResource
server.registerResource('about', 'weather://about', { title: 'About this server', mimeType: 'text/plain' }, async uri => ({
contents: [{ uri: uri.href, text: 'Alert data comes from the US National Weather Service.' }]
}));
//#endregion firstClient_registerResource
}
void firstClient_registerResource;
//#region firstClient_readResource
const { resources } = await client.listResources();
console.log(resources);
const { contents } = await client.readResource({ uri: 'weather://about' });
console.log(contents);
//#endregion firstClient_readResource
// Harness: the page quotes both logs above verbatim; pin the values they
// depend on so a drift in `./src/index.ts` fails the run instead of silently
// changing the page.
if (resources.length !== 1 || resources[0]?.uri !== 'weather://about' || resources[0].title !== 'About this server') {
throw new Error(`first-client.md listResources output drifted: ${JSON.stringify(resources)}`);
}
const aboutContents = contents[0];
if (
contents.length !== 1 ||
aboutContents === undefined ||
aboutContents.uri !== 'weather://about' ||
!('text' in aboutContents) ||
aboutContents.text !== 'Alert data comes from the US National Weather Service.'
) {
throw new Error(`first-client.md readResource output drifted: ${JSON.stringify(contents)}`);
}
// ---------------------------------------------------------------------------
// "Close the connection"
// ---------------------------------------------------------------------------
//#region firstClient_close
await client.close();
//#endregion firstClient_close

View file

@ -0,0 +1,93 @@
/**
* Runnable, type-checked companion for `docs/get-started/first-server.md`.
*
* Each `//#region` block is synced byte-for-byte into that page's code fences by
* `pnpm sync:snippets` (`pnpm sync:snippets --check` reports drift). The regions
* are one linear program — the `src/index.ts` the tutorial builds. Running the
* file (`npx tsx firstServer.examples.ts`) starts that server and then runs the
* harness below the regions, which proves the validation-error output the page
* quotes verbatim and exits.
*
* @module
*/
/* eslint-disable no-console */
//#region firstServer_registerTool
import { McpServer } from '@modelcontextprotocol/server';
import { serveStdio } from '@modelcontextprotocol/server/stdio';
import * as z from 'zod/v4';
const NWS_API = 'https://api.weather.gov';
interface AlertsResponse {
features: { properties: { event?: string; headline?: string } }[];
}
function createServer(): McpServer {
const server = new McpServer({ name: 'weather', version: '1.0.0' });
server.registerTool(
'get-alerts',
{
description: 'Get the active weather alerts for a US state',
inputSchema: z.object({
state: z.string().length(2).describe('Two-letter US state code, e.g. CA')
})
},
async ({ state }) => {
const code = state.toUpperCase();
const url = `${NWS_API}/alerts/active?area=${code}`;
const res = await fetch(url, { headers: { 'User-Agent': 'mcp-weather-tutorial/1.0' } });
if (!res.ok) {
return { content: [{ type: 'text', text: `NWS API error: HTTP ${res.status}` }], isError: true };
}
const { features } = (await res.json()) as AlertsResponse;
if (features.length === 0) {
return { content: [{ type: 'text', text: `No active alerts for ${code}.` }] };
}
const lines = features.map(f => f.properties.headline ?? f.properties.event ?? 'Unnamed alert');
return { content: [{ type: 'text', text: lines.join('\n') }] };
}
);
return server;
}
//#endregion firstServer_registerTool
//#region firstServer_serve
void serveStdio(createServer);
console.error('weather MCP server running on stdio');
//#endregion firstServer_serve
// ---------------------------------------------------------------------------
// Harness (not shown on the page). The page's ::: tip quotes the SDK's
// validation error for `get-alerts` called with `{ state: 'California' }`
// verbatim; this proves it. A second server instance from the same factory is
// driven by an in-memory client (any MCP client behaves the same), and the
// process exits non-zero if the produced text drifts from what the page quotes.
// `serveStdio` above is still waiting on stdin, so the harness exits explicitly.
// Imported dynamically so the page's lead region stays self-contained.
// ---------------------------------------------------------------------------
const { Client, InMemoryTransport } = await import('@modelcontextprotocol/client');
const harnessServer = createServer();
const harnessClient = new Client({ name: 'first-server-docs-harness', version: '1.0.0' });
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await harnessServer.connect(serverTransport);
await harnessClient.connect(clientTransport);
// "Call `get-alerts` with `{ "state": "California" }`" — the rejection the tip quotes.
const rejected = await harnessClient.callTool({ name: 'get-alerts', arguments: { state: 'California' } });
const block = rejected.content[0];
const quotedOnPage =
'Input validation error: Invalid arguments for tool get-alerts: state: Too big: expected string to have <=2 characters';
if (rejected.isError !== true || block?.type !== 'text' || block.text !== quotedOnPage) {
throw new Error(`first-server.md tip output drifted from the SDK: ${JSON.stringify(rejected)}`);
}
await harnessClient.close();
await harnessServer.close();
// `serveStdio` above is still reading stdin; this file runs as a program, so end it here.
// eslint-disable-next-line unicorn/no-process-exit
process.exit(0);

View file

@ -0,0 +1,28 @@
// docs: typecheck-only
/**
* Type-checked companion for `docs/get-started/packages.md`.
*
* Each `//#region` block is synced byte-for-byte into that page's `ts` fences by
* `pnpm sync:snippets` (`pnpm sync:snippets --check` reports drift). The page is
* an explanation of the published packages and their subpath exports, so the
* regions are import shapes — nothing meaningful to run.
*
* pnpm --filter @modelcontextprotocol/examples typecheck
*
* @module
*/
/* eslint-disable @typescript-eslint/no-unused-vars */
// "Start from one package" — the two import paths the first-server tutorial used.
//#region packages_serverEntryPoints
import { McpServer } from '@modelcontextprotocol/server';
import { serveStdio } from '@modelcontextprotocol/server/stdio';
//#endregion packages_serverEntryPoints
// "Keep Node-only code behind the ./stdio subpath" — the client-side pair.
//#region packages_clientStdioSubpath
// Runs anywhere: browsers, Workers, Node.
import { Client } from '@modelcontextprotocol/client';
// Spawns a child process — Node-only, so it lives behind the subpath.
import { StdioClientTransport } from '@modelcontextprotocol/client/stdio';
//#endregion packages_clientStdioSubpath

View file

@ -0,0 +1,95 @@
/**
* Runnable, type-checked companion for `docs/get-started/real-host.md`.
*
* The page registers an existing server in MCP hosts; its one `ts` fence is
* the tail of the `src/index.ts` built in `first-server.md` — the entry every
* host on the page launches. The `//#region` block is synced byte-for-byte
* into the page by `pnpm sync:snippets` (`pnpm sync:snippets --check` reports
* drift). Running the file (`npx tsx realHost.examples.ts`) prints the stderr
* banner the page quotes verbatim, proves over an in-memory client that the
* launched server advertises exactly the one `get-alerts` tool the page says a
* host lists, and exits.
*
* @module
*/
/* eslint-disable no-console */
import { McpServer } from '@modelcontextprotocol/server';
import { serveStdio } from '@modelcontextprotocol/server/stdio';
import * as z from 'zod/v4';
// ---------------------------------------------------------------------------
// The server from "Build your first server" — one `get-alerts` tool. The page
// adds no server code; this factory is its prerequisite, reproduced here so the
// file is the same `src/index.ts` a host launches with `npx tsx src/index.ts`.
// ---------------------------------------------------------------------------
const NWS_API = 'https://api.weather.gov';
interface AlertsResponse {
features: { properties: { event?: string; headline?: string } }[];
}
function createServer(): McpServer {
const server = new McpServer({ name: 'weather', version: '1.0.0' });
server.registerTool(
'get-alerts',
{
description: 'Get the active weather alerts for a US state',
inputSchema: z.object({
state: z.string().length(2).describe('Two-letter US state code, e.g. CA')
})
},
async ({ state }) => {
const code = state.toUpperCase();
const url = `${NWS_API}/alerts/active?area=${code}`;
const res = await fetch(url, { headers: { 'User-Agent': 'mcp-weather-tutorial/1.0' } });
if (!res.ok) {
return { content: [{ type: 'text', text: `NWS API error: HTTP ${res.status}` }], isError: true };
}
const { features } = (await res.json()) as AlertsResponse;
if (features.length === 0) {
return { content: [{ type: 'text', text: `No active alerts for ${code}.` }] };
}
const lines = features.map(f => f.properties.headline ?? f.properties.event ?? 'Unnamed alert');
return { content: [{ type: 'text', text: lines.join('\n') }] };
}
);
return server;
}
//#region realHost_serve
void serveStdio(createServer);
console.error('weather MCP server running on stdio');
//#endregion realHost_serve
// ---------------------------------------------------------------------------
// Harness (not shown on the page). A host's first move after launching the
// command above is `tools/list`; the page claims the result is the single
// `get-alerts` tool. An in-memory client connected to the same factory proves
// it — any MCP host sees the same list over stdio. `serveStdio` above is still
// waiting on stdin, so the harness exits explicitly. Imported dynamically so
// the page's region stays exactly the tail of `src/index.ts`.
// ---------------------------------------------------------------------------
const { Client, InMemoryTransport } = await import('@modelcontextprotocol/client');
const harnessServer = createServer();
const harnessClient = new Client({ name: 'real-host-docs-harness', version: '1.0.0' });
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await harnessServer.connect(serverTransport);
await harnessClient.connect(clientTransport);
// The list every host shows after it trusts and starts the server.
const { tools } = await harnessClient.listTools();
const names = tools.map(tool => tool.name);
if (names.length !== 1 || names[0] !== 'get-alerts') {
throw new Error(`real-host.md expects hosts to list exactly [get-alerts], got: ${JSON.stringify(names)}`);
}
await harnessClient.close();
await harnessServer.close();
// `serveStdio` above is still reading stdin; this file runs as a program, so end it here.
// eslint-disable-next-line unicorn/no-process-exit
process.exit(0);

View file

@ -0,0 +1,62 @@
/**
* The `src/index.ts` the get-started tutorials build: the weather server from
* `docs/get-started/first-server.md` plus the `about` resource that
* `docs/get-started/first-client.md` adds.
*
* `firstClient.examples.ts` (one directory up) spawns this file over stdio
* exactly as the tutorial reader's client does — `npx tsx src/index.ts` from
* the project root. Keep `get-alerts` in lockstep with
* `firstServer.examples.ts`, and `registerResource` in lockstep with the
* `firstClient_registerResource` region in `firstClient.examples.ts` (the
* harness there asserts on the values this file advertises).
*
* @module
*/
/* eslint-disable no-console */
import { McpServer } from '@modelcontextprotocol/server';
import { serveStdio } from '@modelcontextprotocol/server/stdio';
import * as z from 'zod/v4';
const NWS_API = 'https://api.weather.gov';
interface AlertsResponse {
features: { properties: { event?: string; headline?: string } }[];
}
function createServer(): McpServer {
const server = new McpServer({ name: 'weather', version: '1.0.0' });
server.registerTool(
'get-alerts',
{
description: 'Get the active weather alerts for a US state',
inputSchema: z.object({
state: z.string().length(2).describe('Two-letter US state code, e.g. CA')
})
},
async ({ state }) => {
const code = state.toUpperCase();
const url = `${NWS_API}/alerts/active?area=${code}`;
const res = await fetch(url, { headers: { 'User-Agent': 'mcp-weather-tutorial/1.0' } });
if (!res.ok) {
return { content: [{ type: 'text', text: `NWS API error: HTTP ${res.status}` }], isError: true };
}
const { features } = (await res.json()) as AlertsResponse;
if (features.length === 0) {
return { content: [{ type: 'text', text: `No active alerts for ${code}.` }] };
}
const lines = features.map(f => f.properties.headline ?? f.properties.event ?? 'Unnamed alert');
return { content: [{ type: 'text', text: lines.join('\n') }] };
}
);
// Added by docs/get-started/first-client.md ("Add a resource and read it").
server.registerResource('about', 'weather://about', { title: 'About this server', mimeType: 'text/plain' }, async uri => ({
contents: [{ uri: uri.href, text: 'Alert data comes from the US National Weather Service.' }]
}));
return server;
}
void serveStdio(createServer);
console.error('weather MCP server running on stdio');