docs: cover tool content types, prompt image content, elicitation defaults, ping, legacy SSE serving (#2679)

This commit is contained in:
Felix Weinberger 2026-08-18 13:42:45 +01:00 • committed by GitHub
commit 68523682fd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 377 additions and 3 deletions

View file

@ -201,5 +201,14 @@ const exported = await client.callTool(
console.log(exported.content);
//#endregion callTool_progress
// "Check the connection" — the empty result the page quotes.
//#region ping_basic
const pong = await client.ping({ timeout: 5000 });
console.log(pong);
//#endregion ping_basic
if (Object.keys(pong).length !== 0) {
throw new Error(`calling.md claim failed: ping resolved with ${JSON.stringify(pong)}`);
}
await client.close();
await server.close();

View file

@ -78,6 +78,37 @@ server.registerTool(
);
//#endregion registerTool_elicitActions
// "Prefill a field with a default" — the requested schema carries `default`.
// Wrapped so the harness can register the same tool on a second server whose
// client declares `applyDefaults`; the page's fence shows the body unindented.
function registerExportReport(server: McpServer): void {
//#region registerTool_elicitDefault
server.registerTool(
'export-report',
{
description: 'Export a report after the user picks a format',
inputSchema: z.object({ name: z.string() })
},
async ({ name }, ctx) => {
const result = await ctx.mcpReq.elicitInput({
mode: 'form',
message: `Export ${name} as which format?`,
requestedSchema: {
type: 'object',
properties: { format: { type: 'string', title: 'Format', enum: ['pdf', 'csv'], default: 'pdf' } },
required: ['format']
}
});
if (result.action !== 'accept') {
return { content: [{ type: 'text', text: `Export ${result.action}.` }] };
}
return { content: [{ type: 'text', text: `Exported ${name} as ${result.content?.format}.` }] };
}
);
//#endregion registerTool_elicitDefault
}
registerExportReport(server);
// "Send the end user to a URL" — url mode hands the browser flow to the client.
//#region registerTool_elicitUrl
server.registerTool(
@ -144,6 +175,28 @@ client.setRequestHandler('elicitation/create', async () => ({ action: 'decline'
const declined = await client.callTool({ name: 'delete-dataset', arguments: { name: 'staging-snapshots' } });
console.log(declined.content);
// "Prefill a field with a default" — a client that declares `applyDefaults`
// accepts with `format` left out; the SDK fills it from the schema before the
// accept reaches the handler.
const defaultsClient = new Client(
{ name: 'defaults-host', version: '1.0.0' },
{ capabilities: { elicitation: { form: { applyDefaults: true } } } }
);
defaultsClient.setRequestHandler('elicitation/create', async () => ({ action: 'accept', content: {} }));
const [defaultsClientTransport, defaultsServerTransport] = InMemoryTransport.createLinkedPair();
const defaultsServer = new McpServer({ name: 'feedback', version: '1.0.0' });
registerExportReport(defaultsServer);
await defaultsServer.connect(defaultsServerTransport);
await defaultsClient.connect(defaultsClientTransport);
const exported = await defaultsClient.callTool({ name: 'export-report', arguments: { name: 'quarterly-sales' } });
console.log(exported.content);
const exportedText = Array.isArray(exported.content) && exported.content[0]?.type === 'text' ? exported.content[0].text : undefined;
if (exported.isError || exportedText !== 'Exported quarterly-sales as pdf.') {
throw new Error(`elicitation.md claim failed: applyDefaults round returned ${JSON.stringify(exported.content)}`);
}
await defaultsClient.close();
await defaultsServer.close();
// "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.

View file

@ -60,6 +60,28 @@ server.registerPrompt(
);
//#endregion registerPrompt_messages
//#region registerPrompt_image
server.registerPrompt(
'describe-image',
{
description: 'Describe an image for alt text',
argsSchema: z.object({ imageBase64: z.string().describe('Base64-encoded PNG') })
},
({ imageBase64 }) => ({
messages: [
{
role: 'user' as const,
content: { type: 'image' as const, data: imageBase64, mimeType: 'image/png' }
},
{
role: 'user' as const,
content: { type: 'text' as const, text: 'Write one sentence of alt text for this image.' }
}
]
})
);
//#endregion registerPrompt_image
//#region registerPrompt_embedResource
const styleGuide = '- Prefer const over let.\n- No single-letter identifiers.';
@ -152,6 +174,18 @@ try {
}
//#endregion getPrompt_invalid
// "Add an image to a message" — the image message the page quotes; the
// argument is a real (1x1) PNG.
const described = await client.getPrompt({
name: 'describe-image',
arguments: { imageBase64: 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=' }
});
console.log(described.messages[0]);
const imageMessage = described.messages[0]?.content;
if (imageMessage?.type !== 'image' || imageMessage.mimeType !== 'image/png') {
throw new Error(`prompts.md claim failed: describe-image first message is ${JSON.stringify(imageMessage)}`);
}
// "Embed a resource in a message" — the embedded-resource message the page quotes.
const review = await client.getPrompt({
name: 'review-against-style',

View file

@ -76,6 +76,38 @@ server.registerTool(
);
//#endregion registerTool_annotations
//#region registerTool_contentTypes
// Base64 payloads; read yours from disk: readFileSync('card.png').toString('base64')
const cardPng = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
const spokenNameWav = 'UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQAAAAA=';
server.registerTool(
'product-card',
{
description: 'Render one product as an image, a spoken name, and its catalog record',
inputSchema: z.object({ name: z.string() })
},
async ({ name }) => {
const product = catalog.find(candidate => candidate.name === name);
if (!product) throw new Error(`No product named ${name}`);
return {
content: [
{ type: 'image', data: cardPng, mimeType: 'image/png' },
{ type: 'audio', data: spokenNameWav, mimeType: 'audio/wav' },
{
type: 'resource',
resource: {
uri: `catalog://products/${encodeURIComponent(product.name)}`,
mimeType: 'application/json',
text: JSON.stringify(product)
}
}
]
};
}
);
//#endregion registerTool_contentTypes
// ---------------------------------------------------------------------------
// 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.
@ -105,6 +137,14 @@ console.log(rejected);
const details = await client.callTool({ name: 'product-details', arguments: { name: 'Travel mug' } });
console.log(details);
// "Return other content types" — the three-block result the page quotes.
const card = await client.callTool({ name: 'product-card', arguments: { name: 'Travel mug' } });
console.log(card.content);
const cardTypes = Array.isArray(card.content) ? card.content.map(block => block.type) : [];
if (card.isError || cardTypes.join(',') !== 'image,audio,resource') {
throw new Error(`tools.md claim failed: product-card returned ${JSON.stringify(card.content)}`);
}
// 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.

View file

@ -56,6 +56,40 @@ async function serve(request: Request): Promise<Response> {
}
//#endregion isLegacyRequest_route
// ---------------------------------------------------------------------------
// "Know where SSE went" — the frozen v1 transport on two Express routes. The
// app never listens here: docs companions never bind a port.
// ---------------------------------------------------------------------------
//#region SSEServerTransport_express
import { createMcpExpressApp } from '@modelcontextprotocol/express';
import { SSEServerTransport } from '@modelcontextprotocol/server-legacy/sse';
const sessions = new Map<string, SSEServerTransport>();
const sseApp = createMcpExpressApp({ host: '0.0.0.0', allowedHosts: ['sse.example.com'], jsonLimit: '4mb' });
sseApp.get('/sse', async (_req, res) => {
const transport = new SSEServerTransport('/messages', res);
sessions.set(transport.sessionId, transport);
transport.onclose = () => sessions.delete(transport.sessionId);
await buildServer().connect(transport);
});
sseApp.post('/messages', async (req, res) => {
const sessionId = req.query.sessionId;
if (typeof sessionId !== 'string') {
res.status(400).send('Missing sessionId parameter');
return;
}
const transport = sessions.get(sessionId);
if (!transport) {
res.status(404).send('Session not found');
return;
}
await transport.handlePostMessage(req, res, req.body);
});
//#endregion SSEServerTransport_express
// ---------------------------------------------------------------------------
// Harness (not shown on the page). A 2025-era client opens with a claim-less
// `initialize` POST; build that request twice and send it to the strict

View file

@ -31,6 +31,7 @@
"@modelcontextprotocol/hono": "workspace:^",
"@modelcontextprotocol/node": "workspace:^",
"@modelcontextprotocol/server": "workspace:^",
"@modelcontextprotocol/server-legacy": "workspace:^",
"@valibot/to-json-schema": "catalog:devTools",
"ajv": "catalog:runtimeShared",
"arktype": "catalog:devTools",

View file

@ -11,6 +11,7 @@
"@modelcontextprotocol/server/_shims": ["./node_modules/@modelcontextprotocol/server/src/shimsNode.ts"],
"@modelcontextprotocol/server/validators/ajv": ["./node_modules/@modelcontextprotocol/server/src/validators/ajv.ts"],
"@modelcontextprotocol/server/validators/cf-worker": ["./node_modules/@modelcontextprotocol/server/src/validators/cfWorker.ts"],
"@modelcontextprotocol/server-legacy/sse": ["./node_modules/@modelcontextprotocol/server-legacy/src/sse/index.ts"],
"@modelcontextprotocol/client": ["./node_modules/@modelcontextprotocol/client/src/index.ts"],
"@modelcontextprotocol/client/stdio": ["./node_modules/@modelcontextprotocol/client/src/stdio.ts"],
"@modelcontextprotocol/client/_shims": ["./node_modules/@modelcontextprotocol/client/src/shimsNode.ts"],