Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 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
257 changes: 257 additions & 0 deletions src/server/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
import {
CreateMessageRequest,
CreateMessageResult,
ElicitRequest,
ElicitResult,
ElicitResultSchema,
JSONRPCRequest,
LoggingMessageNotification,
Notification,
Request,
RequestId,
RequestInfo,
RequestMeta,
Result,
ServerNotification,
ServerRequest
} from '../types.js';
import { RequestHandlerExtra, RequestOptions, RequestTaskStore } from '../shared/protocol.js';
import { Server } from './index.js';
import { AuthInfo } from './auth/types.js';
import { AnySchema, SchemaOutput } from './zod-compat.js';

export interface ContextInterface<RequestT extends Request = Request, NotificationT extends Notification = Notification>
extends RequestHandlerExtra<ServerRequest | RequestT, NotificationT | ServerNotification> {
elicit(params: ElicitRequest['params'], options?: RequestOptions): Promise<ElicitResult>;
Comment thread
KKonstantinov marked this conversation as resolved.
Outdated
requestSampling: (params: CreateMessageRequest['params'], options?: RequestOptions) => Promise<CreateMessageResult>;
log(params: LoggingMessageNotification['params'], sessionId?: string): Promise<void>;
debug(message: string, extraLogData?: Record<string, unknown>, sessionId?: string): Promise<void>;
info(message: string, extraLogData?: Record<string, unknown>, sessionId?: string): Promise<void>;
warning(message: string, extraLogData?: Record<string, unknown>, sessionId?: string): Promise<void>;
error(message: string, extraLogData?: Record<string, unknown>, sessionId?: string): Promise<void>;
}
/**
* A context object that is passed to request handlers.
*
* Implements the RequestHandlerExtra interface for backwards compatibility.
*/
export class Context<RequestT extends Request = Request, NotificationT extends Notification = Notification, ResultT extends Result = Result>
implements ContextInterface<RequestT, NotificationT>
{
private readonly server: Server<RequestT, NotificationT, ResultT>;

/**
* The request context.
* A type-safe context that is passed to request handlers.
*/
private readonly requestCtx: RequestHandlerExtra<ServerRequest | RequestT, ServerNotification | NotificationT>;

/**
* The MCP context - Contains information about the current MCP request and session.
*/
public readonly mcpContext: {
/**
* The JSON-RPC ID of the request being handled.
* This can be useful for tracking or logging purposes.
*/
requestId: RequestId;
/**
* The method of the request.
*/
method: string;
/**
* The metadata of the request.
*/
_meta?: RequestMeta;
/**
* The session ID of the request.
*/
sessionId?: string;
};

constructor(args: {
server: Server<RequestT, NotificationT, ResultT>;
request: JSONRPCRequest;
requestCtx: RequestHandlerExtra<ServerRequest | RequestT, ServerNotification | NotificationT>;
}) {
this.server = args.server;
this.requestCtx = args.requestCtx;
this.mcpContext = {
requestId: args.requestCtx.requestId,
method: args.request.method,
_meta: args.requestCtx._meta,
sessionId: args.requestCtx.sessionId
};
}

/**
* The JSON-RPC ID of the request being handled.
* This can be useful for tracking or logging purposes.
*
* @deprecated Use {@link mcpContext.requestId} instead.
*/
public get requestId(): RequestId {
return this.requestCtx.requestId;
}

public get signal(): AbortSignal {
return this.requestCtx.signal;
}

public get authInfo(): AuthInfo | undefined {
return this.requestCtx.authInfo;
}

public get requestInfo(): RequestInfo | undefined {
return this.requestCtx.requestInfo;
}

/**
* @deprecated Use {@link mcpContext._meta} instead.
*/
public get _meta(): RequestMeta | undefined {
return this.requestCtx._meta;
}

/**
* @deprecated Use {@link mcpContext.sessionId} instead.
*/
public get sessionId(): string | undefined {
return this.mcpContext.sessionId;
}

public get taskId(): string | undefined {
return this.requestCtx.taskId;
}

public get taskStore(): RequestTaskStore | undefined {
return this.requestCtx.taskStore;
}

public get taskRequestedTtl(): number | undefined {
return this.requestCtx.taskRequestedTtl ?? undefined;
}

public get closeSSEStream(): (() => void) | undefined {
return this.requestCtx.closeSSEStream;
}

public get closeStandaloneSSEStream(): (() => void) | undefined {
return this.requestCtx.closeStandaloneSSEStream;
}

/**
* Sends a notification that relates to the current request being handled.
*
* This is used by certain transports to correctly associate related messages.
*/
public sendNotification = (notification: NotificationT | ServerNotification): Promise<void> => {
return this.requestCtx.sendNotification(notification);
};

/**
* Sends a request that relates to the current request being handled.
*
* This is used by certain transports to correctly associate related messages.
*/
public sendRequest = <U extends AnySchema>(
request: RequestT | ServerRequest,
resultSchema: U,
options?: RequestOptions
): Promise<SchemaOutput<U>> => {
return this.requestCtx.sendRequest(request, resultSchema, { ...options, relatedRequestId: this.requestId });
};

/**
* Sends a request to sample an LLM via the client.
*/
public requestSampling(params: CreateMessageRequest['params'], options?: RequestOptions) {
return this.server.createMessage(params, options);
}

/**
* Sends an elicitation request to the client.
*/
public async elicit(params: ElicitRequest['params'], options?: RequestOptions): Promise<ElicitResult> {
const request: ElicitRequest = {
method: 'elicitation/create',
params
};
return await this.server.request(request, ElicitResultSchema, { ...options, relatedRequestId: this.requestId });
}

/**
* Sends a logging message.
*/
public async log(params: LoggingMessageNotification['params'], sessionId?: string) {
await this.server.sendLoggingMessage(params, sessionId);
}

/**
* Sends a debug log message.
*/
public async debug(message: string, extraLogData?: Record<string, unknown>, sessionId?: string) {
await this.log(
{
level: 'debug',
data: {
...extraLogData,
message
},
logger: 'server'
},
sessionId
);
}

/**
* Sends an info log message.
*/
public async info(message: string, extraLogData?: Record<string, unknown>, sessionId?: string) {
await this.log(
{
level: 'info',
data: {
...extraLogData,
message
},
logger: 'server'
},
sessionId
);
}

/**
* Sends a warning log message.
*/
public async warning(message: string, extraLogData?: Record<string, unknown>, sessionId?: string) {
await this.log(
{
level: 'warning',
data: {
...extraLogData,
message
},
logger: 'server'
},
sessionId
);
}

/**
* Sends an error log message.
*/
public async error(message: string, extraLogData?: Record<string, unknown>, sessionId?: string) {
await this.log(
{
level: 'error',
data: {
...extraLogData,
message
},
logger: 'server'
},
sessionId
);
}
}
64 changes: 59 additions & 5 deletions src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@ import {
type ToolUseContent,
CallToolRequestSchema,
CallToolResultSchema,
CreateTaskResultSchema
CreateTaskResultSchema,
JSONRPCRequest,
TaskCreationParams,
MessageExtraInfo
} from '../types.js';
import { AjvJsonSchemaValidator } from '../validation/ajv-provider.js';
import type { JsonSchemaType, jsonSchemaValidator } from '../validation/types.js';
Expand All @@ -56,6 +59,9 @@ import {
import { RequestHandlerExtra } from '../shared/protocol.js';
import { ExperimentalServerTasks } from '../experimental/tasks/server.js';
import { assertToolsCallTaskCapability, assertClientRequestTaskCapability } from '../experimental/tasks/helpers.js';
import { Context } from './context.js';
import { TaskStore } from '../experimental/index.js';
import { Transport } from '../shared/transport.js';

export type ServerOptions = ProtocolOptions & {
/**
Expand Down Expand Up @@ -219,9 +225,31 @@ export class Server<
requestSchema: T,
handler: (
request: SchemaOutput<T>,
extra: RequestHandlerExtra<ServerRequest | RequestT, ServerNotification | NotificationT>
extra: Context<RequestT, NotificationT, ResultT>
) => ServerResult | ResultT | Promise<ServerResult | ResultT>
): void {
// Wrap the handler to ensure the extra is a Context and return a decorated handler that can be passed to the base implementation

// Factory function to create a handler decorator that ensures the extra is a Context and returns a decorated handler that can be passed to the base implementation
const handlerDecoratorFactory = (
innerHandler: (
request: SchemaOutput<T>,
extra: Context<RequestT, NotificationT, ResultT>
) => ServerResult | ResultT | Promise<ServerResult | ResultT>
) => {
const decoratedHandler = (
request: SchemaOutput<T>,
extra: RequestHandlerExtra<ServerRequest | RequestT, ServerNotification | NotificationT>
) => {
if (!this.isContextExtra(extra)) {
throw new Error('Internal error: Expected Context for request handler extra');
}
return innerHandler(request, extra);
};

return decoratedHandler;
};

const shape = getObjectShape(requestSchema);
const methodSchema = shape?.method;
if (!methodSchema) {
Expand Down Expand Up @@ -259,7 +287,7 @@ export class Server<

const { params } = validatedRequest.data;

const result = await Promise.resolve(handler(request, extra));
const result = await Promise.resolve(handlerDecoratorFactory(handler)(request, extra));

// When task creation is requested, validate and return CreateTaskResult
if (params.task) {
Expand All @@ -286,11 +314,18 @@ export class Server<
};

// Install the wrapped handler
return super.setRequestHandler(requestSchema, wrappedHandler as unknown as typeof handler);
return super.setRequestHandler(requestSchema, handlerDecoratorFactory(wrappedHandler));
}

// Other handlers use default behavior
return super.setRequestHandler(requestSchema, handler);
return super.setRequestHandler(requestSchema, handlerDecoratorFactory(handler));
}

// Runtime type guard: ensure extra is our Context
private isContextExtra(
extra: RequestHandlerExtra<ServerRequest | RequestT, ServerNotification | NotificationT>
): extra is Context<RequestT, NotificationT, ResultT> {
return extra instanceof Context;
}

protected assertCapabilityForMethod(method: RequestT['method']): void {
Expand Down Expand Up @@ -468,6 +503,25 @@ export class Server<
return this._capabilities;
}

protected createRequestExtra(args: {
request: JSONRPCRequest;
taskStore: TaskStore | undefined;
relatedTaskId: string | undefined;
taskCreationParams: TaskCreationParams | undefined;
abortController: AbortController;
capturedTransport: Transport | undefined;
extra?: MessageExtraInfo;
}): RequestHandlerExtra<ServerRequest | RequestT, ServerNotification | NotificationT> {
const base = super.createRequestExtra(args) as RequestHandlerExtra<ServerRequest | RequestT, ServerNotification | NotificationT>;

// Expose a Context instance to handlers, which implements RequestHandlerExtra
return new Context<RequestT, NotificationT, ResultT>({
server: this,
request: args.request,
requestCtx: base
});
}

async ping() {
return this.request({ method: 'ping' }, EmptyResultSchema);
}
Expand Down
Loading
Loading