docs: rebuild the v2 documentation around a task-based page tree (#2397)
This commit is contained in:
parent
708d5455d9
commit
b153505428
5 changed files with 11884 additions and 4971 deletions
146
examples/guides/get-started/firstClient.examples.ts
Normal file
146
examples/guides/get-started/firstClient.examples.ts
Normal 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
|
||||
93
examples/guides/get-started/firstServer.examples.ts
Normal file
93
examples/guides/get-started/firstServer.examples.ts
Normal 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);
|
||||
28
examples/guides/get-started/packages.examples.ts
Normal file
28
examples/guides/get-started/packages.examples.ts
Normal 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
|
||||
95
examples/guides/get-started/realHost.examples.ts
Normal file
95
examples/guides/get-started/realHost.examples.ts
Normal 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);
|
||||
62
examples/guides/get-started/src/index.ts
Normal file
62
examples/guides/get-started/src/index.ts
Normal 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');
|
||||
Loading…
Reference in a new issue