Skip to content

Commit f765e07

Browse files
committed
update context interface
1 parent 7767079 commit f765e07

31 files changed

+739
-770
lines changed

CLAUDE.md

Lines changed: 35 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -158,37 +158,40 @@ server.setRequestHandler(CallToolRequestSchema, async (request, ctx) => {
158158

159159
### Request Handler Context
160160

161-
The `ctx` parameter in handlers provides a structured context with three layers:
162-
163-
**`ctx.mcpCtx`** - MCP-level context:
164-
165-
- `requestId`: JSON-RPC message ID
166-
- `method`: The method being called
167-
- `_meta`: Request metadata
168-
- `sessionId`: Transport session identifier
169-
170-
**`ctx.requestCtx`** - Request-level context:
171-
172-
- `signal`: AbortSignal for cancellation
173-
- `authInfo`: Validated auth token info (if authenticated)
174-
- For server: `uri`, `headers`, `stream` (HTTP details)
175-
176-
**`ctx.taskCtx`** - Task context (when tasks are enabled):
177-
178-
- `id`: Current task ID (updates after `store.createTask()`)
179-
- `store`: Request-scoped task store (`RequestTaskStore`)
180-
- `requestedTtl`: Requested TTL for the task
181-
182-
**Context methods**:
183-
184-
- `ctx.sendNotification(notification)`: Send notification back
185-
- `ctx.sendRequest(request, schema)`: Send request (for bidirectional flows)
186-
187-
For server contexts, additional helpers:
188-
189-
- `ctx.loggingNotification(level, data, logger)`: Send logging notification
190-
- `ctx.requestSampling(params)`: Request sampling from client
191-
- `ctx.elicitInput(params)`: Request user input from client
161+
The `ctx` parameter in handlers provides a structured context with grouped fields:
162+
163+
**Common structure (both Client and Server)**:
164+
165+
- `ctx.sessionId`: Transport session identifier (top-level)
166+
- `ctx.mcpReq`: MCP protocol context
167+
- `id`: JSON-RPC message ID
168+
- `method`: The method being called
169+
- `_meta`: Request metadata
170+
- `signal`: AbortSignal for cancellation
171+
- `ctx.req`: Request context with authentication and send method
172+
- `authInfo`: Validated auth token info (if authenticated)
173+
- `send(request, schema, options?)`: Send request (for bidirectional flows)
174+
- `ctx.task`: Task context (when tasks are enabled)
175+
- `id`: Current task ID (updates after `store.createTask()`)
176+
- `store`: Request-scoped task store (`RequestTaskStore`)
177+
- `requestedTtl`: Requested TTL for the task
178+
- `ctx.notification`: Notification context
179+
- `send(notification)`: Send notification back
180+
181+
**Server-specific additions**:
182+
183+
- `ctx.req.raw`: Raw fetch Request object (access to URL, headers, etc.)
184+
- `ctx.notification`: Extended with logging methods
185+
- `log(params)`: Send logging notification
186+
- `debug(message, extraLogData?)`: Send debug log
187+
- `info(message, extraLogData?)`: Send info log
188+
- `warning(message, extraLogData?)`: Send warning log
189+
- `error(message, extraLogData?)`: Send error log
190+
- `ctx.stream`: SSE stream control
191+
- `closeSSE?()`: Close SSE stream for polling
192+
- `closeStandaloneSSE?()`: Close standalone SSE stream
193+
- `ctx.requestSampling(params, options?)`: Request sampling from client
194+
- `ctx.elicitInput(params, options?)`: Request user input from client
192195

193196
### Capability Checking
194197

@@ -231,7 +234,7 @@ client.setRequestHandler(CreateMessageRequestSchema, async (request, ctx) => {
231234

232235
```typescript
233236
server.setRequestHandler(SomeRequestSchema, async (request, ctx) => {
234-
// ctx provides mcpCtx, requestCtx, taskCtx, sendNotification, sendRequest
237+
// ctx provides mcpCtx, requestCtx, task, sendNotification, sendRequest
235238
return {
236239
/* result */
237240
};

examples/client/src/simpleStreamableHttp.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ async function connect(url?: string): Promise<void> {
276276
throw new McpError(ErrorCode.InvalidParams, `Unsupported elicitation mode: ${request.params.mode}`);
277277
}
278278

279-
console.log(`${ctx.mcpCtx.method} elicitation request received`);
279+
console.log(`${ctx.mcpReq.method} elicitation request received`);
280280
console.log('\n🔔 Elicitation (form) Request Received:');
281281
console.log(`Message: ${request.params.message}`);
282282
console.log(`Related Task: ${request.params._meta?.[RELATED_TASK_META_KEY]?.taskId}`);

examples/server/src/elicitationUrlExample.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ const getServer = () => {
5151
In a real world scenario, there would be some logic here to check if the user has the provided cartId.
5252
For the purposes of this example, we'll throw an error (-> elicits the client to open a URL to confirm payment)
5353
*/
54-
const sessionId = ctx.mcpCtx.sessionId;
54+
const sessionId = ctx.sessionId;
5555
if (!sessionId) {
5656
throw new Error('Expected a Session ID');
5757
}
@@ -87,7 +87,7 @@ const getServer = () => {
8787
If we don't, we can throw an ElicitationRequiredError to request the user to authenticate.
8888
For the purposes of this example, we'll throw an error (-> elicits the client to open a URL to authenticate).
8989
*/
90-
const sessionId = ctx.mcpCtx.sessionId;
90+
const sessionId = ctx.sessionId;
9191
if (!sessionId) {
9292
throw new Error('Expected a Session ID');
9393
}

examples/server/src/jsonResponseStreamableHttp.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ const getServer = () => {
5959
level: 'debug',
6060
data: `Starting multi-greet for ${name}`
6161
},
62-
ctx.mcpCtx.sessionId
62+
ctx.sessionId
6363
);
6464

6565
await sleep(1000); // Wait 1 second before first greeting
@@ -69,7 +69,7 @@ const getServer = () => {
6969
level: 'info',
7070
data: `Sending first greeting to ${name}`
7171
},
72-
ctx.mcpCtx.sessionId
72+
ctx.sessionId
7373
);
7474

7575
await sleep(1000); // Wait another second before second greeting
@@ -79,7 +79,7 @@ const getServer = () => {
7979
level: 'info',
8080
data: `Sending second greeting to ${name}`
8181
},
82-
ctx.mcpCtx.sessionId
82+
ctx.sessionId
8383
);
8484

8585
return {

examples/server/src/simpleStatelessStreamableHttp.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ const getServer = () => {
6161
level: 'info',
6262
data: `Periodic notification #${counter} at ${new Date().toISOString()}`
6363
},
64-
ctx.mcpCtx.sessionId
64+
ctx.sessionId
6565
);
6666
} catch (error) {
6767
console.error('Error sending notification:', error);

examples/server/src/simpleStreamableHttp.ts

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,12 @@ const getServer = () => {
6363
}
6464
},
6565
async ({ name }, ctx): Promise<CallToolResult> => {
66-
await ctx.loggingNotification.log(
66+
await ctx.notification.log(
6767
{
6868
level: 'debug',
6969
data: `Starting greet for ${name}`
7070
},
71-
ctx.mcpCtx.sessionId
71+
ctx.sessionId
7272
);
7373

7474
return {
@@ -104,7 +104,7 @@ const getServer = () => {
104104
level: 'debug',
105105
data: `Starting multi-greet for ${name}`
106106
},
107-
ctx.mcpCtx.sessionId
107+
ctx.sessionId
108108
);
109109

110110
await sleep(1000); // Wait 1 second before first greeting
@@ -114,7 +114,7 @@ const getServer = () => {
114114
level: 'info',
115115
data: `Sending first greeting to ${name}`
116116
},
117-
ctx.mcpCtx.sessionId
117+
ctx.sessionId
118118
);
119119

120120
await sleep(1000); // Wait another second before second greeting
@@ -124,7 +124,7 @@ const getServer = () => {
124124
level: 'info',
125125
data: `Sending second greeting to ${name}`
126126
},
127-
ctx.mcpCtx.sessionId
127+
ctx.sessionId
128128
);
129129

130130
return {
@@ -247,7 +247,7 @@ const getServer = () => {
247247

248248
try {
249249
// Use sendRequest through the ctx parameter to elicit input
250-
const result = await ctx.sendRequest(
250+
const result = await ctx.mcpReq.send(
251251
{
252252
method: 'elicitation/create',
253253
params: {
@@ -311,12 +311,12 @@ const getServer = () => {
311311
}
312312
},
313313
async ({ name }, ctx): Promise<GetPromptResult> => {
314-
await ctx.loggingNotification.log(
314+
await ctx.notification.log(
315315
{
316316
level: 'debug',
317317
data: `Starting greeting template for ${name}`
318318
},
319-
ctx.mcpCtx.sessionId
319+
ctx.sessionId
320320
);
321321

322322
return {
@@ -355,7 +355,7 @@ const getServer = () => {
355355
level: 'info',
356356
data: `Periodic notification #${counter} at ${new Date().toISOString()}`
357357
},
358-
ctx.mcpCtx.sessionId
358+
ctx.sessionId
359359
);
360360
} catch (error) {
361361
console.error('Error sending notification:', error);
@@ -406,12 +406,12 @@ const getServer = () => {
406406
mimeType: 'text/plain'
407407
},
408408
async (_, ctx): Promise<ReadResourceResult> => {
409-
await ctx.loggingNotification.log(
409+
await ctx.notification.log(
410410
{
411411
level: 'debug',
412412
data: `Starting example file 1`
413413
},
414-
ctx.mcpCtx.sessionId
414+
ctx.sessionId
415415
);
416416

417417
return {
@@ -510,10 +510,10 @@ const getServer = () => {
510510
{
511511
async createTask({ duration }, ctx) {
512512
// Create the task
513-
if (!ctx.taskCtx?.store) throw new Error('Task store not found');
514-
const taskStore = ctx.taskCtx.store;
513+
if (!ctx.task?.store) throw new Error('Task store not found');
514+
const taskStore = ctx.task.store;
515515
const task = await taskStore.createTask({
516-
ttl: ctx.taskCtx.requestedTtl
516+
ttl: ctx.task.requestedTtl
517517
});
518518

519519
// Simulate out-of-band work
@@ -535,12 +535,12 @@ const getServer = () => {
535535
};
536536
},
537537
async getTask(_args, ctx) {
538-
if (!ctx.taskCtx?.store) throw new Error('Task store not found');
539-
return await ctx.taskCtx.store.getTask(ctx.taskCtx.id!);
538+
if (!ctx.task?.store) throw new Error('Task store not found');
539+
return await ctx.task.store.getTask(ctx.task.id!);
540540
},
541541
async getTaskResult(_args, ctx) {
542-
if (!ctx.taskCtx?.store) throw new Error('Task store not found');
543-
const result = await ctx.taskCtx.store.getTaskResult(ctx.taskCtx.id!);
542+
if (!ctx.task?.store) throw new Error('Task store not found');
543+
const result = await ctx.task.store.getTaskResult(ctx.task.id!);
544544
return result as CallToolResult;
545545
}
546546
}

examples/server/src/simpleTaskInteractive.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -531,7 +531,7 @@ const createServer = (): Server => {
531531
pollInterval: taskParams.pollInterval ?? 1000
532532
};
533533

534-
const task = await taskStore.createTask(taskOptions, ctx.mcpCtx.requestId, request, ctx.mcpCtx.sessionId);
534+
const task = await taskStore.createTask(taskOptions, ctx.mcpReq.id, request, ctx.sessionId);
535535

536536
console.log(`\n[Server] ${name} called, task created: ${task.taskId}`);
537537

@@ -609,7 +609,7 @@ const createServer = (): Server => {
609609
activeTaskExecutions.set(task.taskId, {
610610
promise: taskExecution,
611611
server,
612-
sessionId: ctx.mcpCtx.sessionId ?? ''
612+
sessionId: ctx.sessionId ?? ''
613613
});
614614

615615
return { task };
@@ -629,7 +629,7 @@ const createServer = (): Server => {
629629
server.setRequestHandler(GetTaskPayloadRequestSchema, async (request, ctx): Promise<GetTaskPayloadResult> => {
630630
const { taskId } = request.params;
631631
console.log(`[Server] tasks/result called for task ${taskId}`);
632-
return taskResultHandler.handle(taskId, server, ctx.mcpCtx.sessionId ?? '');
632+
return taskResultHandler.handle(taskId, server, ctx.sessionId ?? '');
633633
});
634634

635635
return server;

examples/server/src/ssePollingExample.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { randomUUID } from 'node:crypto';
1616

1717
import { createMcpExpressApp } from '@modelcontextprotocol/express';
1818
import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
19-
import type { CallToolResult, ServerRequestContext } from '@modelcontextprotocol/server';
19+
import type { CallToolResult } from '@modelcontextprotocol/server';
2020
import { McpServer } from '@modelcontextprotocol/server';
2121
import cors from 'cors';
2222
import type { Request, Response } from 'express';
@@ -42,8 +42,7 @@ server.registerTool(
4242
},
4343
async (ctx): Promise<CallToolResult> => {
4444
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
45-
const sessionId = ctx.mcpCtx.sessionId;
46-
const requestCtx = ctx.requestCtx as ServerRequestContext;
45+
const sessionId = ctx.sessionId;
4746

4847
console.log(`[${sessionId}] Starting long-task...`);
4948

@@ -69,10 +68,10 @@ server.registerTool(
6968

7069
// Server decides to disconnect the client to free resources
7170
// Client will reconnect via GET with Last-Event-ID after the transport's retryInterval
72-
// Use requestCtx.stream.closeSSEStream callback - available when eventStore is configured
73-
if (requestCtx.stream.closeSSEStream) {
71+
// Use ctx.stream.closeSSE callback - available when eventStore is configured
72+
if (ctx.http?.closeSSE) {
7473
console.log(`[${sessionId}] Closing SSE stream to trigger client polling...`);
75-
requestCtx.stream.closeSSEStream();
74+
ctx.http.closeSSE();
7675
}
7776

7877
// Continue processing while client is disconnected

packages/client/src/client/client.ts

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import type {
2020
ListResourceTemplatesRequest,
2121
ListToolsRequest,
2222
LoggingLevel,
23-
McpContext,
2423
MessageExtraInfo,
2524
Notification,
2625
ProtocolOptions,
@@ -76,7 +75,7 @@ import {
7675
} from '@modelcontextprotocol/core';
7776

7877
import { ExperimentalClientTasks } from '../experimental/tasks/client.js';
79-
import type { ClientContextInterface, ClientRequestContext } from './context.js';
78+
import type { ClientContextInterface } from './context.js';
8079
import { ClientContext } from './context.js';
8180

8281
/**
@@ -505,17 +504,8 @@ export class Client<
505504
const { request, taskStore, relatedTaskId, taskCreationParams, abortController, capturedTransport, extra } = args;
506505
const sessionId = capturedTransport?.sessionId;
507506

508-
// Build the MCP context using the helper from Protocol
509-
const mcpContext: McpContext = this.buildMcpContext({ request, sessionId });
510-
511-
// Build the client request context (minimal, no HTTP details - client-specific)
512-
const requestCtx: ClientRequestContext = {
513-
signal: abortController.signal,
514-
authInfo: extra?.authInfo
515-
};
516-
517507
// Build the task context using the helper from Protocol
518-
const taskCtx: TaskContext | undefined = this.buildTaskContext({
508+
const task: TaskContext | undefined = this.buildTaskContext({
519509
taskStore,
520510
request,
521511
sessionId,
@@ -527,9 +517,19 @@ export class Client<
527517
return new ClientContext<RequestT, NotificationT, ResultT>({
528518
client: this,
529519
request,
530-
mcpContext,
531-
requestCtx,
532-
task: taskCtx
520+
sessionId,
521+
mcpReq: {
522+
id: request.id,
523+
method: request.method,
524+
_meta: request.params?._meta,
525+
signal: abortController.signal
526+
},
527+
http: extra?.authInfo
528+
? {
529+
authInfo: extra?.authInfo
530+
}
531+
: undefined,
532+
task: task
533533
});
534534
}
535535

0 commit comments

Comments
 (0)