Skip to content
Closed
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
36 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
a5704c5
Merge branch 'main' into feature/ctx-in-callbacks
KKonstantinov Jan 23, 2026
f39dc4e
Merge branch 'main' into feature/ctx-in-callbacks
KKonstantinov Jan 26, 2026
cb4c500
switch server and client to work with ServerContextInterface and Clie…
KKonstantinov Jan 27, 2026
f5b27d4
Merge branch 'feature/ctx-in-callbacks' of github.com:KKonstantinov/t…
KKonstantinov Jan 27, 2026
9b0ed8d
Merge branch 'main' into feature/ctx-in-callbacks
KKonstantinov Jan 29, 2026
7767079
Merge branch 'main' of github.com:modelcontextprotocol/typescript-sdk…
KKonstantinov Feb 2, 2026
f765e07
update context interface
KKonstantinov Feb 2, 2026
184dbca
Merge branch 'feature/ctx-in-callbacks' of github.com:KKonstantinov/t…
KKonstantinov Feb 2, 2026
69b1ae6
Merge branch 'main' into feature/ctx-in-callbacks
KKonstantinov Feb 2, 2026
9132b5a
update migration docs
KKonstantinov Feb 2, 2026
08da5e0
merge commit
KKonstantinov Feb 3, 2026
5b05ff6
merge commit
KKonstantinov Feb 3, 2026
ebac5d4
update conformance ctx - sampling fail
KKonstantinov Feb 3, 2026
b79763f
Merge branch 'main' into feature/ctx-in-callbacks
KKonstantinov Feb 3, 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: 3 additions & 1 deletion examples/client/src/simpleStreamableHttp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,10 +271,12 @@ async function connect(url?: string): Promise<void> {
};

// Set up elicitation request handler with proper validation
client.setRequestHandler(ElicitRequestSchema, async request => {
client.setRequestHandler(ElicitRequestSchema, async (request, ctx) => {
if (request.params.mode !== 'form') {
throw new McpError(ErrorCode.InvalidParams, `Unsupported elicitation mode: ${request.params.mode}`);
}

console.log(`${ctx.mcpCtx.method} elicitation request received`);
console.log('\n🔔 Elicitation (form) Request Received:');
console.log(`Message: ${request.params.message}`);
console.log(`Related Task: ${request.params._meta?.[RELATED_TASK_META_KEY]?.taskId}`);
Expand Down
10 changes: 5 additions & 5 deletions examples/server/src/elicitationUrlExample.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,12 @@ const getServer = () => {
cartId: z.string().describe('The ID of the cart to confirm')
}
},
async ({ cartId }, extra): Promise<CallToolResult> => {
async ({ cartId }, ctx): Promise<CallToolResult> => {
/*
In a real world scenario, there would be some logic here to check if the user has the provided cartId.
For the purposes of this example, we'll throw an error (-> elicits the client to open a URL to confirm payment)
*/
const sessionId = extra.sessionId;
const sessionId = ctx.mcpCtx.sessionId;
if (!sessionId) {
throw new Error('Expected a Session ID');
}
Expand Down Expand Up @@ -79,15 +79,15 @@ const getServer = () => {
param1: z.string().describe('First parameter')
}
},
async (_, extra): Promise<CallToolResult> => {
async (_, ctx): Promise<CallToolResult> => {
/*
In a real world scenario, there would be some logic here to check if we already have a valid access token for the user.
Auth info (with a subject or `sub` claim) can be typically be found in `extra.authInfo`.
Auth info (with a subject or `sub` claim) can be typically be found in `ctx.requestCtx.authInfo`.
If we do, we can just return the result of the tool call.
If we don't, we can throw an ElicitationRequiredError to request the user to authenticate.
For the purposes of this example, we'll throw an error (-> elicits the client to open a URL to authenticate).
*/
const sessionId = extra.sessionId;
const sessionId = ctx.mcpCtx.sessionId;
if (!sessionId) {
throw new Error('Expected a Session ID');
}
Expand Down
8 changes: 4 additions & 4 deletions examples/server/src/jsonResponseStreamableHttp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,15 @@ const getServer = () => {
name: z.string().describe('Name to greet')
}
},
async ({ name }, extra): Promise<CallToolResult> => {
async ({ name }, ctx): Promise<CallToolResult> => {
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));

await server.sendLoggingMessage(
{
level: 'debug',
data: `Starting multi-greet for ${name}`
},
extra.sessionId
ctx.mcpCtx.sessionId
);

await sleep(1000); // Wait 1 second before first greeting
Expand All @@ -69,7 +69,7 @@ const getServer = () => {
level: 'info',
data: `Sending first greeting to ${name}`
},
extra.sessionId
ctx.mcpCtx.sessionId
);

await sleep(1000); // Wait another second before second greeting
Expand All @@ -79,7 +79,7 @@ const getServer = () => {
level: 'info',
data: `Sending second greeting to ${name}`
},
extra.sessionId
ctx.mcpCtx.sessionId
);

return {
Expand Down
4 changes: 2 additions & 2 deletions examples/server/src/simpleStatelessStreamableHttp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ const getServer = () => {
count: z.number().describe('Number of notifications to send (0 for 100)').default(10)
}
},
async ({ interval, count }, extra): Promise<CallToolResult> => {
async ({ interval, count }, ctx): Promise<CallToolResult> => {
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
let counter = 0;

Expand All @@ -61,7 +61,7 @@ const getServer = () => {
level: 'info',
data: `Periodic notification #${counter} at ${new Date().toISOString()}`
},
extra.sessionId
ctx.mcpCtx.sessionId
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[style] would this be cool if it were ctx.mcp.sessionId rather than repeating ctx. Same with requestCtx.

);
} catch (error) {
console.error('Error sending notification:', error);
Expand Down
64 changes: 46 additions & 18 deletions examples/server/src/simpleStreamableHttp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,15 @@ const getServer = () => {
name: z.string().describe('Name to greet')
}
},
async ({ name }): Promise<CallToolResult> => {
async ({ name }, ctx): Promise<CallToolResult> => {
await ctx.loggingNotification.log(
{
level: 'debug',
data: `Starting greet for ${name}`
},
ctx.mcpCtx.sessionId
);

return {
content: [
{
Expand All @@ -88,15 +96,15 @@ const getServer = () => {
openWorldHint: false
}
},
async ({ name }, extra): Promise<CallToolResult> => {
async ({ name }, ctx): Promise<CallToolResult> => {
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));

await server.sendLoggingMessage(
{
level: 'debug',
data: `Starting multi-greet for ${name}`
},
extra.sessionId
ctx.mcpCtx.sessionId
);

await sleep(1000); // Wait 1 second before first greeting
Expand All @@ -106,7 +114,7 @@ const getServer = () => {
level: 'info',
data: `Sending first greeting to ${name}`
},
extra.sessionId
ctx.mcpCtx.sessionId
);

await sleep(1000); // Wait another second before second greeting
Expand All @@ -116,7 +124,7 @@ const getServer = () => {
level: 'info',
data: `Sending second greeting to ${name}`
},
extra.sessionId
ctx.mcpCtx.sessionId
);

return {
Expand All @@ -139,7 +147,7 @@ const getServer = () => {
infoType: z.enum(['contact', 'preferences', 'feedback']).describe('Type of information to collect')
}
},
async ({ infoType }, extra): Promise<CallToolResult> => {
async ({ infoType }, ctx): Promise<CallToolResult> => {
let message: string;
let requestedSchema: {
type: 'object';
Expand Down Expand Up @@ -238,8 +246,8 @@ const getServer = () => {
}

try {
// Use sendRequest through the extra parameter to elicit input
const result = await extra.sendRequest(
// Use sendRequest through the ctx parameter to elicit input
const result = await ctx.sendRequest(
{
method: 'elicitation/create',
params: {
Expand Down Expand Up @@ -302,7 +310,15 @@ const getServer = () => {
name: z.string().describe('Name to include in greeting')
}
},
async ({ name }): Promise<GetPromptResult> => {
async ({ name }, ctx): Promise<GetPromptResult> => {
await ctx.loggingNotification.log(
{
level: 'debug',
data: `Starting greeting template for ${name}`
},
ctx.mcpCtx.sessionId
);

return {
messages: [
{
Expand All @@ -327,7 +343,7 @@ const getServer = () => {
count: z.number().describe('Number of notifications to send (0 for 100)').default(50)
}
},
async ({ interval, count }, extra): Promise<CallToolResult> => {
async ({ interval, count }, ctx): Promise<CallToolResult> => {
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
let counter = 0;

Expand All @@ -339,7 +355,7 @@ const getServer = () => {
level: 'info',
data: `Periodic notification #${counter} at ${new Date().toISOString()}`
},
extra.sessionId
ctx.mcpCtx.sessionId
);
} catch (error) {
console.error('Error sending notification:', error);
Expand Down Expand Up @@ -389,7 +405,15 @@ const getServer = () => {
description: 'First example file for ResourceLink demonstration',
mimeType: 'text/plain'
},
async (): Promise<ReadResourceResult> => {
async (_, ctx): Promise<ReadResourceResult> => {
await ctx.loggingNotification.log(
{
level: 'debug',
data: `Starting example file 1`
},
ctx.mcpCtx.sessionId
);

return {
contents: [
{
Expand Down Expand Up @@ -484,10 +508,12 @@ const getServer = () => {
}
},
{
async createTask({ duration }, { taskStore, taskRequestedTtl }) {
async createTask({ duration }, ctx) {
// Create the task
if (!ctx.taskCtx?.store) throw new Error('Task store not found');
const taskStore = ctx.taskCtx.store;
const task = await taskStore.createTask({
ttl: taskRequestedTtl
ttl: ctx.taskCtx.requestedTtl
});

// Simulate out-of-band work
Expand All @@ -508,11 +534,13 @@ const getServer = () => {
task
};
},
async getTask(_args, { taskId, taskStore }) {
return await taskStore.getTask(taskId);
async getTask(_args, ctx) {
if (!ctx.taskCtx?.store) throw new Error('Task store not found');
return await ctx.taskCtx.store.getTask(ctx.taskCtx.id!);
},
async getTaskResult(_args, { taskId, taskStore }) {
const result = await taskStore.getTaskResult(taskId);
async getTaskResult(_args, ctx) {
if (!ctx.taskCtx?.store) throw new Error('Task store not found');
const result = await ctx.taskCtx.store.getTaskResult(ctx.taskCtx.id!);
return result as CallToolResult;
}
}
Expand Down
Loading
Loading