From 59786e045e8ceac027deea60cac98e81de8379ca Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Mon, 6 Oct 2025 16:56:58 +0300 Subject: [PATCH 1/5] draft: request context utility - utility methods, lifespan context as per python sdk --- src/server/context.ts | 204 ++++++++++++++++++++++++++ src/server/index.ts | 92 +++++++++++- src/server/mcp.test.ts | 178 +++++++++++++++++++++-- src/server/mcp.ts | 267 +++++++++++++++++++---------------- src/shared/protocol.ts | 33 +++-- src/shared/requestContext.ts | 98 +++++++++++++ 6 files changed, 726 insertions(+), 146 deletions(-) create mode 100644 src/server/context.ts create mode 100644 src/shared/requestContext.ts diff --git a/src/server/context.ts b/src/server/context.ts new file mode 100644 index 000000000..dbc02a241 --- /dev/null +++ b/src/server/context.ts @@ -0,0 +1,204 @@ +import { + CreateMessageRequest, + CreateMessageResultSchema, + ElicitRequest, + ElicitResultSchema, + LoggingMessageNotification, + Notification, + Request, + RequestId, + RequestInfo, + RequestMeta, + Result, + ServerNotification, + ServerRequest, + ServerResult +} from '../types.js'; +import { RequestHandlerExtra, RequestOptions } from '../shared/protocol.js'; +import { ZodType } from 'zod'; +import type { z } from 'zod'; +import { Server } from './index.js'; +import { LifespanContext, RequestContext } from '../shared/requestContext.js'; +import { AuthInfo } from './auth/types.js'; + +/** + * A context object that is passed to request handlers. + * + * Implements the RequestHandlerExtra interface for backwards compatibility. + * Notes: + * Keeps this backwards compatible with the old RequestHandlerExtra interface and provides getter methods for backwards compatibility. + * In a breaking change, this can be removed and the RequestContext can be used directly via ctx.requestContext. + * + * TODO(Konstantin): Could be restructured better when breaking changes are allowed. More + */ +export class Context< + LifespanContextT extends LifespanContext | undefined = undefined, + RequestT extends Request = Request, + NotificationT extends Notification = Notification, + ResultT extends Result = Result +> implements RequestHandlerExtra +{ + private readonly server: Server; + + /** + * The request context. + * A type-safe context that is passed to request handlers. + */ + public readonly requestCtx: RequestContext< + LifespanContextT, + 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; + } + + /** + * 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) { + const request: CreateMessageRequest = { + method: 'sampling/createMessage', + params + }; + return this.server.request(request, CreateMessageResultSchema, { ...options, relatedRequestId: this.requestId }); + } + + /** + * Sends an elicitation request to the client. + */ + public elicit(params: ElicitRequest['params'], options?: RequestOptions) { + const request: ElicitRequest = { + method: 'elicitation/create', + params + }; + return 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/src/server/index.ts b/src/server/index.ts index 3eb0ba0d4..966b73711 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -1,4 +1,4 @@ -import { mergeCapabilities, Protocol, ProtocolOptions, RequestOptions } from '../shared/protocol.js'; +import { mergeCapabilities, Protocol, ProtocolOptions, RequestHandlerExtra, RequestOptions } from '../shared/protocol.js'; import { ClientCapabilities, CreateMessageRequest, @@ -29,11 +29,18 @@ import { SUPPORTED_PROTOCOL_VERSIONS, LoggingLevel, SetLevelRequestSchema, - LoggingLevelSchema + LoggingLevelSchema, + JSONRPCRequest, + MessageExtraInfo } from '../types.js'; import Ajv from 'ajv'; +import { Context } from './context.js'; +import { LifespanContext, RequestContext } from '../shared/requestContext.js'; +import { ZodLiteral, ZodObject } from 'zod'; +import { z } from 'zod'; +import { Transport } from 'src/shared/transport.js'; -export type ServerOptions = ProtocolOptions & { +export type ServerOptions = ProtocolOptions & { /** * Capabilities to advertise as being supported by this server. */ @@ -43,6 +50,11 @@ export type ServerOptions = ProtocolOptions & { * Optional instructions describing how to use the server and its features. */ instructions?: string; + + /** + * Optional lifespan context type. + */ + lifespan?: LifespanContextT; }; /** @@ -73,12 +85,14 @@ export type ServerOptions = ProtocolOptions & { export class Server< RequestT extends Request = Request, NotificationT extends Notification = Notification, - ResultT extends Result = Result + ResultT extends Result = Result, + LifespanContextT extends LifespanContext | undefined = undefined > extends Protocol { private _clientCapabilities?: ClientCapabilities; private _clientVersion?: Implementation; private _capabilities: ServerCapabilities; private _instructions?: string; + private _lifespan?: LifespanContextT; /** * Callback for when initialization has fully completed (i.e., the client has sent an `initialized` notification). @@ -90,12 +104,12 @@ export class Server< */ constructor( private _serverInfo: Implementation, - options?: ServerOptions + options?: ServerOptions ) { super(options); this._capabilities = options?.capabilities ?? {}; this._instructions = options?.instructions; - + this._lifespan = options?.lifespan; this.setRequestHandler(InitializeRequestSchema, request => this._oninitialize(request)); this.setNotificationHandler(InitializedNotificationSchema, () => this.oninitialized?.()); @@ -125,6 +139,13 @@ export class Server< return currentLevel ? this.LOG_LEVEL_SEVERITY.get(level)! < this.LOG_LEVEL_SEVERITY.get(currentLevel)! : false; }; + // Runtime type guard: ensure extra is our Context + private isContextExtra( + extra: RequestHandlerExtra + ): extra is Context { + return extra instanceof Context; + } + /** * Registers new capabilities. This can only be called before connecting to a transport. * @@ -277,6 +298,65 @@ export class Server< return this._capabilities; } + /** + * Registers a handler where `extra` is typed as `Context` for ergonomic server-side usage. + * Internally, this wraps the handler and forwards to the base implementation. + */ + public override setRequestHandler< + T extends ZodObject<{ + method: ZodLiteral; + }> + >( + requestSchema: T, + handler: ( + request: z.infer, + extra: Context + ) => ServerResult | ResultT | Promise + ): void { + super.setRequestHandler(requestSchema, (request, extra) => { + if (!this.isContextExtra(extra)) { + throw new Error('Internal error: Expected Context for request handler extra'); + } + return handler(request, extra); + }); + } + + protected override createRequestExtra( + request: JSONRPCRequest, + abortController: AbortController, + capturedTransport: Transport | undefined, + extra?: MessageExtraInfo + ): RequestHandlerExtra { + const base = super.createRequestExtra(request, abortController, capturedTransport, extra) as RequestHandlerExtra< + ServerRequest | RequestT, + ServerNotification | NotificationT + >; + + // Wrap base in Context to add server utilities while preserving shape + const requestCtx = new RequestContext< + LifespanContextT, + ServerRequest | RequestT, + ServerNotification | NotificationT, + ServerResult | ResultT + >({ + signal: base.signal, + authInfo: base.authInfo, + requestInfo: base.requestInfo, + requestId: base.requestId, + _meta: base._meta, + sessionId: base.sessionId, + lifespanContext: this._lifespan as LifespanContextT, + protocol: this + }); + + const ctx = new Context({ + server: this, + requestCtx + }); + + return ctx; + } + async ping() { return this.request({ method: 'ping' }, EmptyResultSchema); } diff --git a/src/server/mcp.test.ts b/src/server/mcp.test.ts index 4bb42d7fc..14dc72355 100644 --- a/src/server/mcp.test.ts +++ b/src/server/mcp.test.ts @@ -20,6 +20,9 @@ import { ResourceTemplate } from './mcp.js'; import { completable } from './completable.js'; import { UriTemplate } from '../shared/uriTemplate.js'; import { getDisplayName } from '../shared/metadataUtils.js'; +import { Context } from './context.js'; +import { Server } from './index.js'; +import { RequestContext } from '../shared/requestContext.js'; describe('McpServer', () => { /*** @@ -167,6 +170,157 @@ describe('McpServer', () => { }); }); +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.tool( + 'ctx-tool', + { + 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.resource('ctx-resource', 'test://res/1', 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 tmpl = new ResourceTemplate('test://items/{id}', { + list: async extra => { + seen.isContext = extra instanceof Context; + seen.hasRequestId = !!extra.requestId; + return { resources: [] }; + } + }); + mcpServer.resource('ctx-template', tmpl, async (_uri, _vars, _extra) => ({ contents: [] })); + }, + client => client.request({ method: 'resources/list', params: {} }, ListResourcesResultSchema) + ], + [ + 'prompt', + (mcpServer, seen) => { + mcpServer.prompt('ctx-prompt', {}, async (_args, 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); + }); + + test('should allow typed lifespanContext access in tool callback', async () => { + const mcpServer = new McpServer( + { name: 'ctx-test', version: '1.0' }, + { + lifespan: { + userId: 'user-123', + attempt: 0 + } + } + ); + const client = new Client({ name: 'ctx-client', version: '1.0' }); + + // Register a tool that reads/writes typed lifespanContext + mcpServer.tool('lifespan-tool', { name: z.string() }, async (_args: { name: string }, extra) => { + expect(extra).toBeInstanceOf(Context); + + // Demonstrate type-safe access to lifespanContext + const lc = extra.requestCtx.lifespanContext; + + // Typed writes + lc.userId = 'user-123'; + lc.attempt = lc.attempt + 1; + + // Typed read + const userId: string = lc.userId; + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify({ userId, attempt: lc.attempt }) + } + ] + }; + }); + + 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: 'lifespan-tool', + arguments: { + name: 'sample-name' + } + } + }, + CallToolResultSchema + ); + + expect(result.content).toBeDefined(); + const text = (result.content![0] as TextContent).text; + const parsed = JSON.parse(text) as { userId: string; attempt: number }; + expect(parsed.userId).toBe('user-123'); + expect(parsed.attempt).toBe(1); + }); +}); + describe('ResourceTemplate', () => { /*** * Test: ResourceTemplate Creation with String Pattern @@ -200,17 +354,23 @@ describe('ResourceTemplate', () => { const template = new ResourceTemplate('test://{id}', { list }); expect(template.listCallback).toBe(list); + const server = new Server({ + name: 'test server', + version: '1.0' + }); + const abortController = new AbortController(); - const result = await template.listCallback?.({ - signal: abortController.signal, - requestId: 'not-implemented', - sendRequest: () => { - throw new Error('Not implemented'); - }, - sendNotification: () => { - throw new Error('Not implemented'); - } + const ctx = new Context({ + server: server, + requestCtx: new RequestContext({ + signal: abortController.signal, + requestId: 'not-implemented', + lifespanContext: undefined, + protocol: server + }) }); + + const result = await template.listCallback?.(ctx); expect(result?.resources).toHaveLength(1); expect(list).toHaveBeenCalled(); }); diff --git a/src/server/mcp.ts b/src/server/mcp.ts index cef1722d6..918d5058e 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -28,35 +28,37 @@ import { PromptArgument, GetPromptResult, ReadResourceResult, - ServerRequest, - ServerNotification, ToolAnnotations, - LoggingMessageNotification + LoggingMessageNotification, + Result, + Notification, + Request } from '../types.js'; import { Completable, CompletableDef } from './completable.js'; import { UriTemplate, Variables } from '../shared/uriTemplate.js'; -import { RequestHandlerExtra } from '../shared/protocol.js'; import { Transport } from '../shared/transport.js'; +import { Context } from './context.js'; +import type { LifespanContext } from 'src/shared/requestContext.js'; /** * High-level MCP server that provides a simpler API for working with resources, tools, and prompts. * For advanced usage (like sending notifications or setting custom request handlers), use the underlying * Server instance available via the `server` property. */ -export class McpServer { +export class McpServer { /** * The underlying Server instance, useful for advanced operations like sending notifications. */ - public readonly server: Server; + public readonly server: Server; - private _registeredResources: { [uri: string]: RegisteredResource } = {}; + private _registeredResources: { [uri: string]: RegisteredResource } = {}; private _registeredResourceTemplates: { - [name: string]: RegisteredResourceTemplate; + [name: string]: RegisteredResourceTemplate; } = {}; - private _registeredTools: { [name: string]: RegisteredTool } = {}; - private _registeredPrompts: { [name: string]: RegisteredPrompt } = {}; + private _registeredTools: { [name: string]: RegisteredTool } = {}; + private _registeredPrompts: { [name: string]: RegisteredPrompt } = {}; - constructor(serverInfo: Implementation, options?: ServerOptions) { + constructor(serverInfo: Implementation, options?: ServerOptions) { this.server = new Server(serverInfo, options); } @@ -144,7 +146,7 @@ export class McpServer { } const args = parseResult.data; - const cb = tool.callback as ToolCallback; + const cb = tool.callback as ToolCallback; try { result = await Promise.resolve(cb(args, extra)); } catch (error) { @@ -159,7 +161,7 @@ export class McpServer { }; } } else { - const cb = tool.callback as ToolCallback; + const cb = tool.callback as ToolCallback; try { result = await Promise.resolve(cb(extra)); } catch (error) { @@ -408,10 +410,10 @@ export class McpServer { } const args = parseResult.data; - const cb = prompt.callback as PromptCallback; + const cb = prompt.callback as PromptCallback; return await Promise.resolve(cb(args, extra)); } else { - const cb = prompt.callback as PromptCallback; + const cb = prompt.callback as PromptCallback; return await Promise.resolve(cb(extra)); } }); @@ -424,35 +426,48 @@ export class McpServer { /** * Registers a resource `name` at a fixed URI, which will use the given callback to respond to read requests. */ - resource(name: string, uri: string, readCallback: ReadResourceCallback): RegisteredResource; + resource(name: string, uri: string, readCallback: ReadResourceCallback): RegisteredResource; /** * Registers a resource `name` at a fixed URI with metadata, which will use the given callback to respond to read requests. */ - resource(name: string, uri: string, metadata: ResourceMetadata, readCallback: ReadResourceCallback): RegisteredResource; + resource( + name: string, + uri: string, + metadata: ResourceMetadata, + readCallback: ReadResourceCallback + ): RegisteredResource; /** * Registers a resource `name` with a template pattern, which will use the given callback to respond to read requests. */ - resource(name: string, template: ResourceTemplate, readCallback: ReadResourceTemplateCallback): RegisteredResourceTemplate; + resource( + name: string, + template: ResourceTemplate, + readCallback: ReadResourceTemplateCallback + ): RegisteredResourceTemplate; /** * Registers a resource `name` with a template pattern and metadata, which will use the given callback to respond to read requests. */ resource( name: string, - template: ResourceTemplate, + template: ResourceTemplate, metadata: ResourceMetadata, - readCallback: ReadResourceTemplateCallback - ): RegisteredResourceTemplate; + readCallback: ReadResourceTemplateCallback + ): RegisteredResourceTemplate; - resource(name: string, uriOrTemplate: string | ResourceTemplate, ...rest: unknown[]): RegisteredResource | RegisteredResourceTemplate { + resource( + name: string, + uriOrTemplate: string | ResourceTemplate, + ...rest: unknown[] + ): RegisteredResource | RegisteredResourceTemplate { let metadata: ResourceMetadata | undefined; if (typeof rest[0] === 'object') { metadata = rest.shift() as ResourceMetadata; } - const readCallback = rest[0] as ReadResourceCallback | ReadResourceTemplateCallback; + const readCallback = rest[0] as ReadResourceCallback | ReadResourceTemplateCallback; if (typeof uriOrTemplate === 'string') { if (this._registeredResources[uriOrTemplate]) { @@ -464,7 +479,7 @@ export class McpServer { undefined, uriOrTemplate, metadata, - readCallback as ReadResourceCallback + readCallback as ReadResourceCallback ); this.setResourceRequestHandlers(); @@ -480,7 +495,7 @@ export class McpServer { undefined, uriOrTemplate, metadata, - readCallback as ReadResourceTemplateCallback + readCallback as ReadResourceTemplateCallback ); this.setResourceRequestHandlers(); @@ -493,19 +508,24 @@ export class McpServer { * Registers a resource with a config object and callback. * For static resources, use a URI string. For dynamic resources, use a ResourceTemplate. */ - registerResource(name: string, uriOrTemplate: string, config: ResourceMetadata, readCallback: ReadResourceCallback): RegisteredResource; registerResource( name: string, - uriOrTemplate: ResourceTemplate, + uriOrTemplate: string, config: ResourceMetadata, - readCallback: ReadResourceTemplateCallback - ): RegisteredResourceTemplate; + readCallback: ReadResourceCallback + ): RegisteredResource; registerResource( name: string, - uriOrTemplate: string | ResourceTemplate, + uriOrTemplate: ResourceTemplate, config: ResourceMetadata, - readCallback: ReadResourceCallback | ReadResourceTemplateCallback - ): RegisteredResource | RegisteredResourceTemplate { + readCallback: ReadResourceTemplateCallback + ): RegisteredResourceTemplate; + registerResource( + name: string, + uriOrTemplate: string | ResourceTemplate, + config: ResourceMetadata, + readCallback: ReadResourceCallback | ReadResourceTemplateCallback + ): RegisteredResource | RegisteredResourceTemplate { if (typeof uriOrTemplate === 'string') { if (this._registeredResources[uriOrTemplate]) { throw new Error(`Resource ${uriOrTemplate} is already registered`); @@ -516,7 +536,7 @@ export class McpServer { (config as BaseMetadata).title, uriOrTemplate, config, - readCallback as ReadResourceCallback + readCallback as ReadResourceCallback ); this.setResourceRequestHandlers(); @@ -532,7 +552,7 @@ export class McpServer { (config as BaseMetadata).title, uriOrTemplate, config, - readCallback as ReadResourceTemplateCallback + readCallback as ReadResourceTemplateCallback ); this.setResourceRequestHandlers(); @@ -546,9 +566,9 @@ export class McpServer { title: string | undefined, uri: string, metadata: ResourceMetadata | undefined, - readCallback: ReadResourceCallback - ): RegisteredResource { - const registeredResource: RegisteredResource = { + readCallback: ReadResourceCallback + ): RegisteredResource { + const registeredResource: RegisteredResource = { name, title, metadata, @@ -577,11 +597,11 @@ export class McpServer { private _createRegisteredResourceTemplate( name: string, title: string | undefined, - template: ResourceTemplate, + template: ResourceTemplate, metadata: ResourceMetadata | undefined, - readCallback: ReadResourceTemplateCallback - ): RegisteredResourceTemplate { - const registeredResourceTemplate: RegisteredResourceTemplate = { + readCallback: ReadResourceTemplateCallback + ): RegisteredResourceTemplate { + const registeredResourceTemplate: RegisteredResourceTemplate = { resourceTemplate: template, title, metadata, @@ -612,9 +632,9 @@ export class McpServer { title: string | undefined, description: string | undefined, argsSchema: PromptArgsRawShape | undefined, - callback: PromptCallback - ): RegisteredPrompt { - const registeredPrompt: RegisteredPrompt = { + callback: PromptCallback + ): RegisteredPrompt { + const registeredPrompt: RegisteredPrompt = { title, description, argsSchema: argsSchema === undefined ? undefined : z.object(argsSchema), @@ -648,9 +668,9 @@ export class McpServer { outputSchema: ZodRawShape | undefined, annotations: ToolAnnotations | undefined, _meta: Record | undefined, - callback: ToolCallback - ): RegisteredTool { - const registeredTool: RegisteredTool = { + callback: ToolCallback + ): RegisteredTool { + const registeredTool: RegisteredTool = { title, description, inputSchema: inputSchema === undefined ? undefined : z.object(inputSchema), @@ -688,12 +708,12 @@ export class McpServer { /** * Registers a zero-argument tool `name`, which will run the given function when the client calls it. */ - tool(name: string, cb: ToolCallback): RegisteredTool; + tool(name: string, cb: ToolCallback): RegisteredTool; /** * Registers a zero-argument tool `name` (with a description) which will run the given function when the client calls it. */ - tool(name: string, description: string, cb: ToolCallback): RegisteredTool; + tool(name: string, description: string, cb: ToolCallback): RegisteredTool; /** * Registers a tool taking either a parameter schema for validation or annotations for additional metadata. @@ -702,7 +722,11 @@ export class McpServer { * Note: We use a union type for the second parameter because TypeScript cannot reliably disambiguate * between ToolAnnotations and ZodRawShape during overload resolution, as both are plain object types. */ - tool(name: string, paramsSchemaOrAnnotations: Args | ToolAnnotations, cb: ToolCallback): RegisteredTool; + tool( + name: string, + paramsSchemaOrAnnotations: Args | ToolAnnotations, + cb: ToolCallback + ): RegisteredTool; /** * Registers a tool `name` (with a description) taking either parameter schema or annotations. @@ -716,13 +740,19 @@ export class McpServer { name: string, description: string, paramsSchemaOrAnnotations: Args | ToolAnnotations, - cb: ToolCallback - ): RegisteredTool; + cb: ToolCallback + ): RegisteredTool; /** * Registers a tool with both parameter schema and annotations. */ - tool(name: string, paramsSchema: Args, annotations: ToolAnnotations, cb: ToolCallback): RegisteredTool; + // tool(name: string, paramsSchema: Args, annotations: ToolAnnotations, cb: ToolCallback): RegisteredTool; + tool( + name: string, + paramsSchema: Args, + annotations: ToolAnnotations, + cb: ToolCallback + ): RegisteredTool; /** * Registers a tool with description, parameter schema, and annotations. @@ -732,13 +762,13 @@ export class McpServer { description: string, paramsSchema: Args, annotations: ToolAnnotations, - cb: ToolCallback - ): RegisteredTool; + cb: ToolCallback + ): RegisteredTool; /** * tool() implementation. Parses arguments passed to overrides defined above. */ - tool(name: string, ...rest: unknown[]): RegisteredTool { + tool(name: string, ...rest: unknown[]): RegisteredTool { if (this._registeredTools[name]) { throw new Error(`Tool ${name} is already registered`); } @@ -778,7 +808,7 @@ export class McpServer { annotations = rest.shift() as ToolAnnotations; } } - const callback = rest[0] as ToolCallback; + const callback = rest[0] as ToolCallback; return this._createRegisteredTool(name, undefined, description, inputSchema, outputSchema, annotations, undefined, callback); } @@ -796,40 +826,35 @@ export class McpServer { annotations?: ToolAnnotations; _meta?: Record; }, - cb: ToolCallback - ): RegisteredTool { + cb: ToolCallback + ): RegisteredTool { if (this._registeredTools[name]) { throw new Error(`Tool ${name} is already registered`); } const { title, description, inputSchema, outputSchema, annotations, _meta } = config; - return this._createRegisteredTool( - name, - title, - description, - inputSchema, - outputSchema, - annotations, - _meta, - cb as ToolCallback - ); + return this._createRegisteredTool(name, title, description, inputSchema, outputSchema, annotations, _meta, cb); } /** * Registers a zero-argument prompt `name`, which will run the given function when the client calls it. */ - prompt(name: string, cb: PromptCallback): RegisteredPrompt; + prompt(name: string, cb: PromptCallback): RegisteredPrompt; /** * Registers a zero-argument prompt `name` (with a description) which will run the given function when the client calls it. */ - prompt(name: string, description: string, cb: PromptCallback): RegisteredPrompt; + prompt(name: string, description: string, cb: PromptCallback): RegisteredPrompt; /** * Registers a prompt `name` accepting the given arguments, which must be an object containing named properties associated with Zod schemas. When the client calls it, the function will be run with the parsed and validated arguments. */ - prompt(name: string, argsSchema: Args, cb: PromptCallback): RegisteredPrompt; + prompt( + name: string, + argsSchema: Args, + cb: PromptCallback + ): RegisteredPrompt; /** * Registers a prompt `name` (with a description) accepting the given arguments, which must be an object containing named properties associated with Zod schemas. When the client calls it, the function will be run with the parsed and validated arguments. @@ -838,10 +863,10 @@ export class McpServer { name: string, description: string, argsSchema: Args, - cb: PromptCallback - ): RegisteredPrompt; + cb: PromptCallback + ): RegisteredPrompt; - prompt(name: string, ...rest: unknown[]): RegisteredPrompt { + prompt(name: string, ...rest: unknown[]): RegisteredPrompt { if (this._registeredPrompts[name]) { throw new Error(`Prompt ${name} is already registered`); } @@ -856,7 +881,7 @@ export class McpServer { argsSchema = rest.shift() as PromptArgsRawShape; } - const cb = rest[0] as PromptCallback; + const cb = rest[0] as PromptCallback; const registeredPrompt = this._createRegisteredPrompt(name, undefined, description, argsSchema, cb); this.setPromptRequestHandlers(); @@ -875,21 +900,15 @@ export class McpServer { description?: string; argsSchema?: Args; }, - cb: PromptCallback - ): RegisteredPrompt { + cb: PromptCallback + ): RegisteredPrompt { if (this._registeredPrompts[name]) { throw new Error(`Prompt ${name} is already registered`); } const { title, description, argsSchema } = config; - const registeredPrompt = this._createRegisteredPrompt( - name, - title, - description, - argsSchema, - cb as PromptCallback - ); + const registeredPrompt = this._createRegisteredPrompt(name, title, description, argsSchema, cb); this.setPromptRequestHandlers(); this.sendPromptListChanged(); @@ -957,7 +976,7 @@ export type CompleteResourceTemplateCallback = ( * A resource template combines a URI pattern with optional functionality to enumerate * all resources matching that pattern. */ -export class ResourceTemplate { +export class ResourceTemplate { private _uriTemplate: UriTemplate; constructor( @@ -966,7 +985,7 @@ export class ResourceTemplate { /** * A callback to list all resources matching this template. This is required to specified, even if `undefined`, to avoid accidentally forgetting resource listing. */ - list: ListResourcesCallback | undefined; + list: ListResourcesCallback | undefined; /** * An optional callback to autocomplete variables within the URI template. Useful for clients and users to discover possible values. @@ -989,7 +1008,7 @@ export class ResourceTemplate { /** * Gets the list callback, if one was provided. */ - get listCallback(): ListResourcesCallback | undefined { + get listCallback(): ListResourcesCallback | undefined { return this._callbacks.list; } @@ -1011,21 +1030,27 @@ export class ResourceTemplate { * - `content` if the tool does not have an outputSchema * - Both fields are optional but typically one should be provided */ -export type ToolCallback = Args extends ZodRawShape - ? ( - args: z.objectOutputType, - extra: RequestHandlerExtra - ) => CallToolResult | Promise - : (extra: RequestHandlerExtra) => CallToolResult | Promise; - -export type RegisteredTool = { +type LifespanLike = LifespanContext | undefined; +export type ToolCallbackWithArgs = ( + args: z.objectOutputType, + extra: Context +) => CallToolResult | Promise; + +export type ToolCallbackNoArgs = (extra: Context) => CallToolResult | Promise; + +export type ToolCallback< + Args extends undefined | ZodRawShape = undefined, + LifespanContextT extends LifespanLike = undefined +> = Args extends ZodRawShape ? ToolCallbackWithArgs : ToolCallbackNoArgs; + +export type RegisteredTool = { title?: string; description?: string; inputSchema?: AnyZodObject; outputSchema?: AnyZodObject; annotations?: ToolAnnotations; _meta?: Record; - callback: ToolCallback; + callback: ToolCallbackWithArgs | ToolCallbackNoArgs; enabled: boolean; enable(): void; disable(): void; @@ -1037,7 +1062,7 @@ export type RegisteredTool = { outputSchema?: OutputArgs; annotations?: ToolAnnotations; _meta?: Record; - callback?: ToolCallback; + callback?: ToolCallback; enabled?: boolean; }): void; remove(): void; @@ -1078,23 +1103,23 @@ export type ResourceMetadata = Omit; /** * Callback to list all resources matching a given template. */ -export type ListResourcesCallback = ( - extra: RequestHandlerExtra +export type ListResourcesCallback = ( + extra: Context ) => ListResourcesResult | Promise; /** * Callback to read a resource at a given URI. */ -export type ReadResourceCallback = ( +export type ReadResourceCallback = ( uri: URL, - extra: RequestHandlerExtra + extra: Context ) => ReadResourceResult | Promise; -export type RegisteredResource = { +export type RegisteredResource = { name: string; title?: string; metadata?: ResourceMetadata; - readCallback: ReadResourceCallback; + readCallback: ReadResourceCallback; enabled: boolean; enable(): void; disable(): void; @@ -1103,7 +1128,7 @@ export type RegisteredResource = { title?: string; uri?: string | null; metadata?: ResourceMetadata; - callback?: ReadResourceCallback; + callback?: ReadResourceCallback; enabled?: boolean; }): void; remove(): void; @@ -1112,26 +1137,26 @@ export type RegisteredResource = { /** * Callback to read a resource at a given URI, following a filled-in URI template. */ -export type ReadResourceTemplateCallback = ( +export type ReadResourceTemplateCallback = ( uri: URL, variables: Variables, - extra: RequestHandlerExtra + extra: Context ) => ReadResourceResult | Promise; -export type RegisteredResourceTemplate = { - resourceTemplate: ResourceTemplate; +export type RegisteredResourceTemplate = { + resourceTemplate: ResourceTemplate; title?: string; metadata?: ResourceMetadata; - readCallback: ReadResourceTemplateCallback; + readCallback: ReadResourceTemplateCallback; enabled: boolean; enable(): void; disable(): void; update(updates: { name?: string | null; title?: string; - template?: ResourceTemplate; + template?: ResourceTemplate; metadata?: ResourceMetadata; - callback?: ReadResourceTemplateCallback; + callback?: ReadResourceTemplateCallback; enabled?: boolean; }): void; remove(): void; @@ -1141,18 +1166,18 @@ type PromptArgsRawShape = { [k: string]: ZodType | ZodOptional>; }; -export type PromptCallback = Args extends PromptArgsRawShape - ? ( - args: z.objectOutputType, - extra: RequestHandlerExtra - ) => GetPromptResult | Promise - : (extra: RequestHandlerExtra) => GetPromptResult | Promise; +export type PromptCallback< + Args extends undefined | PromptArgsRawShape = undefined, + LifespanContextT extends LifespanContext | undefined = undefined +> = Args extends PromptArgsRawShape + ? (args: z.objectOutputType, extra: Context) => GetPromptResult | Promise + : (extra: Context) => GetPromptResult | Promise; -export type RegisteredPrompt = { +export type RegisteredPrompt = { title?: string; description?: string; argsSchema?: ZodObject; - callback: PromptCallback; + callback: PromptCallback; enabled: boolean; enable(): void; disable(): void; @@ -1161,7 +1186,7 @@ export type RegisteredPrompt = { title?: string; description?: string; argsSchema?: Args; - callback?: PromptCallback; + callback?: PromptCallback; enabled?: boolean; }): void; remove(): void; diff --git a/src/shared/protocol.ts b/src/shared/protocol.ts index 5cb969418..2a722b836 100644 --- a/src/shared/protocol.ts +++ b/src/shared/protocol.ts @@ -360,16 +360,7 @@ export abstract class Protocol = { - signal: abortController.signal, - sessionId: capturedTransport?.sessionId, - _meta: request.params?._meta, - sendNotification: notification => this.notification(notification, { relatedRequestId: request.id }), - sendRequest: (r, resultSchema, options?) => this.request(r, resultSchema, { ...options, relatedRequestId: request.id }), - authInfo: extra?.authInfo, - requestId: request.id, - requestInfo: extra?.requestInfo - }; + const fullExtra = this.createRequestExtra(request, abortController, capturedTransport, extra); // Starting with Promise.resolve() puts any synchronous errors into the monad as well. Promise.resolve() @@ -407,6 +398,28 @@ export abstract class Protocol { + return { + signal: abortController.signal, + sessionId: capturedTransport?.sessionId, + _meta: request.params?._meta, + sendNotification: notification => this.notification(notification, { relatedRequestId: request.id }), + sendRequest: (r, resultSchema, options?) => this.request(r, resultSchema, { ...options, relatedRequestId: request.id }), + authInfo: extra?.authInfo, + requestId: request.id, + requestInfo: extra?.requestInfo + } as RequestHandlerExtra; + } + private _onprogress(notification: ProgressNotification): void { const { progressToken, ...params } = notification.params; const messageId = Number(progressToken); diff --git a/src/shared/requestContext.ts b/src/shared/requestContext.ts new file mode 100644 index 000000000..29e114d18 --- /dev/null +++ b/src/shared/requestContext.ts @@ -0,0 +1,98 @@ +import { AuthInfo } from 'src/server/auth/types.js'; +import { Notification, Request, RequestId, RequestInfo, RequestMeta, Result } from 'src/types.js'; +import { Protocol, RequestHandlerExtra, RequestOptions } from './protocol.js'; +import { ZodType } from 'zod'; +import type { z } from 'zod'; + +export type LifespanContext = { + [key: string]: unknown | undefined; +}; +/** + * A context object that is passed to request handlers. + * + * Implements the RequestHandlerExtra interface for backwards compatibility. + * + * TODO(Konstantin): Could be restructured better when breaking changes are allowed. + */ +export class RequestContext< + LifespanContextT extends LifespanContext | undefined, + 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 lifespan context. A type-safe context that is passed to request handlers. + */ + public readonly lifespanContext: LifespanContextT; + + private readonly protocol: Protocol; + constructor(args: { + signal: AbortSignal; + authInfo?: AuthInfo; + requestInfo?: RequestInfo; + requestId: RequestId; + _meta?: RequestMeta; + sessionId?: string; + lifespanContext: LifespanContextT; + protocol: Protocol; + }) { + 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.lifespanContext = args.lifespanContext; + this.protocol = args.protocol; + } + + /** + * 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?: RequestOptions): Promise> => { + return this.protocol.request(request, resultSchema, { ...options, relatedRequestId: this.requestId }); + }; +} From 7aec2ef8734fd884901a80a013d5dd93ef41a71a Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Mon, 6 Oct 2025 18:37:37 +0300 Subject: [PATCH 2/5] add log utility tests --- src/server/mcp.test.ts | 168 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 167 insertions(+), 1 deletion(-) diff --git a/src/server/mcp.test.ts b/src/server/mcp.test.ts index 14dc72355..cad04d479 100644 --- a/src/server/mcp.test.ts +++ b/src/server/mcp.test.ts @@ -14,7 +14,8 @@ import { LoggingMessageNotificationSchema, Notification, TextContent, - ElicitRequestSchema + ElicitRequestSchema, + SetLevelRequestSchema } from '../types.js'; import { ResourceTemplate } from './mcp.js'; import { completable } from './completable.js'; @@ -319,6 +320,62 @@ describe('Context', () => { expect(parsed.userId).toBe('user-123'); expect(parsed.attempt).toBe(1); }); + + 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: { + logging: {} + } + }); + + 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.tool('ctx-log-test', { 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].type).toBe('text'); + expect(result.content[0].text).toBe('ok'); + }); }); describe('ResourceTemplate', () => { @@ -4132,6 +4189,115 @@ describe('elicitInput()', () => { ); }); + test('should be able to call elicit from Context', async () => { + mcpServer.tool( + 'elicit-through-ctx-tool', + { + restaurant: z.string(), + date: z.string(), + partySize: z.number() + }, + async ({ restaurant, date, partySize }, extra) => { + // Check availability + const available = await checkAvailability(restaurant, date, partySize); + + if (!available) { + // Ask user if they want to try alternative dates + const result = await extra.elicit({ + message: `No tables available at ${restaurant} on ${date}. Would you like to check alternative dates?`, + requestedSchema: { + type: 'object', + properties: { + checkAlternatives: { + type: 'boolean', + title: 'Check alternative dates', + description: 'Would you like me to check other dates?' + }, + flexibleDates: { + type: 'string', + title: 'Date flexibility', + description: 'How flexible are your dates?', + enum: ['next_day', 'same_week', 'next_week'], + enumNames: ['Next day', 'Same week', 'Next week'] + } + }, + required: ['checkAlternatives'] + } + }); + + if (result.action === 'accept' && result.content?.checkAlternatives) { + const alternatives = await findAlternatives(restaurant, date, partySize, result.content.flexibleDates as string); + return { + content: [ + { + type: 'text', + text: `Found these alternatives: ${alternatives.join(', ')}` + } + ] + }; + } + + return { + content: [ + { + type: 'text', + text: 'No booking made. Original date not available.' + } + ] + }; + } + + await makeBooking(restaurant, date, partySize); + return { + content: [ + { + type: 'text', + text: `Booked table for ${partySize} at ${restaurant} on ${date}` + } + ] + }; + } + ); + + checkAvailability.mockResolvedValue(false); + findAlternatives.mockResolvedValue(['2024-12-26', '2024-12-27', '2024-12-28']); + + // Set up client to accept alternative date checking + client.setRequestHandler(ElicitRequestSchema, async request => { + expect(request.params.message).toContain('No tables available at ABC Restaurant on 2024-12-25'); + return { + action: 'accept', + content: { + checkAlternatives: true, + flexibleDates: 'same_week' + } + }; + }); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + + await Promise.all([client.connect(clientTransport), mcpServer.server.connect(serverTransport)]); + + // Call the tool + const result = await client.callTool({ + name: 'book-restaurant', + arguments: { + restaurant: 'ABC Restaurant', + date: '2024-12-25', + partySize: 2 + } + }); + + expect(checkAvailability).toHaveBeenCalledWith('ABC Restaurant', '2024-12-25', 2); + expect(findAlternatives).toHaveBeenCalledWith('ABC Restaurant', '2024-12-25', 2, 'same_week'); + expect(result.content).toEqual([ + { + type: 'text', + text: 'Found these alternatives: 2024-12-26, 2024-12-27, 2024-12-28' + } + ]); + }); + test('should successfully elicit additional information', async () => { // Mock availability check to return false checkAvailability.mockResolvedValue(false); From a694ba24634ff280fb03263736f2a64e43ab48d4 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Mon, 6 Oct 2025 18:41:54 +0300 Subject: [PATCH 3/5] add tests --- src/server/mcp.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/server/mcp.test.ts b/src/server/mcp.test.ts index cad04d479..ed30612bc 100644 --- a/src/server/mcp.test.ts +++ b/src/server/mcp.test.ts @@ -14,8 +14,7 @@ import { LoggingMessageNotificationSchema, Notification, TextContent, - ElicitRequestSchema, - SetLevelRequestSchema + ElicitRequestSchema } from '../types.js'; import { ResourceTemplate } from './mcp.js'; import { completable } from './completable.js'; From 8dcb65bc2a67a6f6dba96a9cbecf0dec9b76b932 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Mon, 6 Oct 2025 18:51:23 +0300 Subject: [PATCH 4/5] code formatting --- src/server/context.ts | 7 +------ src/server/mcp.test.ts | 45 +++++++++++++++++++++++++----------------- 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/src/server/context.ts b/src/server/context.ts index dbc02a241..c91a3c419 100644 --- a/src/server/context.ts +++ b/src/server/context.ts @@ -1,6 +1,5 @@ import { CreateMessageRequest, - CreateMessageResultSchema, ElicitRequest, ElicitResultSchema, LoggingMessageNotification, @@ -109,11 +108,7 @@ export class Context< * Sends a request to sample an LLM via the client. */ public requestSampling(params: CreateMessageRequest['params'], options?: RequestOptions) { - const request: CreateMessageRequest = { - method: 'sampling/createMessage', - params - }; - return this.server.request(request, CreateMessageResultSchema, { ...options, relatedRequestId: this.requestId }); + return this.server.createMessage(params, options); } /** diff --git a/src/server/mcp.test.ts b/src/server/mcp.test.ts index ed30612bc..1383e172f 100644 --- a/src/server/mcp.test.ts +++ b/src/server/mcp.test.ts @@ -14,7 +14,8 @@ import { LoggingMessageNotificationSchema, Notification, TextContent, - ElicitRequestSchema + ElicitRequestSchema, + CreateMessageResultSchema } from '../types.js'; import { ResourceTemplate } from './mcp.js'; import { completable } from './completable.js'; @@ -323,21 +324,27 @@ describe('Context', () => { 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: {} + 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: { - logging: {} + ); + const client = new Client( + { name: 'ctx-client', version: '1.0' }, + { + capabilities: { + logging: {} + } } - }); + ); let seen = 0; - client.setNotificationHandler(LoggingMessageNotificationSchema, (notification) => { + client.setNotificationHandler(LoggingMessageNotificationSchema, notification => { seen++; expect(notification.params.level).toBe(level); expect(notification.params.data).toBe('Test message'); @@ -346,20 +353,22 @@ describe('Context', () => { return; }); - mcpServer.tool('ctx-log-test', { 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'); + 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', From 3075eae6bf7ece3a0a337ae865141ab580bf9a95 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Mon, 6 Oct 2025 19:45:43 +0300 Subject: [PATCH 5/5] lint fix --- src/server/mcp.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/server/mcp.test.ts b/src/server/mcp.test.ts index 1383e172f..97eef5dc5 100644 --- a/src/server/mcp.test.ts +++ b/src/server/mcp.test.ts @@ -14,8 +14,7 @@ import { LoggingMessageNotificationSchema, Notification, TextContent, - ElicitRequestSchema, - CreateMessageResultSchema + ElicitRequestSchema } from '../types.js'; import { ResourceTemplate } from './mcp.js'; import { completable } from './completable.js';