From 107767a39681c8a83709227dcfd7b59927e7e02d Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Sun, 7 Dec 2025 09:27:06 +0200 Subject: [PATCH 1/6] save commit --- src/server/context.ts | 222 +++++++++++++++++++++++++++++++++++ src/server/index.ts | 79 ++++++++++++- src/server/requestContext.ts | 109 +++++++++++++++++ src/shared/protocol.ts | 94 +++++++++------ 4 files changed, 462 insertions(+), 42 deletions(-) create mode 100644 src/server/context.ts create mode 100644 src/server/requestContext.ts diff --git a/src/server/context.ts b/src/server/context.ts new file mode 100644 index 000000000..f529e1ea0 --- /dev/null +++ b/src/server/context.ts @@ -0,0 +1,222 @@ +import { + CreateMessageRequest, + CreateMessageResult, + ElicitRequest, + ElicitResult, + ElicitResultSchema, + LoggingMessageNotification, + Notification, + Request, + RequestId, + RequestInfo, + RequestMeta, + Result, + ServerNotification, + ServerRequest, + ServerResult +} from '../types.js'; +import { RequestHandlerExtra, RequestOptions, RequestTaskStore } from '../shared/protocol.js'; +import { Server } from './index.js'; +import { RequestContext } from './requestContext.js'; +import { AuthInfo } from './auth/types.js'; +import { AnySchema, SchemaOutput } from './zod-compat.js'; + +export interface ContextInterface extends RequestHandlerExtra { + elicit(params: ElicitRequest['params'], options?: RequestOptions): Promise; + requestSampling: (params: CreateMessageRequest['params'], options?: RequestOptions) => Promise; + log(params: LoggingMessageNotification['params'], sessionId?: string): Promise; + debug(message: string, extraLogData?: Record, sessionId?: string): Promise; + info(message: string, extraLogData?: Record, sessionId?: string): Promise; + warning(message: string, extraLogData?: Record, sessionId?: string): Promise; + error(message: string, extraLogData?: Record, sessionId?: string): Promise; +} +/** + * 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 +{ + private readonly server: Server; + + /** + * The request context. + * A type-safe context that is passed to request handlers. + */ + public readonly requestCtx: RequestContext< + RequestT | ServerRequest, + NotificationT | ServerNotification, + ResultT | ServerResult + >; + + constructor(args: { + server: Server; + requestCtx: RequestContext; + }) { + this.server = args.server; + this.requestCtx = args.requestCtx; + } + + 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; + } + + public get _meta(): RequestMeta | undefined { + return this.requestCtx._meta; + } + + public get sessionId(): string | undefined { + return this.requestCtx.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 closeSSEStream = (): void => { + return this.requestCtx?.closeSSEStream(); + } + + public closeStandaloneSSEStream = (): void => { + 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 => { + 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 = ( + request: RequestT | ServerRequest, + resultSchema: U, + options?: RequestOptions + ): Promise> => { + 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 { + 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, 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, 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, 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, sessionId?: string) { + await this.log( + { + level: 'error', + data: { + ...extraLogData, + message + }, + logger: 'server' + }, + sessionId + ); + } +} \ No newline at end of file diff --git a/src/server/index.ts b/src/server/index.ts index aa1a62d00..19cb39c39 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -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'; @@ -56,6 +59,10 @@ 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'; +import { RequestContext } from './requestContext.js'; export type ServerOptions = ProtocolOptions & { /** @@ -219,9 +226,23 @@ export class Server< requestSchema: T, handler: ( request: SchemaOutput, - extra: RequestHandlerExtra + extra: Context ) => ServerResult | ResultT | Promise ): 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, extra: Context) => ServerResult | ResultT | Promise) => { + const decoratedHandler = (request: SchemaOutput, extra: RequestHandlerExtra) => { + 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) { @@ -259,7 +280,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) { @@ -286,11 +307,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 + ): extra is Context { + return extra instanceof Context; } protected assertCapabilityForMethod(method: RequestT['method']): void { @@ -468,6 +496,47 @@ 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 { + const base = super.createRequestExtra(args) as RequestHandlerExtra< + ServerRequest | RequestT, + ServerNotification | NotificationT + >; + + // Wrap base in Context to add server utilities while preserving shape + const requestCtx = new RequestContext< + ServerRequest | RequestT, + ServerNotification | NotificationT, + ServerResult | ResultT + >({ + signal: base.signal, + authInfo: base.authInfo, + requestInfo: base.requestInfo, + requestId: base.requestId, + _meta: base._meta, + sessionId: base.sessionId, + protocol: this, + closeSSEStream: base.closeSSEStream ?? undefined, + closeStandaloneSSEStream: base.closeStandaloneSSEStream ?? undefined + }); + + const ctx = new Context({ + server: this, + requestCtx + }); + + return ctx; + } + async ping() { return this.request({ method: 'ping' }, EmptyResultSchema); } diff --git a/src/server/requestContext.ts b/src/server/requestContext.ts new file mode 100644 index 000000000..d7be71986 --- /dev/null +++ b/src/server/requestContext.ts @@ -0,0 +1,109 @@ +import { AuthInfo } from './auth/types.js'; +import { Notification, Request, RequestId, RequestInfo, RequestMeta, Result } from '../types.js'; +import { Protocol, RequestHandlerExtra, RequestTaskStore, TaskRequestOptions } from '../shared/protocol.js'; +import { AnySchema, SchemaOutput } from './zod-compat.js'; + +/** + * A context object that is passed to request handlers. + * + * Implements the RequestHandlerExtra interface for backwards compatibility. + */ +export class RequestContext< + RequestT extends Request = Request, + NotificationT extends Notification = Notification, + ResultT extends Result = Result +> implements RequestHandlerExtra +{ + /** + * An abort signal used to communicate if the request was cancelled from the sender's side. + */ + public readonly signal: AbortSignal; + + /** + * Information about a validated access token, provided to request handlers. + */ + public readonly authInfo?: AuthInfo; + + /** + * The original HTTP request. + */ + public readonly requestInfo?: RequestInfo; + + /** + * The JSON-RPC ID of the request being handled. + * This can be useful for tracking or logging purposes. + */ + public readonly requestId: RequestId; + + /** + * Metadata from the original request. + */ + public readonly _meta?: RequestMeta; + + /** + * The session ID from the transport, if available. + */ + public readonly sessionId?: string; + + /** + * The task store, if available. + */ + public readonly taskStore?: RequestTaskStore; + + public readonly taskId?: string; + + public readonly taskRequestedTtl?: number | null; + + private readonly protocol: Protocol; + constructor(args: { + signal: AbortSignal; + authInfo?: AuthInfo; + requestInfo?: RequestInfo; + requestId: RequestId; + _meta?: RequestMeta; + sessionId?: string; + protocol: Protocol; + taskStore?: RequestTaskStore; + taskId?: string; + taskRequestedTtl?: number | null; + closeSSEStream: (() => void) | undefined; + closeStandaloneSSEStream: (() => void) | undefined; + }) { + this.signal = args.signal; + this.authInfo = args.authInfo; + this.requestInfo = args.requestInfo; + this.requestId = args.requestId; + this._meta = args._meta; + this.sessionId = args.sessionId; + this.protocol = args.protocol; + this.taskStore = args.taskStore; + this.taskId = args.taskId; + this.taskRequestedTtl = args.taskRequestedTtl; + } + + /** + * 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): Promise => { + return this.protocol.notification(notification, { relatedRequestId: this.requestId }); + }; + + /** + * Sends a request that relates to the current request being handled. + * + * This is used by certain transports to correctly associate related messages. + */ + public sendRequest = (request: RequestT, resultSchema: U, options?: TaskRequestOptions): Promise> => { + return this.protocol.request(request, resultSchema, { ...options, relatedRequestId: this.requestId }); + }; + + public closeSSEStream = (): void => { + return this.closeSSEStream(); + } + + public closeStandaloneSSEStream = (): void => { + return this.closeStandaloneSSEStream(); + } +} \ No newline at end of file diff --git a/src/shared/protocol.ts b/src/shared/protocol.ts index e195478f2..5ec1d0151 100644 --- a/src/shared/protocol.ts +++ b/src/shared/protocol.ts @@ -709,43 +709,7 @@ export abstract class Protocol = { - signal: abortController.signal, - sessionId: capturedTransport?.sessionId, - _meta: request.params?._meta, - sendNotification: async notification => { - // Include related-task metadata if this request is part of a task - const notificationOptions: NotificationOptions = { relatedRequestId: request.id }; - if (relatedTaskId) { - notificationOptions.relatedTask = { taskId: relatedTaskId }; - } - await this.notification(notification, notificationOptions); - }, - sendRequest: async (r, resultSchema, options?) => { - // Include related-task metadata if this request is part of a task - const requestOptions: RequestOptions = { ...options, relatedRequestId: request.id }; - if (relatedTaskId && !requestOptions.relatedTask) { - requestOptions.relatedTask = { taskId: relatedTaskId }; - } - - // Set task status to input_required when sending a request within a task context - // Use the taskId from options (explicit) or fall back to relatedTaskId (inherited) - const effectiveTaskId = requestOptions.relatedTask?.taskId ?? relatedTaskId; - if (effectiveTaskId && taskStore) { - await taskStore.updateTaskStatus(effectiveTaskId, 'input_required'); - } - - return await this.request(r, resultSchema, requestOptions); - }, - authInfo: extra?.authInfo, - requestId: request.id, - requestInfo: extra?.requestInfo, - taskId: relatedTaskId, - taskStore: taskStore, - taskRequestedTtl: taskCreationParams?.ttl, - closeSSEStream: extra?.closeSSEStream, - closeStandaloneSSEStream: extra?.closeStandaloneSSEStream - }; + const fullExtra: RequestHandlerExtra = this.createRequestExtra({ request, taskStore, relatedTaskId, taskCreationParams, abortController, capturedTransport, extra }); // Starting with Promise.resolve() puts any synchronous errors into the monad as well. Promise.resolve() @@ -823,6 +787,62 @@ export abstract class Protocol { + const { request, taskStore, relatedTaskId, taskCreationParams, abortController, capturedTransport, extra } = args; + + return { + signal: abortController.signal, + sessionId: capturedTransport?.sessionId, + _meta: request.params?._meta, + sendNotification: async notification => { + // Include related-task metadata if this request is part of a task + const notificationOptions: NotificationOptions = { relatedRequestId: request.id }; + if (relatedTaskId) { + notificationOptions.relatedTask = { taskId: relatedTaskId }; + } + await this.notification(notification, notificationOptions); + }, + sendRequest: async (r, resultSchema, options?) => { + // Include related-task metadata if this request is part of a task + const requestOptions: RequestOptions = { ...options, relatedRequestId: request.id }; + if (relatedTaskId && !requestOptions.relatedTask) { + requestOptions.relatedTask = { taskId: relatedTaskId }; + } + + // Set task status to input_required when sending a request within a task context + // Use the taskId from options (explicit) or fall back to relatedTaskId (inherited) + const effectiveTaskId = requestOptions.relatedTask?.taskId ?? relatedTaskId; + if (effectiveTaskId && taskStore) { + await taskStore.updateTaskStatus(effectiveTaskId, 'input_required'); + } + + return await this.request(r, resultSchema, requestOptions); + }, + authInfo: extra?.authInfo, + requestId: request.id, + requestInfo: extra?.requestInfo, + taskId: relatedTaskId, + taskStore: taskStore, + taskRequestedTtl: taskCreationParams?.ttl, + closeSSEStream: extra?.closeSSEStream, + closeStandaloneSSEStream: extra?.closeStandaloneSSEStream + } as RequestHandlerExtra; + } + private _onprogress(notification: ProgressNotification): void { const { progressToken, ...params } = notification.params; const messageId = Number(progressToken); From 68ff6650d8703efd8e59223c9ab8b67dc58892de Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Sun, 7 Dec 2025 10:24:29 +0200 Subject: [PATCH 2/6] context API - backwards compatible introduction --- src/server/context.ts | 81 ++++++--- src/server/index.ts | 69 +++---- src/server/mcp.ts | 17 +- src/server/requestContext.ts | 109 ------------ src/shared/protocol.ts | 32 ++-- test/server/context.test.ts | 277 +++++++++++++++++++++++++++++ test/server/mcp.test.ts | 30 ++-- test/server/streamableHttp.test.ts | 4 +- 8 files changed, 411 insertions(+), 208 deletions(-) delete mode 100644 src/server/requestContext.ts create mode 100644 test/server/context.test.ts diff --git a/src/server/context.ts b/src/server/context.ts index f529e1ea0..a90d3db09 100644 --- a/src/server/context.ts +++ b/src/server/context.ts @@ -4,6 +4,7 @@ import { ElicitRequest, ElicitResult, ElicitResultSchema, + JSONRPCRequest, LoggingMessageNotification, Notification, Request, @@ -12,16 +13,15 @@ import { RequestMeta, Result, ServerNotification, - ServerRequest, - ServerResult + ServerRequest } from '../types.js'; import { RequestHandlerExtra, RequestOptions, RequestTaskStore } from '../shared/protocol.js'; import { Server } from './index.js'; -import { RequestContext } from './requestContext.js'; import { AuthInfo } from './auth/types.js'; import { AnySchema, SchemaOutput } from './zod-compat.js'; -export interface ContextInterface extends RequestHandlerExtra { +export interface ContextInterface + extends RequestHandlerExtra { elicit(params: ElicitRequest['params'], options?: RequestOptions): Promise; requestSampling: (params: CreateMessageRequest['params'], options?: RequestOptions) => Promise; log(params: LoggingMessageNotification['params'], sessionId?: string): Promise; @@ -35,11 +35,8 @@ export interface ContextInterface implements ContextInterface +export class Context + implements ContextInterface { private readonly server: Server; @@ -47,20 +44,52 @@ export class Context< * The request context. * A type-safe context that is passed to request handlers. */ - public readonly requestCtx: RequestContext< - RequestT | ServerRequest, - NotificationT | ServerNotification, - ResultT | ServerResult - >; + private readonly requestCtx: RequestHandlerExtra; + + /** + * 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; - requestCtx: RequestContext; + request: JSONRPCRequest; + requestCtx: RequestHandlerExtra; }) { 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; } @@ -77,12 +106,18 @@ export class Context< 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.requestCtx.sessionId; + return this.mcpContext.sessionId; } public get taskId(): string | undefined { @@ -97,12 +132,12 @@ export class Context< return this.requestCtx.taskRequestedTtl ?? undefined; } - public closeSSEStream = (): void => { - return this.requestCtx?.closeSSEStream(); + public get closeSSEStream(): (() => void) | undefined { + return this.requestCtx.closeSSEStream; } - public closeStandaloneSSEStream = (): void => { - return this.requestCtx?.closeStandaloneSSEStream(); + public get closeStandaloneSSEStream(): (() => void) | undefined { + return this.requestCtx.closeStandaloneSSEStream; } /** @@ -111,7 +146,7 @@ export class Context< * This is used by certain transports to correctly associate related messages. */ public sendNotification = (notification: NotificationT | ServerNotification): Promise => { - return this.requestCtx.sendNotification(notification); + return this.server.notification(notification); }; /** @@ -124,7 +159,7 @@ export class Context< resultSchema: U, options?: RequestOptions ): Promise> => { - return this.requestCtx.sendRequest(request, resultSchema, { ...options, relatedRequestId: this.requestId }); + return this.server.request(request, resultSchema, { ...options, relatedRequestId: this.requestId }); }; /** @@ -219,4 +254,4 @@ export class Context< sessionId ); } -} \ No newline at end of file +} diff --git a/src/server/index.ts b/src/server/index.ts index 19cb39c39..fa0d038f1 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -62,7 +62,6 @@ import { assertToolsCallTaskCapability, assertClientRequestTaskCapability } from import { Context } from './context.js'; import { TaskStore } from '../experimental/index.js'; import { Transport } from '../shared/transport.js'; -import { RequestContext } from './requestContext.js'; export type ServerOptions = ProtocolOptions & { /** @@ -232,16 +231,24 @@ export class Server< // 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, extra: Context) => ServerResult | ResultT | Promise) => { - const decoratedHandler = (request: SchemaOutput, extra: RequestHandlerExtra) => { + const handlerDecoratorFactory = ( + innerHandler: ( + request: SchemaOutput, + extra: Context + ) => ServerResult | ResultT | Promise + ) => { + const decoratedHandler = ( + request: SchemaOutput, + extra: RequestHandlerExtra + ) => { 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; @@ -496,45 +503,23 @@ 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 { - const base = super.createRequestExtra(args) as RequestHandlerExtra< - ServerRequest | RequestT, - ServerNotification | NotificationT - >; - - // Wrap base in Context to add server utilities while preserving shape - const requestCtx = new RequestContext< - ServerRequest | RequestT, - ServerNotification | NotificationT, - ServerResult | ResultT - >({ - signal: base.signal, - authInfo: base.authInfo, - requestInfo: base.requestInfo, - requestId: base.requestId, - _meta: base._meta, - sessionId: base.sessionId, - protocol: this, - closeSSEStream: base.closeSSEStream ?? undefined, - closeStandaloneSSEStream: base.closeStandaloneSSEStream ?? undefined - }); - - const ctx = new Context({ + protected createRequestExtra(args: { + request: JSONRPCRequest; + taskStore: TaskStore | undefined; + relatedTaskId: string | undefined; + taskCreationParams: TaskCreationParams | undefined; + abortController: AbortController; + capturedTransport: Transport | undefined; + extra?: MessageExtraInfo; + }): RequestHandlerExtra { + const base = super.createRequestExtra(args) as RequestHandlerExtra; + + // Expose a Context instance to handlers, which implements RequestHandlerExtra + return new Context({ server: this, - requestCtx + request: args.request, + requestCtx: base }); - - return ctx; } async ping() { diff --git a/src/server/mcp.ts b/src/server/mcp.ts index 7e61b4364..ba7024df9 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -63,6 +63,7 @@ import { validateAndWarnToolName } from '../shared/toolNameValidation.js'; import { ExperimentalMcpServerTasks } from '../experimental/tasks/mcp-server.js'; import type { ToolTaskHandler } from '../experimental/tasks/interfaces.js'; import { ZodOptional } from 'zod'; +import { ContextInterface } from './context.js'; /** * High-level MCP server that provides a simpler API for working with resources, tools, and prompts. @@ -324,7 +325,7 @@ export class McpServer { private async executeToolHandler( tool: RegisteredTool, args: unknown, - extra: RequestHandlerExtra + extra: ContextInterface ): Promise { const handler = tool.handler as AnyToolHandler; const isTaskHandler = 'createTask' in handler; @@ -1270,7 +1271,7 @@ export class ResourceTemplate { export type BaseToolCallback< SendResultT extends Result, - Extra extends RequestHandlerExtra, + Extra extends ContextInterface, Args extends undefined | ZodRawShapeCompat | AnySchema > = Args extends ZodRawShapeCompat ? (args: ShapeOutput, extra: Extra) => SendResultT | Promise @@ -1290,7 +1291,7 @@ export type BaseToolCallback< */ export type ToolCallback = BaseToolCallback< CallToolResult, - RequestHandlerExtra, + ContextInterface, Args >; @@ -1409,7 +1410,7 @@ export type ResourceMetadata = Omit; * Callback to list all resources matching a given template. */ export type ListResourcesCallback = ( - extra: RequestHandlerExtra + extra: ContextInterface ) => ListResourcesResult | Promise; /** @@ -1417,7 +1418,7 @@ export type ListResourcesCallback = ( */ export type ReadResourceCallback = ( uri: URL, - extra: RequestHandlerExtra + extra: ContextInterface ) => ReadResourceResult | Promise; export type RegisteredResource = { @@ -1445,7 +1446,7 @@ export type RegisteredResource = { export type ReadResourceTemplateCallback = ( uri: URL, variables: Variables, - extra: RequestHandlerExtra + extra: ContextInterface ) => ReadResourceResult | Promise; export type RegisteredResourceTemplate = { @@ -1470,8 +1471,8 @@ export type RegisteredResourceTemplate = { type PromptArgsRawShape = ZodRawShapeCompat; export type PromptCallback = Args extends PromptArgsRawShape - ? (args: ShapeOutput, extra: RequestHandlerExtra) => GetPromptResult | Promise - : (extra: RequestHandlerExtra) => GetPromptResult | Promise; + ? (args: ShapeOutput, extra: ContextInterface) => GetPromptResult | Promise + : (extra: ContextInterface) => GetPromptResult | Promise; export type RegisteredPrompt = { title?: string; diff --git a/src/server/requestContext.ts b/src/server/requestContext.ts deleted file mode 100644 index d7be71986..000000000 --- a/src/server/requestContext.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { AuthInfo } from './auth/types.js'; -import { Notification, Request, RequestId, RequestInfo, RequestMeta, Result } from '../types.js'; -import { Protocol, RequestHandlerExtra, RequestTaskStore, TaskRequestOptions } from '../shared/protocol.js'; -import { AnySchema, SchemaOutput } from './zod-compat.js'; - -/** - * A context object that is passed to request handlers. - * - * Implements the RequestHandlerExtra interface for backwards compatibility. - */ -export class RequestContext< - RequestT extends Request = Request, - NotificationT extends Notification = Notification, - ResultT extends Result = Result -> implements RequestHandlerExtra -{ - /** - * An abort signal used to communicate if the request was cancelled from the sender's side. - */ - public readonly signal: AbortSignal; - - /** - * Information about a validated access token, provided to request handlers. - */ - public readonly authInfo?: AuthInfo; - - /** - * The original HTTP request. - */ - public readonly requestInfo?: RequestInfo; - - /** - * The JSON-RPC ID of the request being handled. - * This can be useful for tracking or logging purposes. - */ - public readonly requestId: RequestId; - - /** - * Metadata from the original request. - */ - public readonly _meta?: RequestMeta; - - /** - * The session ID from the transport, if available. - */ - public readonly sessionId?: string; - - /** - * The task store, if available. - */ - public readonly taskStore?: RequestTaskStore; - - public readonly taskId?: string; - - public readonly taskRequestedTtl?: number | null; - - private readonly protocol: Protocol; - constructor(args: { - signal: AbortSignal; - authInfo?: AuthInfo; - requestInfo?: RequestInfo; - requestId: RequestId; - _meta?: RequestMeta; - sessionId?: string; - protocol: Protocol; - taskStore?: RequestTaskStore; - taskId?: string; - taskRequestedTtl?: number | null; - closeSSEStream: (() => void) | undefined; - closeStandaloneSSEStream: (() => void) | undefined; - }) { - this.signal = args.signal; - this.authInfo = args.authInfo; - this.requestInfo = args.requestInfo; - this.requestId = args.requestId; - this._meta = args._meta; - this.sessionId = args.sessionId; - this.protocol = args.protocol; - this.taskStore = args.taskStore; - this.taskId = args.taskId; - this.taskRequestedTtl = args.taskRequestedTtl; - } - - /** - * 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): Promise => { - return this.protocol.notification(notification, { relatedRequestId: this.requestId }); - }; - - /** - * Sends a request that relates to the current request being handled. - * - * This is used by certain transports to correctly associate related messages. - */ - public sendRequest = (request: RequestT, resultSchema: U, options?: TaskRequestOptions): Promise> => { - return this.protocol.request(request, resultSchema, { ...options, relatedRequestId: this.requestId }); - }; - - public closeSSEStream = (): void => { - return this.closeSSEStream(); - } - - public closeStandaloneSSEStream = (): void => { - return this.closeStandaloneSSEStream(); - } -} \ No newline at end of file diff --git a/src/shared/protocol.ts b/src/shared/protocol.ts index 5ec1d0151..87d70b10d 100644 --- a/src/shared/protocol.ts +++ b/src/shared/protocol.ts @@ -231,6 +231,8 @@ export interface RequestTaskStore { /** * Extra data given to request handlers. + * + * @deprecated Use {@link ContextInterface} from {@link Context} instead. Future major versions will remove this type. */ export type RequestHandlerExtra = { /** @@ -709,7 +711,15 @@ export abstract class Protocol = this.createRequestExtra({ request, taskStore, relatedTaskId, taskCreationParams, abortController, capturedTransport, extra }); + const fullExtra: RequestHandlerExtra = this.createRequestExtra({ + request, + taskStore, + relatedTaskId, + taskCreationParams, + abortController, + capturedTransport, + extra + }); // Starting with Promise.resolve() puts any synchronous errors into the monad as well. Promise.resolve() @@ -791,17 +801,15 @@ export abstract class Protocol { + protected createRequestExtra(args: { + request: JSONRPCRequest; + taskStore: TaskStore | undefined; + relatedTaskId: string | undefined; + taskCreationParams: TaskCreationParams | undefined; + abortController: AbortController; + capturedTransport: Transport | undefined; + extra?: MessageExtraInfo; + }): RequestHandlerExtra { const { request, taskStore, relatedTaskId, taskCreationParams, abortController, capturedTransport, extra } = args; return { diff --git a/test/server/context.test.ts b/test/server/context.test.ts new file mode 100644 index 000000000..8c9518ae8 --- /dev/null +++ b/test/server/context.test.ts @@ -0,0 +1,277 @@ +import { z } from 'zod/v4'; +import { Client } from '../../src/client/index.js'; +import { McpServer, ResourceTemplate } from '../../src/server/mcp.js'; +import { Context } from '../../src/server/context.js'; +import { + CallToolResultSchema, + GetPromptResultSchema, + ListResourcesResultSchema, + LoggingMessageNotificationSchema, + ReadResourceResultSchema, + ServerNotification, + ServerRequest +} from '../../src/types.js'; +import { InMemoryTransport } from '../../src/inMemory.js'; +import { RequestHandlerExtra } from '../../src/shared/protocol.js'; +import { ShapeOutput, ZodRawShapeCompat } from '../../src/server/zod-compat.js'; + +describe('Context', () => { + /*** + * Test: `extra` provided to callbacks is Context (parameterized) + */ + type Seen = { isContext: boolean; hasRequestId: boolean }; + const contextCases: Array<[string, (mcpServer: McpServer, seen: Seen) => void | Promise, (client: Client) => Promise]> = + [ + [ + 'tool', + (mcpServer, seen) => { + mcpServer.registerTool( + 'ctx-tool', + { + inputSchema: z.object({ name: z.string() }) + }, + (_args: { name: string }, extra) => { + seen.isContext = extra instanceof Context; + seen.hasRequestId = !!extra.requestId; + return { content: [{ type: 'text', text: 'ok' }] }; + } + ); + }, + client => + client.request( + { + method: 'tools/call', + params: { + name: 'ctx-tool', + arguments: { + name: 'ctx-tool-name' + } + } + }, + CallToolResultSchema + ) + ], + [ + 'resource', + (mcpServer, seen) => { + mcpServer.registerResource('ctx-resource', 'test://res/1', { title: 'ctx-resource' }, async (_uri, extra) => { + seen.isContext = extra instanceof Context; + seen.hasRequestId = !!extra.requestId; + return { contents: [{ uri: 'test://res/1', mimeType: 'text/plain', text: 'hello' }] }; + }); + }, + client => client.request({ method: 'resources/read', params: { uri: 'test://res/1' } }, ReadResourceResultSchema) + ], + [ + 'resource template list', + (mcpServer, seen) => { + const template = new ResourceTemplate('test://items/{id}', { + list: async extra => { + seen.isContext = extra instanceof Context; + seen.hasRequestId = !!extra.requestId; + return { resources: [] }; + } + }); + mcpServer.registerResource('ctx-template', template, { title: 'ctx-template' }, async (_uri, _vars, _extra) => ({ + contents: [] + })); + }, + client => client.request({ method: 'resources/list', params: {} }, ListResourcesResultSchema) + ], + [ + 'prompt', + (mcpServer, seen) => { + mcpServer.registerPrompt('ctx-prompt', {}, async extra => { + seen.isContext = extra instanceof Context; + seen.hasRequestId = !!extra.requestId; + return { messages: [] }; + }); + }, + client => client.request({ method: 'prompts/get', params: { name: 'ctx-prompt', arguments: {} } }, GetPromptResultSchema) + ] + ]; + + test.each(contextCases)('should pass Context as extra to %s callbacks', async (_kind, register, trigger) => { + const mcpServer = new McpServer({ name: 'ctx-test', version: '1.0' }); + const client = new Client({ name: 'ctx-client', version: '1.0' }); + + const seen: Seen = { isContext: false, hasRequestId: false }; + + await register(mcpServer, seen); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + await Promise.all([client.connect(clientTransport), mcpServer.server.connect(serverTransport)]); + + await trigger(client); + + expect(seen.isContext).toBe(true); + expect(seen.hasRequestId).toBe(true); + }); + + const logLevelsThroughContext = ['debug', 'info', 'warning', 'error'] as const; + + //it.each for each log level, test that logging message is sent to client + it.each(logLevelsThroughContext)('should send logging message to client for %s level from Context', async level => { + const mcpServer = new McpServer( + { name: 'ctx-test', version: '1.0' }, + { + capabilities: { + logging: {} + } + } + ); + const client = new Client( + { name: 'ctx-client', version: '1.0' }, + { + capabilities: {} + } + ); + + let seen = 0; + + client.setNotificationHandler(LoggingMessageNotificationSchema, notification => { + seen++; + expect(notification.params.level).toBe(level); + expect(notification.params.data).toBe('Test message'); + expect(notification.params.test).toBe('test'); + expect(notification.params.sessionId).toBe('sample-session-id'); + return; + }); + + mcpServer.registerTool('ctx-log-test', { inputSchema: z.object({ name: z.string() }) }, async (_args: { name: string }, extra) => { + await extra[level]('Test message', { test: 'test' }, 'sample-session-id'); + await extra.log( + { + level, + data: 'Test message', + logger: 'test-logger-namespace' + }, + 'sample-session-id' + ); + return { content: [{ type: 'text', text: 'ok' }] }; + }); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + await Promise.all([client.connect(clientTransport), mcpServer.server.connect(serverTransport)]); + + const result = await client.request( + { + method: 'tools/call', + params: { name: 'ctx-log-test', arguments: { name: 'ctx-log-test-name' } } + }, + CallToolResultSchema + ); + + // two messages should have been sent - one from the .log method and one from the .debug/info/warning/error method + expect(seen).toBe(2); + + expect(result.content).toHaveLength(1); + expect(result.content[0]).toMatchObject({ + type: 'text', + text: 'ok' + }); + }); + describe('Legacy RequestHandlerExtra API', () => { + const contextCases: Array< + [string, (mcpServer: McpServer, seen: Seen) => void | Promise, (client: Client) => Promise] + > = [ + [ + 'tool', + (mcpServer, seen) => { + mcpServer.registerTool( + 'ctx-tool', + { + inputSchema: z.object({ name: z.string() }) + }, + // The test is to ensure that the extra is compatible with the RequestHandlerExtra type + (_args: { name: string }, extra: RequestHandlerExtra) => { + seen.isContext = extra instanceof Context; + seen.hasRequestId = !!extra.requestId; + return { content: [{ type: 'text', text: 'ok' }] }; + } + ); + }, + client => + client.request( + { + method: 'tools/call', + params: { + name: 'ctx-tool', + arguments: { + name: 'ctx-tool-name' + } + } + }, + CallToolResultSchema + ) + ], + [ + 'resource', + (mcpServer, seen) => { + // The test is to ensure that the extra is compatible with the RequestHandlerExtra type + mcpServer.registerResource( + 'ctx-resource', + 'test://res/1', + { title: 'ctx-resource' }, + async (_uri, extra: RequestHandlerExtra) => { + seen.isContext = extra instanceof Context; + seen.hasRequestId = !!extra.requestId; + return { contents: [{ uri: 'test://res/1', mimeType: 'text/plain', text: 'hello' }] }; + } + ); + }, + client => client.request({ method: 'resources/read', params: { uri: 'test://res/1' } }, ReadResourceResultSchema) + ], + [ + 'resource template list', + (mcpServer, seen) => { + // The test is to ensure that the extra is compatible with the RequestHandlerExtra type + const template = new ResourceTemplate('test://items/{id}', { + list: async (extra: RequestHandlerExtra) => { + seen.isContext = extra instanceof Context; + seen.hasRequestId = !!extra.requestId; + return { resources: [] }; + } + }); + mcpServer.registerResource('ctx-template', template, { title: 'ctx-template' }, async (_uri, _vars, _extra) => ({ + contents: [] + })); + }, + client => client.request({ method: 'resources/list', params: {} }, ListResourcesResultSchema) + ], + [ + 'prompt', + (mcpServer, seen) => { + // The test is to ensure that the extra is compatible with the RequestHandlerExtra type + mcpServer.registerPrompt( + 'ctx-prompt', + {}, + async (args: ShapeOutput, extra: RequestHandlerExtra) => { + seen.isContext = extra instanceof Context; + seen.hasRequestId = !!extra.requestId; + return { messages: [] }; + } + ); + }, + client => client.request({ method: 'prompts/get', params: { name: 'ctx-prompt', arguments: {} } }, GetPromptResultSchema) + ] + ]; + + test.each(contextCases)('should pass Context as extra to %s callbacks', async (_kind, register, trigger) => { + const mcpServer = new McpServer({ name: 'ctx-test', version: '1.0' }); + const client = new Client({ name: 'ctx-client', version: '1.0' }); + + const seen: Seen = { isContext: false, hasRequestId: false }; + + await register(mcpServer, seen); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + await Promise.all([client.connect(clientTransport), mcpServer.server.connect(serverTransport)]); + + await trigger(client); + + expect(seen.isContext).toBe(true); + expect(seen.hasRequestId).toBe(true); + }); + }); +}); diff --git a/test/server/mcp.test.ts b/test/server/mcp.test.ts index f6c2124e1..4be2d24f4 100644 --- a/test/server/mcp.test.ts +++ b/test/server/mcp.test.ts @@ -17,12 +17,15 @@ import { ReadResourceResultSchema, type TextContent, UrlElicitationRequiredError, - ErrorCode + ErrorCode, + ServerRequest, + ServerNotification } from '../../src/types.js'; import { completable } from '../../src/server/completable.js'; import { McpServer, ResourceTemplate } from '../../src/server/mcp.js'; import { InMemoryTaskStore } from '../../src/experimental/tasks/stores/in-memory.js'; import { zodTestMatrix, type ZodMatrixEntry } from '../../src/__fixtures__/zodTestMatrix.js'; +import { Context, ContextInterface } from '../../src/server/context.js'; function createLatch() { let latch = false; @@ -243,7 +246,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { sendNotification: () => { throw new Error('Not implemented'); } - }); + } as unknown as ContextInterface); expect(result?.resources).toHaveLength(1); expect(list).toHaveBeenCalled(); }); @@ -4387,17 +4390,20 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { }) } }, - async ({ department, name }) => ({ - messages: [ - { - role: 'assistant', - content: { - type: 'text', - text: `Hello ${name}, welcome to the ${department} team!` + async ({ department, name }, extra: ContextInterface) => { + expect(extra).toBeInstanceOf(Context); + return { + messages: [ + { + role: 'assistant', + content: { + type: 'text', + text: `Hello ${name}, welcome to the ${department} team!` + } } - } - ] - }) + ] + }; + } ); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); diff --git a/test/server/streamableHttp.test.ts b/test/server/streamableHttp.test.ts index 8d94b272e..be5908f60 100644 --- a/test/server/streamableHttp.test.ts +++ b/test/server/streamableHttp.test.ts @@ -2280,8 +2280,8 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { // Verify we received the notification that was sent while disconnected expect(allText).toContain('Missed while disconnected'); - }); - }, 10000); + }, 10000); + }); // Test onsessionclosed callback describe('StreamableHTTPServerTransport onsessionclosed callback', () => { From f58b491fb6b15f5d170fb54ec0c296ca6d6cd7b4 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Sun, 7 Dec 2025 10:39:36 +0200 Subject: [PATCH 3/6] fixes --- src/server/context.ts | 4 ++-- src/server/mcp.ts | 2 +- test/server/context.test.ts | 15 +++++---------- 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/src/server/context.ts b/src/server/context.ts index a90d3db09..ee6a97f70 100644 --- a/src/server/context.ts +++ b/src/server/context.ts @@ -146,7 +146,7 @@ export class Context => { - return this.server.notification(notification); + return this.requestCtx.sendNotification(notification); }; /** @@ -159,7 +159,7 @@ export class Context> => { - return this.server.request(request, resultSchema, { ...options, relatedRequestId: this.requestId }); + return this.requestCtx.sendRequest(request, resultSchema, { ...options, relatedRequestId: this.requestId }); }; /** diff --git a/src/server/mcp.ts b/src/server/mcp.ts index ba7024df9..6d4e1a5ef 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -1136,7 +1136,7 @@ export class McpServer { /** * Registers a prompt with a config object and callback. */ - registerPrompt( + registerPrompt( name: string, config: { title?: string; diff --git a/test/server/context.test.ts b/test/server/context.test.ts index 8c9518ae8..eaa9374c7 100644 --- a/test/server/context.test.ts +++ b/test/server/context.test.ts @@ -13,7 +13,6 @@ import { } from '../../src/types.js'; import { InMemoryTransport } from '../../src/inMemory.js'; import { RequestHandlerExtra } from '../../src/shared/protocol.js'; -import { ShapeOutput, ZodRawShapeCompat } from '../../src/server/zod-compat.js'; describe('Context', () => { /*** @@ -243,15 +242,11 @@ describe('Context', () => { 'prompt', (mcpServer, seen) => { // The test is to ensure that the extra is compatible with the RequestHandlerExtra type - mcpServer.registerPrompt( - 'ctx-prompt', - {}, - async (args: ShapeOutput, extra: RequestHandlerExtra) => { - seen.isContext = extra instanceof Context; - seen.hasRequestId = !!extra.requestId; - return { messages: [] }; - } - ); + mcpServer.registerPrompt('ctx-prompt', {}, async (extra: RequestHandlerExtra) => { + seen.isContext = extra instanceof Context; + seen.hasRequestId = !!extra.requestId; + return { messages: [] }; + }); }, client => client.request({ method: 'prompts/get', params: { name: 'ctx-prompt', arguments: {} } }, GetPromptResultSchema) ] From e89d9d4c4b1c3dba226934ec0dcd320c8bde3619 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Mon, 8 Dec 2025 20:42:21 +0200 Subject: [PATCH 4/6] moved properties under objects --- src/server/context.ts | 240 ++++++++++++++++++++++++------------ test/server/context.test.ts | 4 +- 2 files changed, 162 insertions(+), 82 deletions(-) diff --git a/src/server/context.ts b/src/server/context.ts index ee6a97f70..0a541ac21 100644 --- a/src/server/context.ts +++ b/src/server/context.ts @@ -20,16 +20,118 @@ import { Server } from './index.js'; import { AuthInfo } from './auth/types.js'; import { AnySchema, SchemaOutput } from './zod-compat.js'; -export interface ContextInterface - extends RequestHandlerExtra { - elicit(params: ElicitRequest['params'], options?: RequestOptions): Promise; - requestSampling: (params: CreateMessageRequest['params'], options?: RequestOptions) => Promise; +/** + * Interface for sending logging messages to the client via {@link LoggingMessageNotification}. + */ +export interface LoggingMessageSenderInterface { + /** + * Sends a logging message to the client. + */ log(params: LoggingMessageNotification['params'], sessionId?: string): Promise; + /** + * Sends a debug log message to the client. + */ debug(message: string, extraLogData?: Record, sessionId?: string): Promise; + /** + * Sends an info log message to the client. + */ info(message: string, extraLogData?: Record, sessionId?: string): Promise; + /** + * Sends a warning log message to the client. + */ warning(message: string, extraLogData?: Record, sessionId?: string): Promise; + /** + * Sends an error log message to the client. + */ error(message: string, extraLogData?: Record, sessionId?: string): Promise; } + +export class ServerLogger implements LoggingMessageSenderInterface { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + constructor(private readonly server: Server) {} + + /** + * 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, 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, 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, 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, sessionId?: string) { + await this.log( + { + level: 'error', + data: { + ...extraLogData, + message + }, + logger: 'server' + }, + sessionId + ); + } +} + +export interface ContextInterface + extends RequestHandlerExtra { + elicitInput(params: ElicitRequest['params'], options?: RequestOptions): Promise; + requestSampling: (params: CreateMessageRequest['params'], options?: RequestOptions) => Promise; + logger: LoggingMessageSenderInterface; +} /** * A context object that is passed to request handlers. * @@ -69,6 +171,31 @@ export class Context void) | undefined; + /** + * Closes the standalone GET SSE stream, triggering client reconnection. + * Only available when using StreamableHTTPServerTransport with eventStore configured. + * Use this to implement polling behavior for server-initiated notifications. + */ + closeStandaloneSSEStream: (() => void) | undefined; + } | undefined; + + public readonly logger: LoggingMessageSenderInterface; + constructor(args: { server: Server; request: JSONRPCRequest; @@ -82,6 +209,19 @@ export class Context void) | undefined { return this.requestCtx.closeSSEStream; } + /** + * @deprecated Use {@link stream.closeStandaloneSSEStream} instead. + */ public get closeStandaloneSSEStream(): (() => void) | undefined { return this.requestCtx.closeStandaloneSSEStream; } @@ -172,86 +327,11 @@ export class Context { + public async elicitInput(params: ElicitRequest['params'], options?: RequestOptions): Promise { 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, 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, 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, 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, sessionId?: string) { - await this.log( - { - level: 'error', - data: { - ...extraLogData, - message - }, - logger: 'server' - }, - sessionId - ); - } } diff --git a/test/server/context.test.ts b/test/server/context.test.ts index eaa9374c7..0f1190df0 100644 --- a/test/server/context.test.ts +++ b/test/server/context.test.ts @@ -138,8 +138,8 @@ describe('Context', () => { }); mcpServer.registerTool('ctx-log-test', { inputSchema: z.object({ name: z.string() }) }, async (_args: { name: string }, extra) => { - await extra[level]('Test message', { test: 'test' }, 'sample-session-id'); - await extra.log( + await extra.logger[level]('Test message', { test: 'test' }, 'sample-session-id'); + await extra.logger.log( { level, data: 'Test message', From 187a3cd13fccd51e8edb4aafd85f9a8860af1a57 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Mon, 8 Dec 2025 20:44:05 +0200 Subject: [PATCH 5/6] prettier fix --- src/server/context.ts | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/server/context.ts b/src/server/context.ts index 0a541ac21..4c6e858b8 100644 --- a/src/server/context.ts +++ b/src/server/context.ts @@ -179,20 +179,22 @@ export class Context void) | undefined; - /** - * Closes the standalone GET SSE stream, triggering client reconnection. - * Only available when using StreamableHTTPServerTransport with eventStore configured. - * Use this to implement polling behavior for server-initiated notifications. - */ - closeStandaloneSSEStream: (() => void) | undefined; - } | undefined; + public readonly stream: + | { + /** + * Closes the SSE stream for this request, triggering client reconnection. + * Only available when using StreamableHTTPServerTransport with eventStore configured. + * Use this to implement polling behavior during long-running operations. + */ + closeSSEStream: (() => void) | undefined; + /** + * Closes the standalone GET SSE stream, triggering client reconnection. + * Only available when using StreamableHTTPServerTransport with eventStore configured. + * Use this to implement polling behavior for server-initiated notifications. + */ + closeStandaloneSSEStream: (() => void) | undefined; + } + | undefined; public readonly logger: LoggingMessageSenderInterface; From 96169b33dcb8954f1997e0cd7e75d98af82ac3fb Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Mon, 8 Dec 2025 20:48:34 +0200 Subject: [PATCH 6/6] move logger methods under loggingNotification --- src/server/context.ts | 10 +++++----- test/server/context.test.ts | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/server/context.ts b/src/server/context.ts index 4c6e858b8..91b075615 100644 --- a/src/server/context.ts +++ b/src/server/context.ts @@ -23,7 +23,7 @@ import { AnySchema, SchemaOutput } from './zod-compat.js'; /** * Interface for sending logging messages to the client via {@link LoggingMessageNotification}. */ -export interface LoggingMessageSenderInterface { +export interface LoggingMessageNotificationSenderInterface { /** * Sends a logging message to the client. */ @@ -46,7 +46,7 @@ export interface LoggingMessageSenderInterface { error(message: string, extraLogData?: Record, sessionId?: string): Promise; } -export class ServerLogger implements LoggingMessageSenderInterface { +export class ServerLogger implements LoggingMessageNotificationSenderInterface { // eslint-disable-next-line @typescript-eslint/no-explicit-any constructor(private readonly server: Server) {} @@ -130,7 +130,7 @@ export interface ContextInterface { elicitInput(params: ElicitRequest['params'], options?: RequestOptions): Promise; requestSampling: (params: CreateMessageRequest['params'], options?: RequestOptions) => Promise; - logger: LoggingMessageSenderInterface; + loggingNotification: LoggingMessageNotificationSenderInterface; } /** * A context object that is passed to request handlers. @@ -196,7 +196,7 @@ export class Context; @@ -218,7 +218,7 @@ export class Context { }); mcpServer.registerTool('ctx-log-test', { inputSchema: z.object({ name: z.string() }) }, async (_args: { name: string }, extra) => { - await extra.logger[level]('Test message', { test: 'test' }, 'sample-session-id'); - await extra.logger.log( + await extra.loggingNotification[level]('Test message', { test: 'test' }, 'sample-session-id'); + await extra.loggingNotification.log( { level, data: 'Test message',