docs: cover tool content types, prompt image content, elicitation defaults, ping, legacy SSE serving (#2679)
This commit is contained in:
parent
e3f39c1a81
commit
68523682fd
14 changed files with 377 additions and 3 deletions
|
|
@ -169,6 +169,25 @@ The updates stream in while the call is still pending; the return type does not
|
|||
[ { type: 'text', text: '2 orders exported as csv' } ]
|
||||
```
|
||||
|
||||
## Check the connection
|
||||
|
||||
`ping` sends a `ping` request and resolves with the empty result the server returns; the SDK answers a `ping` on both sides automatically, so neither side registers a handler.
|
||||
|
||||
```ts source="../../examples/guides/clients/calling.examples.ts#ping_basic"
|
||||
const pong = await client.ping({ timeout: 5000 });
|
||||
console.log(pong);
|
||||
```
|
||||
|
||||
The `orders` server answers at once:
|
||||
|
||||
```
|
||||
{}
|
||||
```
|
||||
|
||||
A server that stops answering rejects the call with an `SdkError` coded `REQUEST_TIMEOUT` once `timeout` elapses.
|
||||
|
||||
`ping` is a 2025-era method — see [Protocol versions](../protocol-versions.md).
|
||||
|
||||
## Recap
|
||||
|
||||
- `listTools`, `listResources`, `listResourceTemplates`, and `listPrompts` aggregate every page; `{ cursor }` fetches a single raw page and `listMaxPages` caps the walk.
|
||||
|
|
@ -176,3 +195,4 @@ The updates stream in while the call is still pending; the return type does not
|
|||
- `readResource({ uri })` and `getPrompt({ name, arguments })` follow the same list-then-fetch shape as tools.
|
||||
- `complete()` returns the server's suggestions for a prompt or resource-template argument.
|
||||
- `onprogress` in the request options streams progress updates without changing the call's return type.
|
||||
- `ping()` checks that the server still answers; both sides answer pings automatically.
|
||||
|
|
|
|||
|
|
@ -171,6 +171,7 @@ This table is the only copy of the era differences in these docs. `getProtocolEr
|
|||
| `ctx.mcpReq.log()` level filter | session-scoped `logging/setLevel` | per-request `logLevel` `_meta` envelope key (absent = no logs) |
|
||||
| HTTP `400` with a JSON-RPC error body | `SdkHttpError` | `ProtocolError`, delivered in-band |
|
||||
| Era-mismatched spec method (outbound) | n/a | `SdkError(MethodNotSupportedByProtocolVersion)` |
|
||||
| Liveness check | `client.ping()` | not defined — outbound call rejects per the era-mismatch row |
|
||||
|
||||
## Separate deprecation from era
|
||||
|
||||
|
|
|
|||
|
|
@ -123,6 +123,45 @@ server.registerTool(
|
|||
[ { type: 'text', text: 'Declined - nothing deleted.' } ]
|
||||
```
|
||||
|
||||
## Prefill a field with a default
|
||||
|
||||
Set `default` on a field and the client renders the form with that value already filled in.
|
||||
|
||||
```ts source="../../examples/guides/servers/elicitation.examples.ts#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}.` }] };
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
`requestedSchema` reaches the client unchanged, `default` included; the end user submits the prefilled `pdf` or picks `csv`. An accept with `format` left out still returns:
|
||||
|
||||
```
|
||||
[ { type: 'text', text: 'Exported quarterly-sales as pdf.' } ]
|
||||
```
|
||||
|
||||
::: info
|
||||
A client that declares `elicitation: { form: { applyDefaults: true } }` — an SDK flag, not a protocol capability — fills defaulted fields the end user leaves out before the accept reaches your handler; the output above is that case.
|
||||
:::
|
||||
|
||||
## Send the end user to a URL
|
||||
|
||||
**URL mode** replaces the form with a browser flow: pass `url` and a unique `elicitationId` instead of `requestedSchema`.
|
||||
|
|
@ -181,5 +220,6 @@ Elicitation only works against a client that declared the `elicitation` capabili
|
|||
- `ctx.mcpReq.elicitInput` sends an `elicitation/create` request mid-handler and resolves with the end user's answer.
|
||||
- Form mode carries a `message` and a flat JSON-Schema `requestedSchema`; the SDK validates accepted content against it.
|
||||
- `result.action` is `accept`, `decline`, or `cancel`; `result.content` is present only on accept.
|
||||
- `default` on a `requestedSchema` field prefills the form; a client that declares `applyDefaults` fills the field in when the end user leaves it out.
|
||||
- URL mode hands the end user a browser flow — use it for anything sensitive.
|
||||
- Calls against a client that never declared the `elicitation` capability fail before reaching the wire.
|
||||
|
|
|
|||
|
|
@ -116,6 +116,47 @@ server.registerPrompt(
|
|||
|
||||
The host hands the messages to the model in order, so the trailing `assistant` message becomes the start of its reply. `content` accepts the same union a tool result does: `text`, `image`, `audio`, `resource_link`, and `resource`.
|
||||
|
||||
## Add an image to a message
|
||||
|
||||
An `image` block carries base64 `data` and a `mimeType`; pair it with a `text` block that says what to do with the image.
|
||||
|
||||
```ts source="../../examples/guides/servers/prompts.examples.ts#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.' }
|
||||
}
|
||||
]
|
||||
})
|
||||
);
|
||||
```
|
||||
|
||||
`prompts/get` returns the image block as the first message, bytes unchanged:
|
||||
|
||||
```
|
||||
{
|
||||
role: 'user',
|
||||
content: {
|
||||
type: 'image',
|
||||
data: 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=',
|
||||
mimeType: 'image/png'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`audio` takes the same shape: base64 `data` plus `mimeType`.
|
||||
|
||||
## Embed a resource in a message
|
||||
|
||||
`type: 'resource'` puts a resource's contents inside a message. Register the resource as usual — see [Resources](./resources.md) — and embed the same `uri`, `mimeType`, and `text` in the prompt.
|
||||
|
|
@ -201,5 +242,5 @@ The client sends `completion/complete` with the characters typed so far; the SDK
|
|||
- `argsSchema` is one Zod object: the advertised argument list, argument validation, and the callback's argument types.
|
||||
- Arguments that fail the schema reject `prompts/get` with a `-32602` protocol error; the callback never runs.
|
||||
- The callback returns `{ messages }`; each message names a `role` and one `content` block.
|
||||
- A message can embed a registered resource's contents with `type: 'resource'`.
|
||||
- A message can carry an `image` (base64 `data` plus `mimeType`) or embed a registered resource's contents with `type: 'resource'`.
|
||||
- `completable()` adds per-argument autocompletion.
|
||||
|
|
|
|||
|
|
@ -129,6 +129,69 @@ Calling `product-details` with `{ name: 'Travel mug' }` returns both renderings:
|
|||
|
||||
The wire encoding of structured results differs by protocol era — see [Protocol versions](../protocol-versions.md).
|
||||
|
||||
## Return other content types
|
||||
|
||||
One result can mix content blocks: `image` and `audio` carry base64 `data` with a `mimeType`; `resource` embeds a resource's contents inline; `resource_link` names a resource by `uri` without its bytes.
|
||||
|
||||
```ts source="../../examples/guides/servers/tools.examples.ts#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)
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
Calling `product-card` with `{ name: 'Travel mug' }` returns the three blocks as written:
|
||||
|
||||
```
|
||||
[
|
||||
{
|
||||
type: 'image',
|
||||
data: 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=',
|
||||
mimeType: 'image/png'
|
||||
},
|
||||
{
|
||||
type: 'audio',
|
||||
data: 'UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQAAAAA=',
|
||||
mimeType: 'audio/wav'
|
||||
},
|
||||
{
|
||||
type: 'resource',
|
||||
resource: {
|
||||
uri: 'catalog://products/Travel%20mug',
|
||||
mimeType: 'application/json',
|
||||
text: '{"name":"Travel mug","price":24}'
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
The blocks reach the client exactly as returned, and the embedded `resource` arrives without a `resources/read` round trip.
|
||||
|
||||
## Annotate the tool
|
||||
|
||||
`title` is the display name; `annotations` are behavior hints for the client.
|
||||
|
|
@ -156,4 +219,5 @@ A tool that takes no arguments omits `inputSchema`. Annotations never change how
|
|||
- The one schema yields the advertised JSON Schema, argument validation, and the handler's argument types.
|
||||
- Arguments that fail the schema come back as an `isError: true` tool result; the handler never runs.
|
||||
- `outputSchema` plus `structuredContent` add machine-readable results, validated before they leave the server.
|
||||
- `content` blocks are `text`, `image`, `audio`, `resource_link`, or an embedded `resource`; one result can mix them.
|
||||
- `title` and `annotations` describe the tool to clients and never change execution.
|
||||
|
|
|
|||
|
|
@ -91,7 +91,40 @@ Behind an Express body parser the Node stream is already drained: build the `Req
|
|||
|
||||
The v2 server never serves the HTTP+SSE transport. An SSE server moving to v2 moves to Streamable HTTP — `createMcpHandler` above — as part of the [v2 upgrade](../migration/upgrade-to-v2.md).
|
||||
|
||||
The client side keeps `SSEClientTransport`, so a v2 `Client` still reaches old SSE servers. For a server deployment that cannot move yet, a frozen v1 copy of the transport ships as `@modelcontextprotocol/server-legacy/sse` (deprecated).
|
||||
The client side keeps `SSEClientTransport`, so a v2 `Client` still reaches old SSE servers. For a server deployment that cannot move yet, a frozen v1 copy of the transport ships as `@modelcontextprotocol/server-legacy/sse` (deprecated, planned for removal in v3).
|
||||
|
||||
Mount the frozen transport on two Express routes: `GET /sse` opens the stream and `POST /messages` delivers each client message to the session its `sessionId` query names. `createMcpExpressApp` takes the same options as on the [Express](./express.md) page: binding beyond localhost drops the default `Host`/`Origin` validation, so name the hosts you serve in `allowedHosts`, and raise `jsonLimit` above Express's 100kb default, since the SSE transport itself accepts messages up to 4mb.
|
||||
|
||||
```ts source="../../examples/guides/serving/legacy-clients.examples.ts#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);
|
||||
});
|
||||
```
|
||||
|
||||
Each `GET /sse` connects a fresh instance from `buildServer` and answers with an `endpoint` event naming `/messages?sessionId=…`; the client POSTs every JSON-RPC message there and reads responses off the stream.
|
||||
|
||||
## Recap
|
||||
|
||||
|
|
@ -99,4 +132,4 @@ The client side keeps `SSEClientTransport`, so a v2 `Client` still reaches old S
|
|||
- The default HTTP posture is per request and stateless: legacy `GET` and `DELETE` session operations answer `405`.
|
||||
- `serveStdio` decides the era once per connection; its default is `'serve'`.
|
||||
- `isLegacyRequest` in front of a strict handler keeps an existing sessionful 2025 deployment serving its clients.
|
||||
- The v2 server never serves SSE; the frozen v1 transport is `@modelcontextprotocol/server-legacy/sse`, and the client keeps `SSEClientTransport`.
|
||||
- The v2 server never serves SSE; the frozen v1 `SSEServerTransport` in `@modelcontextprotocol/server-legacy/sse` mounts on `GET /sse` + `POST /messages`, and the client keeps `SSEClientTransport`.
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
|
|
|
|||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
|
|
@ -331,6 +331,9 @@ importers:
|
|||
'@modelcontextprotocol/server':
|
||||
specifier: workspace:^
|
||||
version: link:../packages/server
|
||||
'@modelcontextprotocol/server-legacy':
|
||||
specifier: workspace:^
|
||||
version: link:../packages/server-legacy
|
||||
'@valibot/to-json-schema':
|
||||
specifier: catalog:devTools
|
||||
version: 1.6.0(valibot@1.3.1(typescript@5.9.3))
|
||||
|
|
|
|||
Loading…
Reference in a new issue