import { ZodLiteral, ZodObject, ZodType, z } from "zod"; import { CancelledNotificationSchema, ErrorCode, JSONRPCError, JSONRPCNotification, JSONRPCRequest, JSONRPCResponse, McpError, Notification, PingRequestSchema, Progress, ProgressNotification, ProgressNotificationSchema, Request, RequestId, Result, } from "../types.js"; import { Transport } from "./transport.js"; /** * Callback for progress notifications. */ export type ProgressCallback = (progress: Progress) => void; /** * Additional initialization options. */ export type ProtocolOptions = { /** * Whether to restrict emitted requests to only those that the remote side has indicated that they can handle, through their advertised capabilities. * * Note that this DOES NOT affect checking of _local_ side capabilities, as it is considered a logic error to mis-specify those. * * Currently this defaults to false, for backwards compatibility with SDK versions that did not advertise capabilities correctly. In future, this will default to true. */ enforceStrictCapabilities?: boolean; }; /** * Options that can be given per request. */ export type RequestOptions = { /** * If set, requests progress notifications from the remote end (if supported). When progress notifications are received, this callback will be invoked. */ onprogress?: ProgressCallback; /** * Can be used to cancel an in-flight request. This will cause an AbortError to be raised from request(). */ signal?: AbortSignal; }; /** * Extra data given to request handlers. */ export type RequestHandlerExtra = { /** * An abort signal used to communicate if the request was cancelled from the sender's side. */ signal: AbortSignal; }; /** * Implements MCP protocol framing on top of a pluggable transport, including * features like request/response linking, notifications, and progress. */ export abstract class Protocol< SendRequestT extends Request, SendNotificationT extends Notification, SendResultT extends Result, > { private _transport?: Transport; private _requestMessageId = 0; private _requestHandlers: Map< string, ( request: JSONRPCRequest, extra: RequestHandlerExtra, ) => Promise > = new Map(); private _requestHandlerAbortControllers: Map = new Map(); private _notificationHandlers: Map< string, (notification: JSONRPCNotification) => Promise > = new Map(); private _responseHandlers: Map< number, (response: JSONRPCResponse | Error) => void > = new Map(); private _progressHandlers: Map = new Map(); /** * Callback for when the connection is closed for any reason. * * This is invoked when close() is called as well. */ onclose?: () => void; /** * Callback for when an error occurs. * * Note that errors are not necessarily fatal; they are used for reporting any kind of exceptional condition out of band. */ onerror?: (error: Error) => void; /** * A handler to invoke for any request types that do not have their own handler installed. */ fallbackRequestHandler?: (request: Request) => Promise; /** * A handler to invoke for any notification types that do not have their own handler installed. */ fallbackNotificationHandler?: (notification: Notification) => Promise; constructor(private _options?: ProtocolOptions) { this.setNotificationHandler(CancelledNotificationSchema, (notification) => { const controller = this._requestHandlerAbortControllers.get( notification.params.requestId, ); controller?.abort(notification.params.reason); }); this.setNotificationHandler(ProgressNotificationSchema, (notification) => { this._onprogress(notification as unknown as ProgressNotification); }); this.setRequestHandler( PingRequestSchema, // Automatic pong by default. (_request) => ({}) as SendResultT, ); } /** * Attaches to the given transport, starts it, and starts listening for messages. * * The Protocol object assumes ownership of the Transport, replacing any callbacks that have already been set, and expects that it is the only user of the Transport instance going forward. */ async connect(transport: Transport): Promise { this._transport = transport; this._transport.onclose = () => { this._onclose(); }; this._transport.onerror = (error: Error) => { this._onerror(error); }; this._transport.onmessage = (message) => { if (!("method" in message)) { this._onresponse(message); } else if ("id" in message) { this._onrequest(message); } else { this._onnotification(message); } }; await this._transport.start(); } private _onclose(): void { const responseHandlers = this._responseHandlers; this._responseHandlers = new Map(); this._progressHandlers.clear(); this._transport = undefined; this.onclose?.(); const error = new McpError(ErrorCode.ConnectionClosed, "Connection closed"); for (const handler of responseHandlers.values()) { handler(error); } } private _onerror(error: Error): void { this.onerror?.(error); } private _onnotification(notification: JSONRPCNotification): void { const handler = this._notificationHandlers.get(notification.method) ?? this.fallbackNotificationHandler; // Ignore notifications not being subscribed to. if (handler === undefined) { return; } handler(notification).catch((error) => this._onerror( new Error(`Uncaught error in notification handler: ${error}`), ), ); } private _onrequest(request: JSONRPCRequest): void { const handler = this._requestHandlers.get(request.method) ?? this.fallbackRequestHandler; if (handler === undefined) { this._transport ?.send({ jsonrpc: "2.0", id: request.id, error: { code: ErrorCode.MethodNotFound, message: "Method not found", }, }) .catch((error) => this._onerror( new Error(`Failed to send an error response: ${error}`), ), ); return; } const abortController = new AbortController(); this._requestHandlerAbortControllers.set(request.id, abortController); handler(request, { signal: abortController.signal }) .then( (result) => { if (abortController.signal.aborted) { return; } return this._transport?.send({ result, jsonrpc: "2.0", id: request.id, }); }, (error) => { if (abortController.signal.aborted) { return; } return this._transport?.send({ jsonrpc: "2.0", id: request.id, error: { code: Number.isSafeInteger(error["code"]) ? error["code"] : ErrorCode.InternalError, message: error.message ?? "Internal error", }, }); }, ) .catch((error) => this._onerror(new Error(`Failed to send response: ${error}`)), ) .finally(() => { this._requestHandlerAbortControllers.delete(request.id); }); } private _onprogress(notification: ProgressNotification): void { const { progress, total, progressToken } = notification.params; const handler = this._progressHandlers.get(Number(progressToken)); if (handler === undefined) { this._onerror( new Error( `Received a progress notification for an unknown token: ${JSON.stringify(notification)}`, ), ); return; } handler({ progress, total }); } private _onresponse(response: JSONRPCResponse | JSONRPCError): void { const messageId = response.id; const handler = this._responseHandlers.get(Number(messageId)); if (handler === undefined) { this._onerror( new Error( `Received a response for an unknown message ID: ${JSON.stringify(response)}`, ), ); return; } this._responseHandlers.delete(Number(messageId)); this._progressHandlers.delete(Number(messageId)); if ("result" in response) { handler(response); } else { const error = new McpError( response.error.code, response.error.message, response.error.data, ); handler(error); } } get transport(): Transport | undefined { return this._transport; } /** * Closes the connection. */ async close(): Promise { await this._transport?.close(); } /** * A method to check if a capability is supported by the remote side, for the given method to be called. * * This should be implemented by subclasses. */ protected abstract assertCapabilityForMethod( method: SendRequestT["method"], ): void; /** * A method to check if a notification is supported by the local side, for the given method to be sent. * * This should be implemented by subclasses. */ protected abstract assertNotificationCapability( method: SendNotificationT["method"], ): void; /** * A method to check if a request handler is supported by the local side, for the given method to be handled. * * This should be implemented by subclasses. */ protected abstract assertRequestHandlerCapability(method: string): void; /** * Sends a request and wait for a response. * * Do not use this method to emit notifications! Use notification() instead. */ request>( request: SendRequestT, resultSchema: T, options?: RequestOptions, ): Promise> { return new Promise((resolve, reject) => { if (!this._transport) { reject(new Error("Not connected")); return; } if (this._options?.enforceStrictCapabilities === true) { this.assertCapabilityForMethod(request.method); } options?.signal?.throwIfAborted(); const messageId = this._requestMessageId++; const jsonrpcRequest: JSONRPCRequest = { ...request, jsonrpc: "2.0", id: messageId, }; if (options?.onprogress) { this._progressHandlers.set(messageId, options.onprogress); jsonrpcRequest.params = { ...request.params, _meta: { progressToken: messageId }, }; } this._responseHandlers.set(messageId, (response) => { if (options?.signal?.aborted) { return; } if (response instanceof Error) { return reject(response); } try { const result = resultSchema.parse(response.result); resolve(result); } catch (error) { reject(error); } }); options?.signal?.addEventListener("abort", () => { const reason = options?.signal?.reason; this._responseHandlers.delete(messageId); this._progressHandlers.delete(messageId); this._transport?.send({ jsonrpc: "2.0", method: "cancelled", params: { requestId: messageId, reason: String(reason), }, }); reject(reason); }); this._transport.send(jsonrpcRequest).catch(reject); }); } /** * Emits a notification, which is a one-way message that does not expect a response. */ async notification(notification: SendNotificationT): Promise { if (!this._transport) { throw new Error("Not connected"); } this.assertNotificationCapability(notification.method); const jsonrpcNotification: JSONRPCNotification = { ...notification, jsonrpc: "2.0", }; await this._transport.send(jsonrpcNotification); } /** * Registers a handler to invoke when this protocol object receives a request with the given method. * * Note that this will replace any previous request handler for the same method. */ setRequestHandler< T extends ZodObject<{ method: ZodLiteral; }>, >( requestSchema: T, handler: ( request: z.infer, extra: RequestHandlerExtra, ) => SendResultT | Promise, ): void { const method = requestSchema.shape.method.value; this.assertRequestHandlerCapability(method); this._requestHandlers.set(method, (request, extra) => Promise.resolve(handler(requestSchema.parse(request), extra)), ); } /** * Removes the request handler for the given method. */ removeRequestHandler(method: string): void { this._requestHandlers.delete(method); } /** * Registers a handler to invoke when this protocol object receives a notification with the given method. * * Note that this will replace any previous notification handler for the same method. */ setNotificationHandler< T extends ZodObject<{ method: ZodLiteral; }>, >( notificationSchema: T, handler: (notification: z.infer) => void | Promise, ): void { this._notificationHandlers.set( notificationSchema.shape.method.value, (notification) => Promise.resolve(handler(notificationSchema.parse(notification))), ); } /** * Removes the notification handler for the given method. */ removeNotificationHandler(method: string): void { this._notificationHandlers.delete(method); } }