Skip to content

Commit dbab53e

Browse files
docs: document the per-request envelope on the handler context
1 parent 97cf203 commit dbab53e

5 files changed

Lines changed: 124 additions & 1 deletion

File tree

docs/client.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,23 @@ client.setRequestHandler('roots/list', async () => {
487487

488488
When the available roots change, notify the server with {@linkcode @modelcontextprotocol/client!client/client.Client#sendRootsListChanged | client.sendRootsListChanged()}.
489489

490+
### Request context
491+
492+
Handlers receive the request context (`ctx`) as their second argument. `ctx.mcpReq.protocolVersion` (from {@linkcode @modelcontextprotocol/client!index.BaseContext | BaseContext}) is the protocol version governing the request:
493+
494+
```ts source="../examples/client/src/clientGuide.examples.ts#requestContext_handler"
495+
client.setRequestHandler('sampling/createMessage', async (request, ctx) => {
496+
console.log(`Sampling request under MCP ${ctx.mcpReq.protocolVersion}:`, request.params.messages.at(-1));
497+
498+
// In production, send messages to your LLM here
499+
return {
500+
model: 'my-model',
501+
role: 'assistant' as const,
502+
content: { type: 'text' as const, text: 'Response from the model' }
503+
};
504+
});
505+
```
506+
490507
## Error handling
491508

492509
### Tool errors vs protocol errors

docs/migration.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -602,6 +602,8 @@ The `RequestHandlerExtra` type has been replaced with a structured context type
602602
| `extra.taskStore` | `ctx.task?.store` |
603603
| `extra.taskId` | `ctx.task?.id` |
604604
| `extra.taskRequestedTtl` | `ctx.task?.requestedTtl` |
605+
| — (new in v2) | `ctx.mcpReq.protocolVersion` |
606+
| — (new in v2) | `ctx.client.capabilities`, `ctx.client.info` (only on `ServerContext`) |
605607

606608
**Before (v1):**
607609

@@ -627,9 +629,10 @@ server.setRequestHandler('tools/call', async (request, ctx) => {
627629

628630
Context fields are organized into 4 groups:
629631

630-
- **`mcpReq`** — request-level concerns: `id`, `method`, `_meta`, `signal`, `send()`, `notify()`, plus server-only `log()`, `elicitInput()`, and `requestSampling()`
632+
- **`mcpReq`** — request-level concerns: `id`, `method`, `protocolVersion`, `_meta`, `signal`, `send()`, `notify()`, plus server-only `log()`, `elicitInput()`, and `requestSampling()`
631633
- **`http?`** — HTTP transport concerns (undefined for stdio): `authInfo`, plus server-only `req`, `closeSSE`, `closeStandaloneSSE`
632634
- **`task?`** — task lifecycle: `id`, `store`, `requestedTtl`
635+
- **`client`** — server-only: the calling client's declared `capabilities` and implementation `info`
633636

634637
`BaseContext` is the common base type shared by both `ServerContext` and `ClientContext`. `ServerContext` extends each group with server-specific additions via type intersection.
635638

docs/server.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,51 @@ server.registerTool(
495495
);
496496
```
497497

498+
## Reading request context
499+
500+
Every handler receives the request context (`ctx`) as its second argument. Beyond the helpers shown above, it carries per-request facts about the caller:
501+
502+
- `ctx.mcpReq.protocolVersion` (from {@linkcode @modelcontextprotocol/server!index.BaseContext | BaseContext}) — the protocol version governing the request.
503+
- `ctx.client.capabilities` and `ctx.client.info` (from {@linkcode @modelcontextprotocol/server!index.ServerContext | ServerContext}) — the calling client's declared capabilities and implementation info.
504+
505+
Check `ctx.client.capabilities` before sending a [server-initiated request](#server-initiated-requests) so you never ask a client to do something it cannot — for example, only [elicit input](#elicitation) when the client declared the `elicitation` capability:
506+
507+
```ts source="../examples/server/src/serverGuide.examples.ts#registerTool_requestContext"
508+
server.registerTool(
509+
'delete-records',
510+
{
511+
description: 'Delete records, asking for confirmation when the client supports it',
512+
inputSchema: z.object({ table: z.string() })
513+
},
514+
async ({ table }, ctx): Promise<CallToolResult> => {
515+
// Per-request facts: the calling client and the protocol version governing this request
516+
const caller = `${ctx.client.info?.name ?? 'unknown client'} (MCP ${ctx.mcpReq.protocolVersion})`;
517+
518+
// Only ask for confirmation if the calling client declared the elicitation capability
519+
if (ctx.client.capabilities.elicitation) {
520+
const result = await ctx.mcpReq.elicitInput({
521+
mode: 'form',
522+
message: `Delete all records in ${table}?`,
523+
requestedSchema: {
524+
type: 'object',
525+
properties: { confirm: { type: 'boolean', title: 'Confirm' } },
526+
required: ['confirm']
527+
}
528+
});
529+
if (result.action !== 'accept' || result.content?.confirm !== true) {
530+
return { content: [{ type: 'text', text: 'Deletion cancelled.' }] };
531+
}
532+
}
533+
534+
// ... delete records, attributing the request to `caller` ...
535+
return { content: [{ type: 'text', text: `Deleted all records in ${table} (requested by ${caller})` }] };
536+
}
537+
);
538+
```
539+
540+
> [!IMPORTANT]
541+
> Capabilities are declarations, not authorization. Never use them to gate access to tools, resources, or data — that is the authorization layer's job.
542+
498543
## Tasks (experimental)
499544

500545
> [!WARNING]

examples/client/src/clientGuide.examples.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,22 @@ function roots_handler(client: Client) {
437437
//#endregion roots_handler
438438
}
439439

440+
/** Example: Read the governing protocol version from the handler context. */
441+
function requestContext_handler(client: Client) {
442+
//#region requestContext_handler
443+
client.setRequestHandler('sampling/createMessage', async (request, ctx) => {
444+
console.log(`Sampling request under MCP ${ctx.mcpReq.protocolVersion}:`, request.params.messages.at(-1));
445+
446+
// In production, send messages to your LLM here
447+
return {
448+
model: 'my-model',
449+
role: 'assistant' as const,
450+
content: { type: 'text' as const, text: 'Response from the model' }
451+
};
452+
});
453+
//#endregion requestContext_handler
454+
}
455+
440456
// ---------------------------------------------------------------------------
441457
// Error handling
442458
// ---------------------------------------------------------------------------
@@ -568,6 +584,7 @@ void capabilities_declaration;
568584
void sampling_handler;
569585
void elicitation_handler;
570586
void roots_handler;
587+
void requestContext_handler;
571588
void errorHandling_toolErrors;
572589
void errorHandling_lifecycle;
573590
void errorHandling_timeout;

examples/server/src/serverGuide.examples.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,46 @@ function registerTool_roots(server: McpServer) {
419419
//#endregion registerTool_roots
420420
}
421421

422+
// ---------------------------------------------------------------------------
423+
// Reading request context
424+
// ---------------------------------------------------------------------------
425+
426+
/** Example: Tool that reads per-request facts (protocol version, client capabilities) from the handler context. */
427+
function registerTool_requestContext(server: McpServer) {
428+
//#region registerTool_requestContext
429+
server.registerTool(
430+
'delete-records',
431+
{
432+
description: 'Delete records, asking for confirmation when the client supports it',
433+
inputSchema: z.object({ table: z.string() })
434+
},
435+
async ({ table }, ctx): Promise<CallToolResult> => {
436+
// Per-request facts: the calling client and the protocol version governing this request
437+
const caller = `${ctx.client.info?.name ?? 'unknown client'} (MCP ${ctx.mcpReq.protocolVersion})`;
438+
439+
// Only ask for confirmation if the calling client declared the elicitation capability
440+
if (ctx.client.capabilities.elicitation) {
441+
const result = await ctx.mcpReq.elicitInput({
442+
mode: 'form',
443+
message: `Delete all records in ${table}?`,
444+
requestedSchema: {
445+
type: 'object',
446+
properties: { confirm: { type: 'boolean', title: 'Confirm' } },
447+
required: ['confirm']
448+
}
449+
});
450+
if (result.action !== 'accept' || result.content?.confirm !== true) {
451+
return { content: [{ type: 'text', text: 'Deletion cancelled.' }] };
452+
}
453+
}
454+
455+
// ... delete records, attributing the request to `caller` ...
456+
return { content: [{ type: 'text', text: `Deleted all records in ${table} (requested by ${caller})` }] };
457+
}
458+
);
459+
//#endregion registerTool_requestContext
460+
}
461+
422462
// ---------------------------------------------------------------------------
423463
// Transports
424464
// ---------------------------------------------------------------------------
@@ -546,6 +586,7 @@ void registerTool_progress;
546586
void registerTool_sampling;
547587
void registerTool_elicitation;
548588
void registerTool_roots;
589+
void registerTool_requestContext;
549590
void registerResource_static;
550591
void registerResource_template;
551592
void registerPrompt_basic;

0 commit comments

Comments
 (0)