docs: rebuild the v2 documentation around a task-based page tree (#2397)
This commit is contained in:
parent
708d5455d9
commit
b153505428
10 changed files with 11884 additions and 4971 deletions
183
examples/guides/servers/completion.examples.ts
Normal file
183
examples/guides/servers/completion.examples.ts
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
/**
|
||||
* Companion example for `docs/servers/completion.md`.
|
||||
*
|
||||
* Every `ts` fence on that page is synced from a `//#region` in this file
|
||||
* (`pnpm sync:snippets --check`). The file also runs: the harness below the
|
||||
* regions connects an in-memory client and produces the output the page quotes
|
||||
* verbatim.
|
||||
*
|
||||
* pnpm --filter @modelcontextprotocol/examples typecheck
|
||||
* npx tsx guides/servers/completion.examples.ts # from examples/
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
/* eslint-disable no-console */
|
||||
//#region completable_language
|
||||
import { completable, McpServer, ResourceTemplate } from '@modelcontextprotocol/server';
|
||||
import * as z from 'zod/v4';
|
||||
|
||||
const languages = ['typescript', 'javascript', 'python', 'rust', 'go'];
|
||||
|
||||
const server = new McpServer({ name: 'review', version: '1.0.0' });
|
||||
|
||||
server.registerPrompt(
|
||||
'review-code',
|
||||
{
|
||||
description: 'Review code for best practices',
|
||||
argsSchema: z.object({
|
||||
language: completable(z.string().describe('Programming language'), value =>
|
||||
languages.filter(language => language.startsWith(value))
|
||||
)
|
||||
})
|
||||
},
|
||||
({ language }) => ({
|
||||
messages: [
|
||||
{
|
||||
role: 'user' as const,
|
||||
content: { type: 'text' as const, text: `Review this ${language} code for best practices.` }
|
||||
}
|
||||
]
|
||||
})
|
||||
);
|
||||
//#endregion completable_language
|
||||
|
||||
// "Return suggestions from the complete callback" — `repo` completes from an
|
||||
// async lookup; `branch` points at `completeBranch`, defined in the next region.
|
||||
//#region registerPrompt_async
|
||||
const branchesByRepo: Record<string, string[]> = {
|
||||
'typescript-sdk': ['main', 'release/1.x', 'release/2.x'],
|
||||
'python-sdk': ['main', 'release/1.x'],
|
||||
inspector: ['main']
|
||||
};
|
||||
|
||||
async function listRepos(): Promise<string[]> {
|
||||
return Object.keys(branchesByRepo);
|
||||
}
|
||||
|
||||
server.registerPrompt(
|
||||
'review-pr',
|
||||
{
|
||||
description: 'Review the open pull requests on one branch',
|
||||
argsSchema: z.object({
|
||||
repo: completable(z.string().describe('Repository name'), async value => {
|
||||
const repos = await listRepos();
|
||||
return repos.filter(repo => repo.startsWith(value));
|
||||
}),
|
||||
branch: completable(z.string().describe('Target branch'), completeBranch)
|
||||
})
|
||||
},
|
||||
({ repo, branch }) => ({
|
||||
messages: [
|
||||
{
|
||||
role: 'user' as const,
|
||||
content: { type: 'text' as const, text: `Review the open pull requests on ${repo}@${branch}.` }
|
||||
}
|
||||
]
|
||||
})
|
||||
);
|
||||
//#endregion registerPrompt_async
|
||||
|
||||
// "Use the other arguments for context" — `branch` suggestions depend on the
|
||||
// `repo` the client has already filled in. Function declarations hoist, so the
|
||||
// registration above can reference this by name.
|
||||
//#region completeCallback_context
|
||||
async function completeBranch(value: string, context?: { arguments?: Record<string, string> }): Promise<string[]> {
|
||||
const repo = context?.arguments?.repo;
|
||||
if (!repo) return [];
|
||||
return (branchesByRepo[repo] ?? []).filter(branch => branch.startsWith(value));
|
||||
}
|
||||
//#endregion completeCallback_context
|
||||
|
||||
// "Complete a resource template variable" — the template's `complete` map.
|
||||
//#region resourceTemplate_complete
|
||||
server.registerResource(
|
||||
'readme',
|
||||
new ResourceTemplate('repo://{repo}/readme', {
|
||||
list: undefined,
|
||||
complete: {
|
||||
repo: async value => {
|
||||
const repos = await listRepos();
|
||||
return repos.filter(repo => repo.startsWith(value));
|
||||
}
|
||||
}
|
||||
}),
|
||||
{ description: 'The README of one repository' },
|
||||
async (uri, { repo }) => ({
|
||||
contents: [{ uri: uri.href, text: `Repository: ${repo}` }]
|
||||
})
|
||||
);
|
||||
//#endregion resourceTemplate_complete
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Harness (not shown on the page). An in-memory client drives the requests
|
||||
// whose output servers/completion.md quotes verbatim. Any MCP client behaves
|
||||
// the same. Imported dynamically so the page's lead region stays self-contained.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const { Client, InMemoryTransport } = await import('@modelcontextprotocol/client');
|
||||
|
||||
const client = new Client({ name: 'completion-docs-harness', version: '1.0.0' });
|
||||
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
||||
await server.connect(serverTransport);
|
||||
await client.connect(clientTransport);
|
||||
|
||||
// Proof for "Wrap an argument with `completable`": the first completable
|
||||
// registration advertised the `completions` capability without any declaration.
|
||||
if (!client.getServerCapabilities()?.completions) {
|
||||
throw new Error('completion.md claim failed: completions capability was not advertised');
|
||||
}
|
||||
|
||||
// Proof for "Wrap an argument with `completable`": `language` typed as `ty`.
|
||||
const language = await client.complete({
|
||||
ref: { type: 'ref/prompt', name: 'review-code' },
|
||||
argument: { name: 'language', value: 'ty' }
|
||||
});
|
||||
if (language.completion.values.join(',') !== 'typescript') {
|
||||
throw new Error(`completion.md claim failed: language 'ty' -> ${JSON.stringify(language.completion.values)}`);
|
||||
}
|
||||
|
||||
// "Return suggestions from the complete callback" — the async `repo` result the page quotes.
|
||||
const repo = await client.complete({
|
||||
ref: { type: 'ref/prompt', name: 'review-pr' },
|
||||
argument: { name: 'repo', value: 'ty' }
|
||||
});
|
||||
console.log(repo.completion);
|
||||
|
||||
// "Use the other arguments for context" — the context-narrowed `branch` result the page quotes.
|
||||
const branch = await client.complete({
|
||||
ref: { type: 'ref/prompt', name: 'review-pr' },
|
||||
argument: { name: 'branch', value: 'rel' },
|
||||
context: { arguments: { repo: 'typescript-sdk' } }
|
||||
});
|
||||
console.log(branch.completion);
|
||||
|
||||
// "Use the other arguments for context" — without context the callback returns nothing.
|
||||
const branchNoContext = await client.complete({
|
||||
ref: { type: 'ref/prompt', name: 'review-pr' },
|
||||
argument: { name: 'branch', value: 'rel' }
|
||||
});
|
||||
if (branchNoContext.completion.values.length !== 0) {
|
||||
throw new Error(`completion.md claim failed: branch without context -> ${JSON.stringify(branchNoContext.completion.values)}`);
|
||||
}
|
||||
|
||||
// Proof for "Complete a resource template variable": the request targets the
|
||||
// template by its URI pattern.
|
||||
const templateRepo = await client.complete({
|
||||
ref: { type: 'ref/resource', uri: 'repo://{repo}/readme' },
|
||||
argument: { name: 'repo', value: 'py' }
|
||||
});
|
||||
if (templateRepo.completion.values.join(',') !== 'python-sdk') {
|
||||
throw new Error(`completion.md claim failed: template repo 'py' -> ${JSON.stringify(templateRepo.completion.values)}`);
|
||||
}
|
||||
|
||||
// "Try it from a client" — the call and output the page quotes.
|
||||
//#region complete_client
|
||||
const result = await client.complete({
|
||||
ref: { type: 'ref/prompt', name: 'review-code' },
|
||||
argument: { name: 'language', value: '' }
|
||||
});
|
||||
console.log(result.completion);
|
||||
//#endregion complete_client
|
||||
|
||||
await client.close();
|
||||
await server.close();
|
||||
173
examples/guides/servers/elicitation.examples.ts
Normal file
173
examples/guides/servers/elicitation.examples.ts
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
/**
|
||||
* Companion example for `docs/servers/elicitation.md`.
|
||||
*
|
||||
* Every `ts` fence on that page is synced from a `//#region` in this file
|
||||
* (`pnpm sync:snippets --check`). The file also runs: the harness below the
|
||||
* regions drives every elicitation round over an in-memory transport pair and
|
||||
* prints the output the page quotes verbatim.
|
||||
*
|
||||
* pnpm --filter @modelcontextprotocol/examples typecheck
|
||||
* npx tsx guides/servers/elicitation.examples.ts # from examples/
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
/* eslint-disable no-console */
|
||||
//#region registerTool_elicitForm
|
||||
import { McpServer } from '@modelcontextprotocol/server';
|
||||
import * as z from 'zod/v4';
|
||||
|
||||
const server = new McpServer({ name: 'feedback', version: '1.0.0' });
|
||||
|
||||
server.registerTool(
|
||||
'collect-feedback',
|
||||
{
|
||||
description: 'Ask the user how something went',
|
||||
inputSchema: z.object({ topic: z.string() })
|
||||
},
|
||||
async ({ topic }, ctx) => {
|
||||
const result = await ctx.mcpReq.elicitInput({
|
||||
mode: 'form',
|
||||
message: `How was ${topic}?`,
|
||||
requestedSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
rating: { type: 'number', title: 'Rating (1-5)', minimum: 1, maximum: 5 },
|
||||
comment: { type: 'string', title: 'Comment' }
|
||||
},
|
||||
required: ['rating']
|
||||
}
|
||||
});
|
||||
if (result.action !== 'accept') {
|
||||
return { content: [{ type: 'text', text: `Feedback ${result.action}.` }] };
|
||||
}
|
||||
return { content: [{ type: 'text', text: `Recorded: ${JSON.stringify(result.content)}` }] };
|
||||
}
|
||||
);
|
||||
//#endregion registerTool_elicitForm
|
||||
|
||||
// "Handle every action" — a confirmation form whose handler answers all three actions.
|
||||
//#region registerTool_elicitActions
|
||||
server.registerTool(
|
||||
'delete-dataset',
|
||||
{
|
||||
description: 'Delete a dataset after the user confirms',
|
||||
inputSchema: z.object({ name: z.string() })
|
||||
},
|
||||
async ({ name }, ctx) => {
|
||||
const result = await ctx.mcpReq.elicitInput({
|
||||
mode: 'form',
|
||||
message: `Delete ${name}? This cannot be undone.`,
|
||||
requestedSchema: {
|
||||
type: 'object',
|
||||
properties: { confirm: { type: 'boolean', title: 'Yes, delete it' } },
|
||||
required: ['confirm']
|
||||
}
|
||||
});
|
||||
switch (result.action) {
|
||||
case 'accept':
|
||||
if (result.content?.confirm !== true) {
|
||||
return { content: [{ type: 'text', text: 'Box left unchecked - nothing deleted.' }] };
|
||||
}
|
||||
return { content: [{ type: 'text', text: `Deleted ${name}.` }] };
|
||||
case 'decline':
|
||||
return { content: [{ type: 'text', text: 'Declined - nothing deleted.' }] };
|
||||
case 'cancel':
|
||||
return { content: [{ type: 'text', text: 'Dismissed - ask again later.' }] };
|
||||
}
|
||||
}
|
||||
);
|
||||
//#endregion registerTool_elicitActions
|
||||
|
||||
// "Send the end user to a URL" — url mode hands the browser flow to the client.
|
||||
//#region registerTool_elicitUrl
|
||||
server.registerTool(
|
||||
'link-account',
|
||||
{
|
||||
description: 'Link a billing account through a hosted sign-in flow',
|
||||
inputSchema: z.object({ provider: z.string() })
|
||||
},
|
||||
async ({ provider }, ctx) => {
|
||||
const result = await ctx.mcpReq.elicitInput({
|
||||
mode: 'url',
|
||||
message: `Sign in to ${provider} to link your account`,
|
||||
url: `https://billing.example.com/connect/${encodeURIComponent(provider)}`,
|
||||
elicitationId: crypto.randomUUID()
|
||||
});
|
||||
if (result.action !== 'accept') {
|
||||
return { content: [{ type: 'text', text: `Sign-in ${result.action}.` }] };
|
||||
}
|
||||
return { content: [{ type: 'text', text: `Linked ${provider}.` }] };
|
||||
}
|
||||
);
|
||||
//#endregion registerTool_elicitUrl
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Harness (not shown on the page beyond the two regions below). An in-memory
|
||||
// client plays the end user; a real host renders UI instead. Imported
|
||||
// dynamically so the page's lead region stays self-contained.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const { Client, InMemoryTransport } = await import('@modelcontextprotocol/client');
|
||||
|
||||
// The client-side handler the page shows once (the full client story lives in
|
||||
// docs/clients/server-requests.md).
|
||||
//#region Client_elicitationHandler
|
||||
const client = new Client({ name: 'feedback-host', version: '1.0.0' }, { capabilities: { elicitation: { form: {}, url: {} } } });
|
||||
|
||||
client.setRequestHandler('elicitation/create', async request => {
|
||||
if (request.params.mode === 'url') {
|
||||
// Open request.params.url in the user's browser; answer when they finish.
|
||||
return { action: 'accept' };
|
||||
}
|
||||
// Render request.params.requestedSchema as a form; return what the user typed.
|
||||
return { action: 'accept', content: { rating: 5, comment: 'Smooth setup' } };
|
||||
});
|
||||
//#endregion Client_elicitationHandler
|
||||
|
||||
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
||||
await server.connect(serverTransport);
|
||||
await client.connect(clientTransport);
|
||||
|
||||
// "Ask for input with a form" — the accept round trip the page quotes.
|
||||
//#region callTool_collectFeedback
|
||||
const result = await client.callTool({ name: 'collect-feedback', arguments: { topic: 'the new editor' } });
|
||||
console.log(result.content);
|
||||
//#endregion callTool_collectFeedback
|
||||
|
||||
// "Send the end user to a URL" — the handler's url branch accepts.
|
||||
const linked = await client.callTool({ name: 'link-account', arguments: { provider: 'github' } });
|
||||
console.log(linked.content);
|
||||
|
||||
// "Handle every action" — the end user clicks Decline; the harness simulates
|
||||
// that by swapping in a handler that declines every request.
|
||||
client.setRequestHandler('elicitation/create', async () => ({ action: 'decline' }));
|
||||
const declined = await client.callTool({ name: 'delete-dataset', arguments: { name: 'staging-snapshots' } });
|
||||
console.log(declined.content);
|
||||
|
||||
// "Require the elicitation capability" — the same form tool served to a client
|
||||
// that never declared the elicitation capability. elicitInput throws before
|
||||
// anything reaches the wire and the message becomes the tool result.
|
||||
const plainServer = new McpServer({ name: 'feedback', version: '1.0.0' });
|
||||
plainServer.registerTool(
|
||||
'collect-feedback',
|
||||
{ description: 'Ask the user how something went', inputSchema: z.object({ topic: z.string() }) },
|
||||
async ({ topic }, ctx) => {
|
||||
const result = await ctx.mcpReq.elicitInput({
|
||||
mode: 'form',
|
||||
message: `How was ${topic}?`,
|
||||
requestedSchema: { type: 'object', properties: { rating: { type: 'number' } }, required: ['rating'] }
|
||||
});
|
||||
return { content: [{ type: 'text', text: result.action }] };
|
||||
}
|
||||
);
|
||||
const plainClient = new Client({ name: 'no-elicitation-host', version: '1.0.0' });
|
||||
const [plainClientTransport, plainServerTransport] = InMemoryTransport.createLinkedPair();
|
||||
await plainServer.connect(plainServerTransport);
|
||||
await plainClient.connect(plainClientTransport);
|
||||
const failed = await plainClient.callTool({ name: 'collect-feedback', arguments: { topic: 'anything' } });
|
||||
console.log(failed);
|
||||
|
||||
await plainClient.close();
|
||||
await plainServer.close();
|
||||
await client.close();
|
||||
await server.close();
|
||||
132
examples/guides/servers/errors.examples.ts
Normal file
132
examples/guides/servers/errors.examples.ts
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
/**
|
||||
* Companion example for `docs/servers/errors.md`.
|
||||
*
|
||||
* Every `ts` fence on that page is synced from a `//#region` in this file
|
||||
* (`pnpm sync:snippets --check`). The file also runs: the harness below the
|
||||
* regions connects an in-memory client and produces the output the page quotes
|
||||
* verbatim.
|
||||
*
|
||||
* pnpm --filter @modelcontextprotocol/examples typecheck
|
||||
* npx tsx guides/servers/errors.examples.ts # from examples/
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
/* eslint-disable no-console */
|
||||
//#region registerTool_isError
|
||||
import { McpServer, ProtocolError, ProtocolErrorCode, ResourceNotFoundError, ResourceTemplate } from '@modelcontextprotocol/server';
|
||||
import * as z from 'zod/v4';
|
||||
|
||||
const notes = new Map([['welcome', 'Read tools.md first.']]);
|
||||
|
||||
const server = new McpServer({ name: 'notes', version: '1.0.0' });
|
||||
|
||||
server.registerTool(
|
||||
'read-note',
|
||||
{
|
||||
description: 'Read a note by its id',
|
||||
inputSchema: z.object({ id: z.string() })
|
||||
},
|
||||
async ({ id }) => {
|
||||
const note = notes.get(id);
|
||||
if (!note) {
|
||||
return {
|
||||
content: [{ type: 'text', text: `No note with id "${id}". Known ids: ${[...notes.keys()].join(', ')}` }],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
return { content: [{ type: 'text', text: note }] };
|
||||
}
|
||||
);
|
||||
//#endregion registerTool_isError
|
||||
|
||||
//#region registerTool_throw
|
||||
server.registerTool(
|
||||
'delete-note',
|
||||
{
|
||||
description: 'Delete a note by its id',
|
||||
inputSchema: z.object({ id: z.string() })
|
||||
},
|
||||
async ({ id }) => {
|
||||
if (!notes.delete(id)) {
|
||||
throw new Error(`Cannot delete "${id}": no such note`);
|
||||
}
|
||||
return { content: [{ type: 'text', text: `Deleted "${id}"` }] };
|
||||
}
|
||||
);
|
||||
//#endregion registerTool_throw
|
||||
|
||||
//#region registerResource_protocolError
|
||||
server.registerResource(
|
||||
'note',
|
||||
new ResourceTemplate('note://{id}', { list: undefined }),
|
||||
{ description: 'A note by its id' },
|
||||
async (uri, { id }) => {
|
||||
const noteId = String(id);
|
||||
if (!/^[a-z]+$/.test(noteId)) {
|
||||
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Note ids are lowercase letters, got "${noteId}"`);
|
||||
}
|
||||
const note = notes.get(noteId);
|
||||
if (!note) throw new ResourceNotFoundError(uri.href);
|
||||
return { contents: [{ uri: uri.href, text: note }] };
|
||||
}
|
||||
);
|
||||
//#endregion registerResource_protocolError
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Harness (not shown on the page). An in-memory client drives the calls whose
|
||||
// output servers/errors.md quotes verbatim. Any MCP client behaves the same.
|
||||
// Imported dynamically so the page's lead region stays self-contained.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const { Client, InMemoryTransport } = await import('@modelcontextprotocol/client');
|
||||
|
||||
const client = new Client({ name: 'errors-docs-harness', version: '1.0.0' });
|
||||
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
||||
await server.connect(serverTransport);
|
||||
await client.connect(clientTransport);
|
||||
|
||||
// "Return a tool error with isError" — the result the page quotes.
|
||||
//#region callTool_isError
|
||||
const missing = await client.callTool({ name: 'read-note', arguments: { id: 'drafts' } });
|
||||
console.log(missing);
|
||||
//#endregion callTool_isError
|
||||
|
||||
// "Let a thrown exception become a tool error" — the converted result the page quotes.
|
||||
//#region callTool_throw
|
||||
const thrown = await client.callTool({ name: 'delete-note', arguments: { id: 'drafts' } });
|
||||
console.log(thrown);
|
||||
//#endregion callTool_throw
|
||||
|
||||
// "Throw a protocol error" — the JSON-RPC error the page quotes.
|
||||
//#region readResource_protocolError
|
||||
try {
|
||||
await client.readResource({ uri: 'note://42' });
|
||||
} catch (error) {
|
||||
const { code, message } = error as ProtocolError;
|
||||
console.log({ code, message });
|
||||
}
|
||||
//#endregion readResource_protocolError
|
||||
|
||||
// "Use the typed error subclasses" — the structured data the page quotes.
|
||||
//#region readResource_notFound
|
||||
try {
|
||||
await client.readResource({ uri: 'note://archived' });
|
||||
} catch (error) {
|
||||
const { code, message, data } = error as ResourceNotFoundError;
|
||||
console.log({ code, message, data });
|
||||
}
|
||||
//#endregion readResource_notFound
|
||||
|
||||
// Proof for the page's claim that a thrown ProtocolError inside a TOOL handler
|
||||
// is still converted to an `isError: true` result, never a JSON-RPC error.
|
||||
// Throws (non-zero exit) if the claim is false.
|
||||
server.registerTool('always-protocol-error', { description: 'Throws a ProtocolError' }, async () => {
|
||||
throw new ProtocolError(ProtocolErrorCode.InternalError, 'unreachable as a protocol error');
|
||||
});
|
||||
const converted = await client.callTool({ name: 'always-protocol-error', arguments: {} });
|
||||
if (converted.isError !== true) {
|
||||
throw new Error(`errors.md claim failed: a ProtocolError thrown from a tool handler was not converted: ${JSON.stringify(converted)}`);
|
||||
}
|
||||
|
||||
await client.close();
|
||||
await server.close();
|
||||
243
examples/guides/servers/input-required.examples.ts
Normal file
243
examples/guides/servers/input-required.examples.ts
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
/**
|
||||
* Companion example for `docs/servers/input-required.md`.
|
||||
*
|
||||
* Every `ts` fence on that page is synced from a `//#region` in this file
|
||||
* (`pnpm sync:snippets --check`). The file also runs: the harness below the
|
||||
* regions connects an in-memory client with an elicitation handler and
|
||||
* produces the output the page quotes verbatim.
|
||||
*
|
||||
* pnpm --filter @modelcontextprotocol/examples typecheck
|
||||
* npx tsx guides/servers/input-required.examples.ts # from examples/
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
/* eslint-disable no-console */
|
||||
import type { CallToolResult, ElicitResult, InputRequiredResult, ProtocolError } from '@modelcontextprotocol/server';
|
||||
import {
|
||||
acceptedContent,
|
||||
createRequestStateCodec,
|
||||
inputRequired,
|
||||
inputResponse,
|
||||
McpServer,
|
||||
ProtocolErrorCode
|
||||
} from '@modelcontextprotocol/server';
|
||||
import * as z from 'zod/v4';
|
||||
|
||||
// "Protect `requestState` with the codec" — the page shows this block AFTER the
|
||||
// handlers that use `stateCodec`, but module evaluation needs it first.
|
||||
//#region requestState_codec
|
||||
const stateCodec = createRequestStateCodec<{ step: string }>({
|
||||
key: crypto.getRandomValues(new Uint8Array(32)), // >= 32 bytes; share it across instances in a fleet
|
||||
ttlSeconds: 600
|
||||
});
|
||||
|
||||
const server = new McpServer({ name: 'releases', version: '1.0.0' }, { requestState: { verify: stateCodec.verify } });
|
||||
//#endregion requestState_codec
|
||||
|
||||
// "Return `input_required` instead of pushing a request"
|
||||
//#region registerTool_inputRequired
|
||||
server.registerTool(
|
||||
'deploy',
|
||||
{
|
||||
description: 'Deploy after the operator confirms',
|
||||
inputSchema: z.object({ env: z.string() })
|
||||
},
|
||||
async ({ env }, ctx): Promise<CallToolResult | InputRequiredResult> => {
|
||||
const confirmed = acceptedContent<{ confirm: boolean }>(ctx.mcpReq.inputResponses, 'confirm');
|
||||
if (confirmed?.confirm !== true) {
|
||||
return inputRequired({
|
||||
inputRequests: {
|
||||
confirm: inputRequired.elicit({
|
||||
message: `Deploy to ${env}?`,
|
||||
requestedSchema: { type: 'object', properties: { confirm: { type: 'boolean' } }, required: ['confirm'] }
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
return { content: [{ type: 'text', text: `Deployed to ${env}` }] };
|
||||
}
|
||||
);
|
||||
//#endregion registerTool_inputRequired
|
||||
|
||||
// "Read the responses on re-entry" — the schema-aware overload + the declined branch.
|
||||
//#region acceptedContent_schema
|
||||
server.registerTool(
|
||||
'tag-release',
|
||||
{
|
||||
description: 'Tag a release after the operator confirms',
|
||||
inputSchema: z.object({ tag: z.string() })
|
||||
},
|
||||
async ({ tag }, ctx): Promise<CallToolResult | InputRequiredResult> => {
|
||||
const view = inputResponse(ctx.mcpReq.inputResponses, 'confirm');
|
||||
if (view.kind === 'elicit' && view.action !== 'accept') {
|
||||
return { content: [{ type: 'text', text: 'Tagging cancelled by the operator' }], isError: true };
|
||||
}
|
||||
const confirmed = acceptedContent(ctx.mcpReq.inputResponses, 'confirm', z.object({ confirm: z.boolean() }));
|
||||
if (confirmed?.confirm !== true) {
|
||||
return inputRequired({
|
||||
inputRequests: {
|
||||
confirm: inputRequired.elicit({
|
||||
message: `Tag ${tag}?`,
|
||||
requestedSchema: { type: 'object', properties: { confirm: { type: 'boolean' } }, required: ['confirm'] }
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
return { content: [{ type: 'text', text: `Tagged ${tag}` }] };
|
||||
}
|
||||
);
|
||||
//#endregion acceptedContent_schema
|
||||
|
||||
// "Write the handler write-once" — two missing inputs requested in one round.
|
||||
//#region registerTool_writeOnce
|
||||
server.registerTool(
|
||||
'provision',
|
||||
{ description: 'Provision a database', inputSchema: z.object({}) },
|
||||
async (_args, ctx): Promise<CallToolResult | InputRequiredResult> => {
|
||||
const name = acceptedContent(ctx.mcpReq.inputResponses, 'name', z.object({ name: z.string() }));
|
||||
const region = acceptedContent(ctx.mcpReq.inputResponses, 'region', z.object({ region: z.string() }));
|
||||
if (name === undefined || region === undefined) {
|
||||
return inputRequired({
|
||||
inputRequests: {
|
||||
...(name === undefined && {
|
||||
name: inputRequired.elicit({
|
||||
message: 'Database name?',
|
||||
requestedSchema: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] }
|
||||
})
|
||||
}),
|
||||
...(region === undefined && {
|
||||
region: inputRequired.elicit({
|
||||
message: 'Which region?',
|
||||
requestedSchema: { type: 'object', properties: { region: { type: 'string' } }, required: ['region'] }
|
||||
})
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
return { content: [{ type: 'text', text: `Provisioned ${name.name} in ${region.region}` }] };
|
||||
}
|
||||
);
|
||||
//#endregion registerTool_writeOnce
|
||||
|
||||
// "Carry state across rounds with `requestState`" — two sequential rounds.
|
||||
//#region requestState_mint
|
||||
server.registerTool(
|
||||
'wipe-cache',
|
||||
{ description: 'Confirm, then pick a scope, then wipe', inputSchema: z.object({}) },
|
||||
async (_args, ctx): Promise<CallToolResult | InputRequiredResult> => {
|
||||
const state = ctx.mcpReq.requestState<{ step: string }>();
|
||||
|
||||
if (state?.step !== 'confirmed') {
|
||||
const confirmed = acceptedContent<{ confirm: boolean }>(ctx.mcpReq.inputResponses, 'confirm');
|
||||
if (confirmed?.confirm !== true) {
|
||||
return inputRequired({
|
||||
inputRequests: {
|
||||
confirm: inputRequired.elicit({
|
||||
message: 'Really wipe the cache?',
|
||||
requestedSchema: { type: 'object', properties: { confirm: { type: 'boolean' } }, required: ['confirm'] }
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
// Mint only what the response above already proved: the operator confirmed.
|
||||
return inputRequired({
|
||||
inputRequests: {
|
||||
scope: inputRequired.elicit({
|
||||
message: 'Which scope?',
|
||||
requestedSchema: { type: 'object', properties: { scope: { type: 'string' } }, required: ['scope'] }
|
||||
})
|
||||
},
|
||||
requestState: await stateCodec.mint({ step: 'confirmed' })
|
||||
});
|
||||
}
|
||||
|
||||
const scope = acceptedContent<{ scope: string }>(ctx.mcpReq.inputResponses, 'scope');
|
||||
return { content: [{ type: 'text', text: `Wiped ${scope?.scope ?? 'all'}` }] };
|
||||
}
|
||||
);
|
||||
//#endregion requestState_mint
|
||||
|
||||
/** "Pick the embedded request kind" — all four builders in one map (typecheck-only). */
|
||||
export function inputRequired_kinds(): InputRequiredResult {
|
||||
//#region inputRequired_kinds
|
||||
const next = inputRequired({
|
||||
inputRequests: {
|
||||
confirm: inputRequired.elicit({
|
||||
message: 'Continue?',
|
||||
requestedSchema: { type: 'object', properties: { ok: { type: 'boolean' } }, required: ['ok'] }
|
||||
}),
|
||||
signin: inputRequired.elicitUrl({ message: 'Sign in to continue', url: 'https://example.com/auth' }),
|
||||
summary: inputRequired.createMessage({
|
||||
messages: [{ role: 'user', content: { type: 'text', text: 'Summarize the diff' } }],
|
||||
maxTokens: 200
|
||||
}),
|
||||
roots: inputRequired.listRoots()
|
||||
}
|
||||
});
|
||||
//#endregion inputRequired_kinds
|
||||
return next;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Harness (not shown on the page). An in-memory client with an elicitation
|
||||
// handler drives the calls whose output servers/input-required.md quotes
|
||||
// verbatim. Any MCP client behaves the same; the SDK's legacy shim fulfils
|
||||
// these `input_required` returns over the 2025-era linked pair.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const { Client, InMemoryTransport } = await import('@modelcontextprotocol/client');
|
||||
|
||||
const client = new Client({ name: 'input-required-docs-harness', version: '1.0.0' }, { capabilities: { elicitation: { form: {} } } });
|
||||
|
||||
const answers: Record<string, Record<string, string | boolean>> = {
|
||||
'Deploy to prod?': { confirm: true },
|
||||
'Database name?': { name: 'analytics' },
|
||||
'Which region?': { region: 'eu-west-1' },
|
||||
'Really wipe the cache?': { confirm: true },
|
||||
'Which scope?': { scope: 'sessions' }
|
||||
};
|
||||
|
||||
const acceptHandler = async (request: { params: { message: string } }): Promise<ElicitResult> => {
|
||||
console.log('[client] elicitation/create →', request.params.message);
|
||||
const content = answers[request.params.message];
|
||||
return content === undefined ? { action: 'decline' } : { action: 'accept', content };
|
||||
};
|
||||
client.setRequestHandler('elicitation/create', acceptHandler);
|
||||
|
||||
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
||||
await server.connect(serverTransport);
|
||||
await client.connect(clientTransport);
|
||||
|
||||
// "Return `input_required` instead of pushing a request" — the round trip the page quotes.
|
||||
console.log(await client.callTool({ name: 'deploy', arguments: { env: 'prod' } }));
|
||||
|
||||
// "Read the responses on re-entry" — the declined branch the page quotes.
|
||||
client.setRequestHandler('elicitation/create', async (request): Promise<ElicitResult> => {
|
||||
console.log('[client] elicitation/create →', request.params.message);
|
||||
return { action: 'decline' };
|
||||
});
|
||||
console.log(await client.callTool({ name: 'tag-release', arguments: { tag: 'v2.1.0' } }));
|
||||
client.setRequestHandler('elicitation/create', acceptHandler);
|
||||
|
||||
// "Write the handler write-once" — both missing inputs requested in one round.
|
||||
console.log(await client.callTool({ name: 'provision', arguments: {} }));
|
||||
|
||||
// "Carry state across rounds with `requestState`" — two sequential rounds.
|
||||
console.log(await client.callTool({ name: 'wipe-cache', arguments: {} }));
|
||||
|
||||
// "Protect `requestState` with the codec" — tampered state answers -32602.
|
||||
// Matched by `code`, not `instanceof` (see docs/servers/errors.md): `instanceof`
|
||||
// fails across separately bundled copies of the SDK.
|
||||
try {
|
||||
await client.request({
|
||||
method: 'tools/call',
|
||||
params: { name: 'wipe-cache', arguments: {}, requestState: 'tampered' }
|
||||
});
|
||||
} catch (error) {
|
||||
const { code, message } = error as ProtocolError;
|
||||
if (code !== ProtocolErrorCode.InvalidParams) throw error;
|
||||
console.log(`${code} ${message}`);
|
||||
}
|
||||
|
||||
await client.close();
|
||||
await server.close();
|
||||
|
|
@ -0,0 +1,168 @@
|
|||
/**
|
||||
* Companion example for `docs/servers/logging-progress-cancellation.md`.
|
||||
*
|
||||
* Every `ts` fence on that page is synced from a `//#region` in this file
|
||||
* (`pnpm sync:snippets --check`). The file also runs: the harness below the
|
||||
* server regions connects an in-memory client and produces the output the
|
||||
* page quotes verbatim.
|
||||
*
|
||||
* pnpm --filter @modelcontextprotocol/examples typecheck
|
||||
* npx tsx guides/servers/logging-progress-cancellation.examples.ts # from examples/
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
/* eslint-disable no-console */
|
||||
import { McpServer } from '@modelcontextprotocol/server';
|
||||
import * as z from 'zod/v4';
|
||||
|
||||
// "Log to the client" — the logging capability is declared at construction.
|
||||
//#region logging_capability
|
||||
const server = new McpServer({ name: 'file-processor', version: '1.0.0' }, { capabilities: { logging: {} } });
|
||||
//#endregion logging_capability
|
||||
|
||||
// "Report progress from a handler" — the page's lead block.
|
||||
//#region registerTool_progress
|
||||
server.registerTool(
|
||||
'process-files',
|
||||
{
|
||||
description: 'Process files with progress updates',
|
||||
inputSchema: z.object({ files: z.array(z.string()) })
|
||||
},
|
||||
async ({ files }, ctx) => {
|
||||
const progressToken = ctx.mcpReq._meta?.progressToken;
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
// ... process files[i] ...
|
||||
|
||||
if (progressToken !== undefined) {
|
||||
await ctx.mcpReq.notify({
|
||||
method: 'notifications/progress',
|
||||
params: { progressToken, progress: i + 1, total: files.length, message: `Processed ${files[i]}` }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { content: [{ type: 'text', text: `Processed ${files.length} files` }] };
|
||||
}
|
||||
);
|
||||
//#endregion registerTool_progress
|
||||
|
||||
// "Log to the client" — `ctx.mcpReq.log(level, data)` inside a handler.
|
||||
//#region registerTool_logging
|
||||
server.registerTool(
|
||||
'validate-records',
|
||||
{
|
||||
description: 'Validate records before import',
|
||||
inputSchema: z.object({ records: z.array(z.string()) })
|
||||
},
|
||||
async ({ records }, ctx) => {
|
||||
await ctx.mcpReq.log('info', `Validating ${records.length} records`);
|
||||
const invalid = records.filter(record => !record.endsWith('.csv'));
|
||||
if (invalid.length > 0) {
|
||||
await ctx.mcpReq.log('warning', { invalid });
|
||||
}
|
||||
return { content: [{ type: 'text', text: `${records.length - invalid.length} of ${records.length} records are valid` }] };
|
||||
}
|
||||
);
|
||||
//#endregion registerTool_logging
|
||||
|
||||
// "Stop work when the request is cancelled" — check `ctx.mcpReq.signal`.
|
||||
//#region registerTool_abort
|
||||
server.registerTool(
|
||||
'scan-archive',
|
||||
{
|
||||
description: 'Scan every page of the archive',
|
||||
inputSchema: z.object({ pages: z.number().int() })
|
||||
},
|
||||
async ({ pages }, ctx) => {
|
||||
let scanned = 0;
|
||||
for (let page = 0; page < pages; page++) {
|
||||
if (ctx.mcpReq.signal.aborted) {
|
||||
console.error(`Stopped after ${scanned} of ${pages} pages: ${ctx.mcpReq.signal.reason}`);
|
||||
break;
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 100)); // ... scan one page ...
|
||||
scanned++;
|
||||
}
|
||||
return { content: [{ type: 'text', text: `Scanned ${scanned} pages` }] };
|
||||
}
|
||||
);
|
||||
//#endregion registerTool_abort
|
||||
|
||||
// "Pass the signal to your own I/O" — registered for the page, never called by
|
||||
// the harness (it would hit the network).
|
||||
//#region registerTool_forwardSignal
|
||||
const SOURCE_URLS = {
|
||||
readme: 'https://example.com/sources/readme.md',
|
||||
changelog: 'https://example.com/sources/changelog.md'
|
||||
};
|
||||
|
||||
server.registerTool(
|
||||
'fetch-source',
|
||||
{
|
||||
description: 'Download one of the known source files',
|
||||
inputSchema: z.object({ source: z.enum(['readme', 'changelog']) })
|
||||
},
|
||||
async ({ source }, ctx) => {
|
||||
const response = await fetch(SOURCE_URLS[source], { signal: ctx.mcpReq.signal });
|
||||
return { content: [{ type: 'text', text: await response.text() }] };
|
||||
}
|
||||
);
|
||||
//#endregion registerTool_forwardSignal
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Harness (not shown on the page). An in-memory client drives the calls whose
|
||||
// output the page quotes verbatim. Any MCP client behaves the same.
|
||||
// Imported dynamically so the page's server regions stay self-contained.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const { Client, InMemoryTransport } = await import('@modelcontextprotocol/client');
|
||||
|
||||
const client = new Client({ name: 'lpc-docs-harness', version: '1.0.0' });
|
||||
|
||||
// "Log to the client" — the client surfaces `notifications/message`.
|
||||
//#region setNotificationHandler_message
|
||||
client.setNotificationHandler('notifications/message', notification => {
|
||||
console.log(notification.params.level, notification.params.data);
|
||||
});
|
||||
//#endregion setNotificationHandler_message
|
||||
|
||||
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
||||
await server.connect(serverTransport);
|
||||
await client.connect(clientTransport);
|
||||
|
||||
// "Report progress from a handler" — `onprogress` opts the call in.
|
||||
//#region callTool_onprogress
|
||||
const result = await client.callTool(
|
||||
{ name: 'process-files', arguments: { files: ['a.csv', 'b.csv', 'c.csv'] } },
|
||||
{ onprogress: update => console.log(update) }
|
||||
);
|
||||
console.log(result.content);
|
||||
//#endregion callTool_onprogress
|
||||
|
||||
// "Skip progress when the client did not ask" — same call, no `onprogress`.
|
||||
//#region callTool_noProgress
|
||||
const quiet = await client.callTool({ name: 'process-files', arguments: { files: ['d.csv', 'e.csv'] } });
|
||||
console.log(quiet.content);
|
||||
//#endregion callTool_noProgress
|
||||
|
||||
// "Log to the client" — both `log` calls land before the result.
|
||||
const validated = await client.callTool({ name: 'validate-records', arguments: { records: ['a.csv', 'b.txt'] } });
|
||||
console.log(validated.content);
|
||||
|
||||
// "Stop work when the request is cancelled".
|
||||
//#region callTool_abort
|
||||
const controller = new AbortController();
|
||||
const scan = client.callTool({ name: 'scan-archive', arguments: { pages: 40 } }, { signal: controller.signal });
|
||||
|
||||
// the end user clicks Stop while the scan runs
|
||||
setTimeout(() => controller.abort('the end user clicked Stop'), 5);
|
||||
|
||||
await scan.catch(error => console.log(String(error)));
|
||||
//#endregion callTool_abort
|
||||
|
||||
// Give the cancelled handler time to observe the abort and stop.
|
||||
await new Promise(resolve => setTimeout(resolve, 250));
|
||||
|
||||
await client.close();
|
||||
await server.close();
|
||||
119
examples/guides/servers/notifications.examples.ts
Normal file
119
examples/guides/servers/notifications.examples.ts
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
/**
|
||||
* Companion example for `docs/servers/notifications.md`.
|
||||
*
|
||||
* Every `ts` fence on that page is synced from a `//#region` in this file
|
||||
* (`pnpm sync:snippets --check`). The file also runs: the harness below the
|
||||
* lead region connects an in-memory client whose notification log the page
|
||||
* quotes verbatim.
|
||||
*
|
||||
* pnpm --filter @modelcontextprotocol/examples typecheck
|
||||
* npx tsx guides/servers/notifications.examples.ts # from examples/
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
/* eslint-disable no-console */
|
||||
//#region notifications_server
|
||||
import { McpServer } from '@modelcontextprotocol/server';
|
||||
|
||||
const jobs = ['nightly-backup'];
|
||||
|
||||
const server = new McpServer({ name: 'jobs', version: '1.0.0' });
|
||||
|
||||
server.registerTool('list-jobs', { description: 'List the configured jobs' }, async () => ({
|
||||
content: [{ type: 'text', text: jobs.join('\n') }]
|
||||
}));
|
||||
//#endregion notifications_server
|
||||
|
||||
// Symbols the later sections use, imported here so the page's lead block shows
|
||||
// only what it teaches. A real server imports everything in one statement.
|
||||
import { createMcpHandler, InMemoryServerEventBus, Server } from '@modelcontextprotocol/server';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Harness (not shown on the page). An in-memory client connected to the server
|
||||
// above logs every list-changed notification it receives — the output the
|
||||
// page quotes verbatim. Any MCP client behaves the same.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const { Client, InMemoryTransport } = await import('@modelcontextprotocol/client');
|
||||
|
||||
const client = new Client({ name: 'notifications-docs-harness', version: '1.0.0' });
|
||||
for (const method of [
|
||||
'notifications/tools/list_changed',
|
||||
'notifications/prompts/list_changed',
|
||||
'notifications/resources/list_changed'
|
||||
] as const) {
|
||||
client.setNotificationHandler(method, async () => console.log(method));
|
||||
}
|
||||
|
||||
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
||||
await server.connect(serverTransport);
|
||||
await client.connect(clientTransport);
|
||||
|
||||
// "Send a list-changed notification" — the explicit push the page quotes.
|
||||
//#region sendToolListChanged_basic
|
||||
server.sendToolListChanged();
|
||||
//#endregion sendToolListChanged_basic
|
||||
|
||||
// A round-trip so the notification above flushes before the next region runs;
|
||||
// the page quotes its single output line on its own.
|
||||
await client.listTools();
|
||||
|
||||
// "Let registration changes notify for you" — three more sends, none explicit.
|
||||
//#region registeredTool_update
|
||||
const report = server.registerTool('run-report', { description: 'Run the weekly report' }, async () => ({
|
||||
content: [{ type: 'text', text: 'report queued' }]
|
||||
}));
|
||||
|
||||
report.update({ description: 'Run the weekly report and email it' });
|
||||
report.disable();
|
||||
//#endregion registeredTool_update
|
||||
|
||||
await client.listTools();
|
||||
await client.close();
|
||||
await server.close();
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// "Advertise the listChanged capability" — the low-level Server needs it
|
||||
// declared up front. Constructed only; nothing on the page quotes output here.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
//#region Server_listChanged
|
||||
const lowLevel = new Server({ name: 'jobs', version: '1.0.0' }, { capabilities: { tools: { listChanged: true } } });
|
||||
//#endregion Server_listChanged
|
||||
void lowLevel;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// "Publish a resource update through the handler". The factory the page refers
|
||||
// to: a fresh McpServer per request, advertising `resources.subscribe` so the
|
||||
// entry honors per-resource subscriptions.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function buildJobsServer(): McpServer {
|
||||
const app = new McpServer({ name: 'jobs', version: '1.0.0' }, { capabilities: { resources: { subscribe: true } } });
|
||||
app.registerResource('config', 'config://app', { mimeType: 'application/json' }, async uri => ({
|
||||
contents: [{ uri: uri.href, text: JSON.stringify({ jobs }) }]
|
||||
}));
|
||||
return app;
|
||||
}
|
||||
|
||||
// Runs as written: with no subscription stream open the publish is a no-op,
|
||||
// and `createMcpHandler` binds no port.
|
||||
//#region handler_notifyResourceUpdated
|
||||
const handler = createMcpHandler(() => buildJobsServer());
|
||||
|
||||
// After config://app changes:
|
||||
handler.notify.resourceUpdated('config://app');
|
||||
//#endregion handler_notifyResourceUpdated
|
||||
|
||||
await handler.close();
|
||||
|
||||
// "Pick an event bus for multi-process deployments" — typecheck-only wrapper.
|
||||
function createMcpHandler_bus() {
|
||||
//#region createMcpHandler_bus
|
||||
const bus = new InMemoryServerEventBus();
|
||||
|
||||
const shared = createMcpHandler(() => buildJobsServer(), { bus });
|
||||
//#endregion createMcpHandler_bus
|
||||
return shared;
|
||||
}
|
||||
void createMcpHandler_bus;
|
||||
163
examples/guides/servers/prompts.examples.ts
Normal file
163
examples/guides/servers/prompts.examples.ts
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
/**
|
||||
* Companion example for `docs/servers/prompts.md`.
|
||||
*
|
||||
* Every `ts` fence on that page is synced from a `//#region` in this file
|
||||
* (`pnpm sync:snippets --check`). The file also runs: the harness below the
|
||||
* regions connects an in-memory client and produces the output the page quotes
|
||||
* verbatim.
|
||||
*
|
||||
* pnpm --filter @modelcontextprotocol/examples typecheck
|
||||
* npx tsx guides/servers/prompts.examples.ts # from examples/
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
/* eslint-disable no-console */
|
||||
//#region registerPrompt_review
|
||||
import { McpServer } from '@modelcontextprotocol/server';
|
||||
import * as z from 'zod/v4';
|
||||
|
||||
const server = new McpServer({ name: 'review', version: '1.0.0' });
|
||||
|
||||
server.registerPrompt(
|
||||
'review-code',
|
||||
{
|
||||
title: 'Code Review',
|
||||
description: 'Review code for best practices and potential issues',
|
||||
argsSchema: z.object({
|
||||
code: z.string().describe('The code to review')
|
||||
})
|
||||
},
|
||||
({ code }) => ({
|
||||
messages: [
|
||||
{
|
||||
role: 'user' as const,
|
||||
content: { type: 'text' as const, text: `Review this code:\n\n${code}` }
|
||||
}
|
||||
]
|
||||
})
|
||||
);
|
||||
//#endregion registerPrompt_review
|
||||
|
||||
//#region registerPrompt_messages
|
||||
server.registerPrompt(
|
||||
'explain-error',
|
||||
{
|
||||
description: 'Explain a compiler error and suggest the smallest fix',
|
||||
argsSchema: z.object({ error: z.string() })
|
||||
},
|
||||
({ error }) => ({
|
||||
messages: [
|
||||
{
|
||||
role: 'user' as const,
|
||||
content: { type: 'text' as const, text: `Explain this compiler error:\n\n${error}` }
|
||||
},
|
||||
{
|
||||
role: 'assistant' as const,
|
||||
content: { type: 'text' as const, text: 'The one-line cause:' }
|
||||
}
|
||||
]
|
||||
})
|
||||
);
|
||||
//#endregion registerPrompt_messages
|
||||
|
||||
//#region registerPrompt_embedResource
|
||||
const styleGuide = '- Prefer const over let.\n- No single-letter identifiers.';
|
||||
|
||||
server.registerResource('style-guide', 'doc://style-guide', { mimeType: 'text/markdown' }, async uri => ({
|
||||
contents: [{ uri: uri.href, mimeType: 'text/markdown', text: styleGuide }]
|
||||
}));
|
||||
|
||||
server.registerPrompt(
|
||||
'review-against-style',
|
||||
{
|
||||
description: 'Review code against the team style guide',
|
||||
argsSchema: z.object({ code: z.string() })
|
||||
},
|
||||
({ code }) => ({
|
||||
messages: [
|
||||
{
|
||||
role: 'user' as const,
|
||||
content: {
|
||||
type: 'resource' as const,
|
||||
resource: { uri: 'doc://style-guide', mimeType: 'text/markdown', text: styleGuide }
|
||||
}
|
||||
},
|
||||
{
|
||||
role: 'user' as const,
|
||||
content: { type: 'text' as const, text: `Review this code against the style guide:\n\n${code}` }
|
||||
}
|
||||
]
|
||||
})
|
||||
);
|
||||
//#endregion registerPrompt_embedResource
|
||||
|
||||
//#region registerPrompt_completable
|
||||
import { completable } from '@modelcontextprotocol/server';
|
||||
|
||||
server.registerPrompt(
|
||||
'translate',
|
||||
{
|
||||
description: 'Translate a snippet into another language',
|
||||
argsSchema: z.object({
|
||||
language: completable(z.string(), value =>
|
||||
['typescript', 'python', 'rust', 'go'].filter(language => language.startsWith(value))
|
||||
),
|
||||
code: z.string()
|
||||
})
|
||||
},
|
||||
({ language, code }) => ({
|
||||
messages: [{ role: 'user' as const, content: { type: 'text' as const, text: `Translate to ${language}:\n\n${code}` } }]
|
||||
})
|
||||
);
|
||||
//#endregion registerPrompt_completable
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Harness (not shown on the page). An in-memory client drives the calls whose
|
||||
// output servers/prompts.md quotes verbatim. Any MCP client behaves the same.
|
||||
// Imported dynamically so the page's lead region stays self-contained.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const { Client, InMemoryTransport } = await import('@modelcontextprotocol/client');
|
||||
|
||||
const client = new Client({ name: 'prompts-docs-harness', version: '1.0.0' });
|
||||
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
||||
await server.connect(serverTransport);
|
||||
await client.connect(clientTransport);
|
||||
|
||||
// "Register a prompt" — the prose claims `prompts/list` advertises `review-code`
|
||||
// with one required `code` argument carrying the `.describe()` string. Throws
|
||||
// (non-zero exit) if the claim is false.
|
||||
const { prompts } = await client.listPrompts();
|
||||
const reviewPrompt = prompts.find(prompt => prompt.name === 'review-code');
|
||||
const codeArgument = reviewPrompt?.arguments?.find(argument => argument.name === 'code');
|
||||
if (codeArgument?.required !== true || codeArgument.description !== 'The code to review') {
|
||||
throw new Error(`prompts.md claim failed: review-code argument is ${JSON.stringify(reviewPrompt?.arguments)}`);
|
||||
}
|
||||
|
||||
// "Register a prompt" — the filled-in messages the page quotes.
|
||||
//#region getPrompt_review
|
||||
const result = await client.getPrompt({ name: 'review-code', arguments: { code: 'let x = 1' } });
|
||||
console.log(result.messages);
|
||||
//#endregion getPrompt_review
|
||||
|
||||
// "Validate the arguments with the schema" — the rejection the page quotes.
|
||||
//#region getPrompt_invalid
|
||||
import type { ProtocolError } from '@modelcontextprotocol/client';
|
||||
|
||||
try {
|
||||
await client.getPrompt({ name: 'review-code', arguments: {} });
|
||||
} catch (error) {
|
||||
const { code, message } = error as ProtocolError;
|
||||
console.log(code, message);
|
||||
}
|
||||
//#endregion getPrompt_invalid
|
||||
|
||||
// "Embed a resource in a message" — the embedded-resource message the page quotes.
|
||||
const review = await client.getPrompt({
|
||||
name: 'review-against-style',
|
||||
arguments: { code: 'let n = 1' }
|
||||
});
|
||||
console.log(review.messages[0]);
|
||||
|
||||
await client.close();
|
||||
await server.close();
|
||||
157
examples/guides/servers/resources.examples.ts
Normal file
157
examples/guides/servers/resources.examples.ts
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
/**
|
||||
* Companion example for `docs/servers/resources.md`.
|
||||
*
|
||||
* Every `ts` fence on that page is synced from a `//#region` in this file
|
||||
* (`pnpm sync:snippets --check`). The file also runs: the harness below the
|
||||
* regions connects an in-memory client and produces the output the page quotes
|
||||
* verbatim.
|
||||
*
|
||||
* pnpm --filter @modelcontextprotocol/examples typecheck
|
||||
* npx tsx guides/servers/resources.examples.ts # from examples/
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
/* eslint-disable no-console */
|
||||
//#region registerResource_static
|
||||
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/server';
|
||||
|
||||
const server = new McpServer({ name: 'workspace', version: '1.0.0' });
|
||||
|
||||
server.registerResource(
|
||||
'config',
|
||||
'config://app',
|
||||
{
|
||||
title: 'Application Config',
|
||||
description: 'Application configuration data',
|
||||
mimeType: 'text/plain'
|
||||
},
|
||||
async uri => ({
|
||||
contents: [{ uri: uri.href, text: 'log_level=info\nregion=eu-west-1' }]
|
||||
})
|
||||
);
|
||||
//#endregion registerResource_static
|
||||
|
||||
//#region registerResource_report
|
||||
// A 1x1 PNG; a production server reads these bytes from disk or object storage.
|
||||
const chartPng = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVR4nGNgAAIAAAUAAXpeqz8AAAAASUVORK5CYII=';
|
||||
|
||||
server.registerResource(
|
||||
'report',
|
||||
'report://latest',
|
||||
{
|
||||
title: 'Latest usage report',
|
||||
description: 'Weekly usage summary with a rendered chart',
|
||||
mimeType: 'text/markdown'
|
||||
},
|
||||
async uri => ({
|
||||
contents: [
|
||||
{ uri: uri.href, mimeType: 'text/markdown', text: 'Active installs grew 12% week over week.' },
|
||||
{ uri: uri.href, mimeType: 'image/png', blob: chartPng }
|
||||
]
|
||||
})
|
||||
);
|
||||
//#endregion registerResource_report
|
||||
|
||||
//#region registerResource_template
|
||||
server.registerResource(
|
||||
'user-profile',
|
||||
new ResourceTemplate('users://{userId}/profile', { list: undefined }),
|
||||
{
|
||||
title: 'User Profile',
|
||||
description: 'Profile data for one user',
|
||||
mimeType: 'application/json'
|
||||
},
|
||||
async (uri, { userId }) => ({
|
||||
contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify({ userId, plan: 'pro' }) }]
|
||||
})
|
||||
);
|
||||
//#endregion registerResource_template
|
||||
|
||||
//#region registerResource_list
|
||||
server.registerResource(
|
||||
'team-roster',
|
||||
new ResourceTemplate('teams://{teamId}/roster', {
|
||||
list: async () => ({
|
||||
resources: [
|
||||
{ uri: 'teams://core/roster', name: 'Core team roster' },
|
||||
{ uri: 'teams://growth/roster', name: 'Growth team roster' }
|
||||
]
|
||||
})
|
||||
}),
|
||||
{
|
||||
description: 'Members of one team',
|
||||
mimeType: 'text/plain'
|
||||
},
|
||||
async (uri, { teamId }) => ({
|
||||
contents: [{ uri: uri.href, text: `Members of team ${teamId}` }]
|
||||
})
|
||||
);
|
||||
//#endregion registerResource_list
|
||||
|
||||
//#region registerResource_file
|
||||
import { readFile, realpath } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
const DOCS_ROOT = path.resolve('./docs');
|
||||
|
||||
server.registerResource(
|
||||
'doc',
|
||||
new ResourceTemplate('docs://{file}', { list: undefined }),
|
||||
{
|
||||
description: 'A markdown page from the docs directory',
|
||||
mimeType: 'text/markdown'
|
||||
},
|
||||
async (uri, { file }) => {
|
||||
const requested = await realpath(path.join(DOCS_ROOT, String(file)));
|
||||
if (!requested.startsWith(DOCS_ROOT + path.sep)) {
|
||||
throw new Error(`${uri.href} resolves outside the docs root`);
|
||||
}
|
||||
return { contents: [{ uri: uri.href, text: await readFile(requested, 'utf8') }] };
|
||||
}
|
||||
);
|
||||
//#endregion registerResource_file
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Harness (not shown on the page). An in-memory client drives the calls whose
|
||||
// output servers/resources.md quotes verbatim. Any MCP client behaves the same.
|
||||
// Imported dynamically so the page's lead region stays self-contained.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const { Client, InMemoryTransport } = await import('@modelcontextprotocol/client');
|
||||
|
||||
const client = new Client({ name: 'resources-docs-harness', version: '1.0.0' });
|
||||
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
||||
await server.connect(serverTransport);
|
||||
await client.connect(clientTransport);
|
||||
|
||||
// "Return the contents from the read callback" — the two-item result the page quotes.
|
||||
//#region readResource_report
|
||||
const { contents } = await client.readResource({ uri: 'report://latest' });
|
||||
console.log(contents);
|
||||
//#endregion readResource_report
|
||||
|
||||
// "Add a resource template" — the parameterized read the page quotes.
|
||||
//#region readResource_template
|
||||
const profile = await client.readResource({ uri: 'users://7/profile' });
|
||||
console.log(profile.contents);
|
||||
//#endregion readResource_template
|
||||
|
||||
// "List the template's instances" — the merged list the page quotes. Must contain
|
||||
// the static URIs and the team:// instances, and no users:// URI.
|
||||
//#region listResources
|
||||
const { resources } = await client.listResources();
|
||||
console.log(resources.map(resource => resource.uri));
|
||||
//#endregion listResources
|
||||
|
||||
const uris = resources.map(resource => resource.uri);
|
||||
if (uris.some(uri => uri.startsWith('users://')) || !uris.includes('teams://core/roster')) {
|
||||
throw new Error(`resources.md list claim failed: ${JSON.stringify(uris)}`);
|
||||
}
|
||||
|
||||
// "Tell clients when a resource changes" — explicit list_changed.
|
||||
//#region sendResourceListChanged
|
||||
server.sendResourceListChanged();
|
||||
//#endregion sendResourceListChanged
|
||||
|
||||
await client.close();
|
||||
await server.close();
|
||||
89
examples/guides/servers/sampling.examples.ts
Normal file
89
examples/guides/servers/sampling.examples.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
/**
|
||||
* Companion example for `docs/servers/sampling.md`.
|
||||
*
|
||||
* The `ts` fence on that page is synced from the `//#region` in this file
|
||||
* (`pnpm sync:snippets --check`). The file also runs: the harness below the
|
||||
* region connects in-memory clients whose sampling handlers stand in for a
|
||||
* host LLM, and produces the output the page quotes verbatim.
|
||||
*
|
||||
* pnpm --filter @modelcontextprotocol/examples typecheck
|
||||
* npx tsx guides/servers/sampling.examples.ts # from examples/
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
/* eslint-disable no-console */
|
||||
import { Client, InMemoryTransport } from '@modelcontextprotocol/client';
|
||||
import { McpServer } from '@modelcontextprotocol/server';
|
||||
import * as z from 'zod/v4';
|
||||
|
||||
// The page's one code block. Wrapped in a function so the harness can stand up
|
||||
// two independent server instances (one per client) without duplicating it.
|
||||
function registerTool_sampling(server: McpServer) {
|
||||
//#region registerTool_sampling
|
||||
server.registerTool(
|
||||
'summarize',
|
||||
{
|
||||
description: 'Summarize text using the client LLM',
|
||||
inputSchema: z.object({ text: z.string() })
|
||||
},
|
||||
async ({ text }, ctx) => {
|
||||
const response = await ctx.mcpReq.requestSampling({
|
||||
messages: [{ role: 'user', content: { type: 'text', text: `Summarize in one sentence:\n\n${text}` } }],
|
||||
maxTokens: 500
|
||||
});
|
||||
return { content: [{ type: 'text', text: `Model (${response.model}): ${JSON.stringify(response.content)}` }] };
|
||||
}
|
||||
);
|
||||
//#endregion registerTool_sampling
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Harness (not shown on the page). An in-memory client declares the sampling
|
||||
// capability and answers `sampling/createMessage` the way a host would — by
|
||||
// running the prompt through its model. Here the "model" is canned so the run
|
||||
// is deterministic; any MCP client behaves the same over stdio or HTTP.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const server = new McpServer({ name: 'summarizer', version: '1.0.0' });
|
||||
registerTool_sampling(server);
|
||||
|
||||
const client = new Client({ name: 'sampling-docs-harness', version: '1.0.0' }, { capabilities: { sampling: {} } });
|
||||
|
||||
client.setRequestHandler('sampling/createMessage', async () => {
|
||||
return {
|
||||
model: 'host-model',
|
||||
role: 'assistant' as const,
|
||||
content: { type: 'text' as const, text: 'Sampling lets a tool ask the client for a completion.' }
|
||||
};
|
||||
});
|
||||
|
||||
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
||||
await server.connect(serverTransport);
|
||||
await client.connect(clientTransport);
|
||||
|
||||
// "Read the model's reply" — the result the page quotes verbatim.
|
||||
const result = await client.callTool({
|
||||
name: 'summarize',
|
||||
arguments: { text: 'Sampling is a server-to-client request for an LLM completion...' }
|
||||
});
|
||||
console.log(result.content);
|
||||
|
||||
await client.close();
|
||||
await server.close();
|
||||
|
||||
// "Require the sampling capability" — a strict server and a client that never
|
||||
// declared `sampling`. The SDK rejects the request before it reaches the wire;
|
||||
// the page quotes the resulting tool error verbatim.
|
||||
const bareServer = new McpServer({ name: 'summarizer', version: '1.0.0' }, { enforceStrictCapabilities: true });
|
||||
registerTool_sampling(bareServer);
|
||||
|
||||
const bare = new Client({ name: 'no-sampling-harness', version: '1.0.0' });
|
||||
const [bareClientTransport, bareServerTransport] = InMemoryTransport.createLinkedPair();
|
||||
await bareServer.connect(bareServerTransport);
|
||||
await bare.connect(bareClientTransport);
|
||||
|
||||
const rejected = await bare.callTool({ name: 'summarize', arguments: { text: 'anything' } });
|
||||
console.log(rejected);
|
||||
|
||||
await bare.close();
|
||||
await bareServer.close();
|
||||
119
examples/guides/servers/tools.examples.ts
Normal file
119
examples/guides/servers/tools.examples.ts
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
/**
|
||||
* Companion example for `docs/servers/tools.md`.
|
||||
*
|
||||
* Every `ts` fence on that page is synced from a `//#region` in this file
|
||||
* (`pnpm sync:snippets --check`). The file also runs: the harness below the
|
||||
* regions connects an in-memory client and produces the output the page quotes
|
||||
* verbatim.
|
||||
*
|
||||
* pnpm --filter @modelcontextprotocol/examples typecheck
|
||||
* npx tsx guides/servers/tools.examples.ts # from examples/
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
/* eslint-disable no-console */
|
||||
//#region registerTool_search
|
||||
import { McpServer } from '@modelcontextprotocol/server';
|
||||
import * as z from 'zod/v4';
|
||||
|
||||
const catalog = [
|
||||
{ name: 'Espresso cup', price: 12 },
|
||||
{ name: 'Travel mug', price: 24 },
|
||||
{ name: 'Mug rack', price: 36 }
|
||||
];
|
||||
|
||||
const server = new McpServer({ name: 'catalog', version: '1.0.0' });
|
||||
|
||||
server.registerTool(
|
||||
'search',
|
||||
{
|
||||
description: 'Search the product catalog',
|
||||
inputSchema: z.object({
|
||||
query: z.string().describe('Substring to match against product names'),
|
||||
limit: z.number().int().max(50).optional()
|
||||
})
|
||||
},
|
||||
async ({ query, limit }) => {
|
||||
const hits = catalog.filter(product => product.name.toLowerCase().includes(query.toLowerCase()));
|
||||
const names = hits.slice(0, limit ?? 10).map(product => product.name);
|
||||
return { content: [{ type: 'text', text: names.join('\n') }] };
|
||||
}
|
||||
);
|
||||
//#endregion registerTool_search
|
||||
|
||||
//#region registerTool_structured
|
||||
server.registerTool(
|
||||
'product-details',
|
||||
{
|
||||
description: 'Look up one product by its exact name',
|
||||
inputSchema: z.object({ name: z.string() }),
|
||||
outputSchema: z.object({ name: z.string(), price: z.number() })
|
||||
},
|
||||
async ({ name }) => {
|
||||
const product = catalog.find(candidate => candidate.name === name);
|
||||
if (!product) throw new Error(`No product named ${name}`);
|
||||
const output = { name: product.name, price: product.price };
|
||||
return {
|
||||
content: [{ type: 'text', text: JSON.stringify(output) }],
|
||||
structuredContent: output
|
||||
};
|
||||
}
|
||||
);
|
||||
//#endregion registerTool_structured
|
||||
|
||||
//#region registerTool_annotations
|
||||
server.registerTool(
|
||||
'clear-catalog',
|
||||
{
|
||||
title: 'Clear the catalog',
|
||||
description: 'Remove every product from the catalog',
|
||||
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true }
|
||||
},
|
||||
async () => {
|
||||
catalog.length = 0;
|
||||
return { content: [{ type: 'text', text: 'Catalog cleared' }] };
|
||||
}
|
||||
);
|
||||
//#endregion registerTool_annotations
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Harness (not shown on the page). An in-memory client drives the calls whose
|
||||
// output servers/tools.md quotes verbatim. Any MCP client behaves the same.
|
||||
// Imported dynamically so the page's lead region stays self-contained.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const { Client, InMemoryTransport } = await import('@modelcontextprotocol/client');
|
||||
|
||||
const client = new Client({ name: 'tools-docs-harness', version: '1.0.0' });
|
||||
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
||||
await server.connect(serverTransport);
|
||||
await client.connect(clientTransport);
|
||||
|
||||
// "Call it" — the happy path the page quotes.
|
||||
//#region callTool_search
|
||||
const result = await client.callTool({ name: 'search', arguments: { query: 'mug' } });
|
||||
console.log(result.content);
|
||||
//#endregion callTool_search
|
||||
|
||||
// "Send arguments the schema rejects" — the rejection the page quotes.
|
||||
//#region callTool_invalid
|
||||
const rejected = await client.callTool({ name: 'search', arguments: { query: 'mug', limit: 999 } });
|
||||
console.log(rejected);
|
||||
//#endregion callTool_invalid
|
||||
|
||||
// "Return structured output" — the structured result the page quotes.
|
||||
const details = await client.callTool({ name: 'product-details', arguments: { name: 'Travel mug' } });
|
||||
console.log(details);
|
||||
|
||||
// Proof for the page's ::: tip — `.describe()` lands in the JSON Schema that
|
||||
// `tools/list` advertises for the `query` argument. Throws (non-zero exit) if
|
||||
// the claim is false.
|
||||
const { tools } = await client.listTools();
|
||||
const searchTool = tools.find(tool => tool.name === 'search');
|
||||
const properties = searchTool?.inputSchema.properties as Record<string, { description?: string }> | undefined;
|
||||
if (properties?.['query']?.description !== 'Substring to match against product names') {
|
||||
throw new Error(`tools.md tip claim failed: query.description is ${JSON.stringify(properties?.['query'])}`);
|
||||
}
|
||||
|
||||
await client.close();
|
||||
await server.close();
|
||||
Loading…
Reference in a new issue