Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
107767a
save commit
KKonstantinov Dec 7, 2025
68ff665
context API - backwards compatible introduction
KKonstantinov Dec 7, 2025
f58b491
fixes
KKonstantinov Dec 7, 2025
459bff4
Merge branch 'main' into feature/ctx-in-callbacks
KKonstantinov Dec 7, 2025
a23e2f2
Merge branch 'main' into feature/ctx-in-callbacks
KKonstantinov Dec 7, 2025
4ff84a1
Merge branch 'main' into feature/ctx-in-callbacks
KKonstantinov Dec 8, 2025
e89d9d4
moved properties under objects
KKonstantinov Dec 8, 2025
187a3cd
prettier fix
KKonstantinov Dec 8, 2025
96169b3
move logger methods under loggingNotification
KKonstantinov Dec 8, 2025
a57840c
merge commit
KKonstantinov Dec 9, 2025
161f584
merge commit - v2
KKonstantinov Dec 23, 2025
d5f5047
merge commit - v2
KKonstantinov Dec 23, 2025
109dc52
base context, client context, server context separation
KKonstantinov Jan 21, 2026
deca7fd
rename method to createRequestContext, update docs
KKonstantinov Jan 21, 2026
36be75e
fix server conformance
KKonstantinov Jan 21, 2026
f609478
fix conformance
KKonstantinov Jan 21, 2026
3c4f9d8
move types
KKonstantinov Jan 21, 2026
2c31eb2
merge commit
KKonstantinov Jan 22, 2026
86549fa
rename extra vars to ctx
KKonstantinov Jan 22, 2026
920da7e
prettier fix
KKonstantinov Jan 22, 2026
639d7bd
add changeset
KKonstantinov Jan 22, 2026
ae9c253
merge commit
KKonstantinov Jan 23, 2026
f512df4
save commit
KKonstantinov Jan 25, 2026
27989ca
save commit
KKonstantinov Jan 26, 2026
a0a89bb
save commit
KKonstantinov Jan 26, 2026
caa3164
merge commit
KKonstantinov Jan 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .changeset/hot-trees-sing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'@modelcontextprotocol/express': patch
'@modelcontextprotocol/hono': patch
'@modelcontextprotocol/node': patch
'@modelcontextprotocol/eslint-config': patch
'@modelcontextprotocol/test-integration': patch
'@modelcontextprotocol/client': patch
'@modelcontextprotocol/server': patch
'@modelcontextprotocol/core': patch
---

add context API to tool, prompt, resource callbacks, linting
50 changes: 36 additions & 14 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,37 +136,59 @@ When a request arrives from the remote side:
2. **`Protocol.connect()`** routes to `_onrequest()`, `_onresponse()`, or `_onnotification()`
3. **`Protocol._onrequest()`**:
- Looks up handler in `_requestHandlers` map (keyed by method name)
- Creates `RequestHandlerExtra` with `signal`, `sessionId`, `sendNotification`, `sendRequest`
- Creates a context object (`ServerContext` or `ClientContext`) via `createRequestContext()`
- Invokes handler, sends JSON-RPC response back via transport
4. **Handler** was registered via `setRequestHandler(Schema, handler)`

### Handler Registration

```typescript
// In Client (for server→client requests like sampling, elicitation)
client.setRequestHandler(CreateMessageRequestSchema, async (request, extra) => {
client.setRequestHandler(CreateMessageRequestSchema, async (request, ctx) => {
// Handle sampling request from server
return { role: "assistant", content: {...}, model: "..." };
});

// In Server (for client→server requests like tools/call)
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
server.setRequestHandler(CallToolRequestSchema, async (request, ctx) => {
// Handle tool call from client
return { content: [...] };
});
```

### Request Handler Extra
### Request Handler Context

The `extra` parameter in handlers (`RequestHandlerExtra`) provides:
The `ctx` parameter in handlers provides a structured context with three layers:

- `signal`: AbortSignal for cancellation
**`ctx.mcpCtx`** - MCP-level context:

- `requestId`: JSON-RPC message ID
- `method`: The method being called
- `_meta`: Request metadata
- `sessionId`: Transport session identifier

**`ctx.requestCtx`** - Request-level context:

- `signal`: AbortSignal for cancellation
- `authInfo`: Validated auth token info (if authenticated)
- `requestId`: JSON-RPC message ID
- `sendNotification(notification)`: Send related notification back
- `sendRequest(request, schema)`: Send related request (for bidirectional flows)
- `taskStore`: Task storage interface (if tasks enabled)
- For server: `uri`, `headers`, `stream` (HTTP details)

**`ctx.taskCtx`** - Task context (when tasks are enabled):

- `id`: Current task ID (updates after `store.createTask()`)
- `store`: Request-scoped task store (`RequestTaskStore`)
- `requestedTtl`: Requested TTL for the task

**Context methods**:

- `ctx.sendNotification(notification)`: Send notification back
- `ctx.sendRequest(request, schema)`: Send request (for bidirectional flows)

For server contexts, additional helpers:

- `ctx.loggingNotification(level, data, logger)`: Send logging notification
- `ctx.requestSampling(params)`: Request sampling from client
- `ctx.elicitInput(params)`: Request user input from client

### Capability Checking

Expand Down Expand Up @@ -197,7 +219,7 @@ const result = await server.createMessage({
});

// Client must have registered handler:
client.setRequestHandler(CreateMessageRequestSchema, async (request, extra) => {
client.setRequestHandler(CreateMessageRequestSchema, async (request, ctx) => {
// Client-side LLM call
return { role: "assistant", content: {...} };
});
Expand All @@ -208,8 +230,8 @@ client.setRequestHandler(CreateMessageRequestSchema, async (request, extra) => {
### Request Handler Registration (Low-Level Server)

```typescript
server.setRequestHandler(SomeRequestSchema, async (request, extra) => {
// extra contains sessionId, authInfo, sendNotification, etc.
server.setRequestHandler(SomeRequestSchema, async (request, ctx) => {
// ctx provides mcpCtx, requestCtx, taskCtx, sendNotification, sendRequest
return {
/* result */
};
Expand All @@ -219,7 +241,7 @@ server.setRequestHandler(SomeRequestSchema, async (request, extra) => {
### Tool Registration (High-Level McpServer)

```typescript
mcpServer.tool('tool-name', { param: z.string() }, async ({ param }, extra) => {
mcpServer.tool('tool-name', { param: z.string() }, async ({ param }, ctx) => {
return { content: [{ type: 'text', text: 'result' }] };
});
```
Expand Down
4 changes: 2 additions & 2 deletions examples/client/src/elicitationUrlExample.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
ErrorCode,
getDisplayName,
ListToolsResultSchema,
McpError,
ProtocolError,
StreamableHTTPClientTransport,
UnauthorizedError,
UrlElicitationRequiredError
Expand Down Expand Up @@ -339,7 +339,7 @@ async function handleElicitationRequest(request: ElicitRequest): Promise<ElicitR
} else {
// Should not happen because the client declares its capabilities to the server,
// but being defensive is a good practice:
throw new McpError(ErrorCode.InvalidParams, `Unsupported elicitation mode: ${mode}`);
throw new ProtocolError(ErrorCode.InvalidParams, `Unsupported elicitation mode: ${mode}`);
}
}

Expand Down
3 changes: 2 additions & 1 deletion examples/client/src/multipleClientsParallel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { CallToolRequest, CallToolResult } from '@modelcontextprotocol/clie
import {
CallToolResultSchema,
Client,
isTextContent,
LoggingMessageNotificationSchema,
StreamableHTTPClientTransport
} from '@modelcontextprotocol/client';
Expand Down Expand Up @@ -133,7 +134,7 @@ async function main(): Promise<void> {
console.log(`\n[${id}] Tool result:`);
if (Array.isArray(result.content)) {
for (const item of result.content) {
if (item.type === 'text' && item.text) {
if (isTextContent(item)) {
console.log(` ${item.text}`);
} else {
console.log(` ${item.type} content:`, item);
Expand Down
3 changes: 2 additions & 1 deletion examples/client/src/parallelToolCallsClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { CallToolResult, ListToolsRequest } from '@modelcontextprotocol/cli
import {
CallToolResultSchema,
Client,
isTextContent,
ListToolsResultSchema,
LoggingMessageNotificationSchema,
StreamableHTTPClientTransport
Expand Down Expand Up @@ -60,7 +61,7 @@ async function main(): Promise<void> {
for (const [caller, result] of Object.entries(toolResults)) {
console.log(`\n=== Tool result for ${caller} ===`);
for (const item of result.content) {
if (item.type === 'text') {
if (isTextContent(item)) {
console.log(` ${item.text}`);
} else {
console.log(` ${item.type} content:`, item);
Expand Down
5 changes: 3 additions & 2 deletions examples/client/src/simpleOAuthClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { CallToolRequest, ListToolsRequest, OAuthClientMetadata } from '@mo
import {
CallToolResultSchema,
Client,
isTextContent,
ListToolsResultSchema,
StreamableHTTPClientTransport,
UnauthorizedError
Expand Down Expand Up @@ -315,7 +316,7 @@ class InteractiveOAuthClient {
console.log(`\n🔧 Tool '${toolName}' result:`);
if (result.content) {
for (const content of result.content) {
if (content.type === 'text') {
if (isTextContent(content)) {
console.log(content.text);
} else {
console.log(content);
Expand Down Expand Up @@ -396,7 +397,7 @@ class InteractiveOAuthClient {
case 'result': {
console.log('✓ Completed!');
for (const content of message.result.content) {
if (content.type === 'text') {
if (isTextContent(content)) {
console.log(content.text);
} else {
console.log(content);
Expand Down
11 changes: 6 additions & 5 deletions examples/client/src/simpleStreamableHttp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@ import {
ErrorCode,
getDisplayName,
GetPromptResultSchema,
isTextContent,
ListPromptsResultSchema,
ListResourcesResultSchema,
ListToolsResultSchema,
LoggingMessageNotificationSchema,
McpError,
ProtocolError,
ReadResourceResultSchema,
RELATED_TASK_META_KEY,
ResourceListChangedNotificationSchema,
Expand Down Expand Up @@ -273,7 +274,7 @@ async function connect(url?: string): Promise<void> {
// Set up elicitation request handler with proper validation
client.setRequestHandler(ElicitRequestSchema, async request => {
if (request.params.mode !== 'form') {
throw new McpError(ErrorCode.InvalidParams, `Unsupported elicitation mode: ${request.params.mode}`);
throw new ProtocolError(ErrorCode.InvalidParams, `Unsupported elicitation mode: ${request.params.mode}`);
}
console.log('\n🔔 Elicitation (form) Request Received:');
console.log(`Message: ${request.params.message}`);
Expand Down Expand Up @@ -737,7 +738,7 @@ async function runNotificationsToolWithResumability(interval: number, count: num

console.log('Tool result:');
for (const item of result.content) {
if (item.type === 'text') {
if (isTextContent(item)) {
console.log(` ${item.text}`);
} else {
console.log(` ${item.type} content:`, item);
Expand Down Expand Up @@ -791,7 +792,7 @@ async function getPrompt(name: string, args: Record<string, unknown>): Promise<v
const promptResult = await client.request(promptRequest, GetPromptResultSchema);
console.log('Prompt template:');
for (const [index, msg] of promptResult.messages.entries()) {
console.log(` [${index + 1}] ${msg.role}: ${msg.content.type === 'text' ? msg.content.text : JSON.stringify(msg.content)}`);
console.log(` [${index + 1}] ${msg.role}: ${isTextContent(msg.content) ? msg.content.text : JSON.stringify(msg.content)}`);
}
} catch (error) {
console.log(`Error getting prompt ${name}: ${error}`);
Expand Down Expand Up @@ -913,7 +914,7 @@ async function callToolTask(name: string, args: Record<string, unknown>): Promis
console.log('Task completed!');
console.log('Tool result:');
for (const item of message.result.content) {
if (item.type === 'text') {
if (isTextContent(item)) {
console.log(` ${item.text}`);
}
}
Expand Down
Loading
Loading