UNPKG

@modelcontextprotocol/sdk

Version:

Model Context Protocol implementation for TypeScript

994 lines 54.7 kB
import { safeParse } from '../server/zod-compat.js'; import { CancelledNotificationSchema, CreateTaskResultSchema, ErrorCode, GetTaskRequestSchema, GetTaskResultSchema, GetTaskPayloadRequestSchema, ListTasksRequestSchema, ListTasksResultSchema, CancelTaskRequestSchema, CancelTaskResultSchema, isJSONRPCError, isJSONRPCRequest, isJSONRPCResponse, isJSONRPCNotification, McpError, PingRequestSchema, ProgressNotificationSchema, RELATED_TASK_META_KEY, TaskStatusNotificationSchema } from '../types.js'; import { isTerminal } from '../experimental/tasks/interfaces.js'; import { getMethodLiteral, parseWithCompat } from '../server/zod-json-schema-compat.js'; /** * The default request timeout, in miliseconds. */ export const DEFAULT_REQUEST_TIMEOUT_MSEC = 60000; /** * Implements MCP protocol framing on top of a pluggable transport, including * features like request/response linking, notifications, and progress. */ export class Protocol { constructor(_options) { this._options = _options; this._requestMessageId = 0; this._requestHandlers = new Map(); this._requestHandlerAbortControllers = new Map(); this._notificationHandlers = new Map(); this._responseHandlers = new Map(); this._progressHandlers = new Map(); this._timeoutInfo = new Map(); this._pendingDebouncedNotifications = new Set(); // Maps task IDs to progress tokens to keep handlers alive after CreateTaskResult this._taskProgressTokens = new Map(); this._requestResolvers = new Map(); this.setNotificationHandler(CancelledNotificationSchema, notification => { this._oncancel(notification); }); this.setNotificationHandler(ProgressNotificationSchema, notification => { this._onprogress(notification); }); this.setRequestHandler(PingRequestSchema, // Automatic pong by default. _request => ({})); // Install task handlers if TaskStore is provided this._taskStore = _options === null || _options === void 0 ? void 0 : _options.taskStore; this._taskMessageQueue = _options === null || _options === void 0 ? void 0 : _options.taskMessageQueue; if (this._taskStore) { this.setRequestHandler(GetTaskRequestSchema, async (request, extra) => { const task = await this._taskStore.getTask(request.params.taskId, extra.sessionId); if (!task) { throw new McpError(ErrorCode.InvalidParams, 'Failed to retrieve task: Task not found'); } // Per spec: tasks/get responses SHALL NOT include related-task metadata // as the taskId parameter is the source of truth // @ts-expect-error SendResultT cannot contain GetTaskResult, but we include it in our derived types everywhere else return { ...task }; }); this.setRequestHandler(GetTaskPayloadRequestSchema, async (request, extra) => { const handleTaskResult = async () => { var _a; const taskId = request.params.taskId; // Deliver queued messages if (this._taskMessageQueue) { let queuedMessage; while ((queuedMessage = await this._taskMessageQueue.dequeue(taskId, extra.sessionId))) { // Handle response and error messages by routing them to the appropriate resolver if (queuedMessage.type === 'response' || queuedMessage.type === 'error') { const message = queuedMessage.message; const requestId = message.id; // Lookup resolver in _requestResolvers map const resolver = this._requestResolvers.get(requestId); if (resolver) { // Remove resolver from map after invocation this._requestResolvers.delete(requestId); // Invoke resolver with response or error if (queuedMessage.type === 'response') { resolver(message); } else { // Convert JSONRPCError to McpError const errorMessage = message; const error = new McpError(errorMessage.error.code, errorMessage.error.message, errorMessage.error.data); resolver(error); } } else { // Handle missing resolver gracefully with error logging const messageType = queuedMessage.type === 'response' ? 'Response' : 'Error'; this._onerror(new Error(`${messageType} handler missing for request ${requestId}`)); } // Continue to next message continue; } // Send the message on the response stream by passing the relatedRequestId // This tells the transport to write the message to the tasks/result response stream await ((_a = this._transport) === null || _a === void 0 ? void 0 : _a.send(queuedMessage.message, { relatedRequestId: extra.requestId })); } } // Now check task status const task = await this._taskStore.getTask(taskId, extra.sessionId); if (!task) { throw new McpError(ErrorCode.InvalidParams, `Task not found: ${taskId}`); } // Block if task is not terminal (we've already delivered all queued messages above) if (!isTerminal(task.status)) { // Wait for status change or new messages await this._waitForTaskUpdate(taskId, extra.signal); // After waking up, recursively call to deliver any new messages or result return await handleTaskResult(); } // If task is terminal, return the result if (isTerminal(task.status)) { const result = await this._taskStore.getTaskResult(taskId, extra.sessionId); this._clearTaskQueue(taskId); return { ...result, _meta: { ...result._meta, [RELATED_TASK_META_KEY]: { taskId: taskId } } }; } return await handleTaskResult(); }; return await handleTaskResult(); }); this.setRequestHandler(ListTasksRequestSchema, async (request, extra) => { var _a; try { const { tasks, nextCursor } = await this._taskStore.listTasks((_a = request.params) === null || _a === void 0 ? void 0 : _a.cursor, extra.sessionId); // @ts-expect-error SendResultT cannot contain ListTasksResult, but we include it in our derived types everywhere else return { tasks, nextCursor, _meta: {} }; } catch (error) { throw new McpError(ErrorCode.InvalidParams, `Failed to list tasks: ${error instanceof Error ? error.message : String(error)}`); } }); this.setRequestHandler(CancelTaskRequestSchema, async (request, extra) => { try { // Get the current task to check if it's in a terminal state, in case the implementation is not atomic const task = await this._taskStore.getTask(request.params.taskId, extra.sessionId); if (!task) { throw new McpError(ErrorCode.InvalidParams, `Task not found: ${request.params.taskId}`); } // Reject cancellation of terminal tasks if (isTerminal(task.status)) { throw new McpError(ErrorCode.InvalidParams, `Cannot cancel task in terminal status: ${task.status}`); } await this._taskStore.updateTaskStatus(request.params.taskId, 'cancelled', 'Client cancelled task execution.', extra.sessionId); this._clearTaskQueue(request.params.taskId); const cancelledTask = await this._taskStore.getTask(request.params.taskId, extra.sessionId); if (!cancelledTask) { // Task was deleted during cancellation (e.g., cleanup happened) throw new McpError(ErrorCode.InvalidParams, `Task not found after cancellation: ${request.params.taskId}`); } return { _meta: {}, ...cancelledTask }; } catch (error) { // Re-throw McpError as-is if (error instanceof McpError) { throw error; } throw new McpError(ErrorCode.InvalidRequest, `Failed to cancel task: ${error instanceof Error ? error.message : String(error)}`); } }); } } async _oncancel(notification) { // Handle request cancellation const controller = this._requestHandlerAbortControllers.get(notification.params.requestId); controller === null || controller === void 0 ? void 0 : controller.abort(notification.params.reason); } _setupTimeout(messageId, timeout, maxTotalTimeout, onTimeout, resetTimeoutOnProgress = false) { this._timeoutInfo.set(messageId, { timeoutId: setTimeout(onTimeout, timeout), startTime: Date.now(), timeout, maxTotalTimeout, resetTimeoutOnProgress, onTimeout }); } _resetTimeout(messageId) { const info = this._timeoutInfo.get(messageId); if (!info) return false; const totalElapsed = Date.now() - info.startTime; if (info.maxTotalTimeout && totalElapsed >= info.maxTotalTimeout) { this._timeoutInfo.delete(messageId); throw McpError.fromError(ErrorCode.RequestTimeout, 'Maximum total timeout exceeded', { maxTotalTimeout: info.maxTotalTimeout, totalElapsed }); } clearTimeout(info.timeoutId); info.timeoutId = setTimeout(info.onTimeout, info.timeout); return true; } _cleanupTimeout(messageId) { const info = this._timeoutInfo.get(messageId); if (info) { clearTimeout(info.timeoutId); this._timeoutInfo.delete(messageId); } } /** * 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) { var _a, _b, _c; this._transport = transport; const _onclose = (_a = this.transport) === null || _a === void 0 ? void 0 : _a.onclose; this._transport.onclose = () => { _onclose === null || _onclose === void 0 ? void 0 : _onclose(); this._onclose(); }; const _onerror = (_b = this.transport) === null || _b === void 0 ? void 0 : _b.onerror; this._transport.onerror = (error) => { _onerror === null || _onerror === void 0 ? void 0 : _onerror(error); this._onerror(error); }; const _onmessage = (_c = this._transport) === null || _c === void 0 ? void 0 : _c.onmessage; this._transport.onmessage = (message, extra) => { _onmessage === null || _onmessage === void 0 ? void 0 : _onmessage(message, extra); if (isJSONRPCResponse(message) || isJSONRPCError(message)) { this._onresponse(message); } else if (isJSONRPCRequest(message)) { this._onrequest(message, extra); } else if (isJSONRPCNotification(message)) { this._onnotification(message); } else { this._onerror(new Error(`Unknown message type: ${JSON.stringify(message)}`)); } }; await this._transport.start(); } _onclose() { var _a; const responseHandlers = this._responseHandlers; this._responseHandlers = new Map(); this._progressHandlers.clear(); this._taskProgressTokens.clear(); this._pendingDebouncedNotifications.clear(); const error = McpError.fromError(ErrorCode.ConnectionClosed, 'Connection closed'); this._transport = undefined; (_a = this.onclose) === null || _a === void 0 ? void 0 : _a.call(this); for (const handler of responseHandlers.values()) { handler(error); } } _onerror(error) { var _a; (_a = this.onerror) === null || _a === void 0 ? void 0 : _a.call(this, error); } _onnotification(notification) { var _a; const handler = (_a = this._notificationHandlers.get(notification.method)) !== null && _a !== void 0 ? _a : this.fallbackNotificationHandler; // Ignore notifications not being subscribed to. if (handler === undefined) { return; } // Starting with Promise.resolve() puts any synchronous errors into the monad as well. Promise.resolve() .then(() => handler(notification)) .catch(error => this._onerror(new Error(`Uncaught error in notification handler: ${error}`))); } _onrequest(request, extra) { var _a, _b, _c, _d, _e, _f; const handler = (_a = this._requestHandlers.get(request.method)) !== null && _a !== void 0 ? _a : this.fallbackRequestHandler; // Capture the current transport at request time to ensure responses go to the correct client const capturedTransport = this._transport; // Extract taskId from request metadata if present (needed early for method not found case) const relatedTaskId = (_d = (_c = (_b = request.params) === null || _b === void 0 ? void 0 : _b._meta) === null || _c === void 0 ? void 0 : _c[RELATED_TASK_META_KEY]) === null || _d === void 0 ? void 0 : _d.taskId; if (handler === undefined) { const errorResponse = { jsonrpc: '2.0', id: request.id, error: { code: ErrorCode.MethodNotFound, message: 'Method not found' } }; // Queue or send the error response based on whether this is a task-related request if (relatedTaskId && this._taskMessageQueue) { this._enqueueTaskMessage(relatedTaskId, { type: 'error', message: errorResponse, timestamp: Date.now() }, capturedTransport === null || capturedTransport === void 0 ? void 0 : capturedTransport.sessionId).catch(error => this._onerror(new Error(`Failed to enqueue error response: ${error}`))); } else { capturedTransport === null || capturedTransport === void 0 ? void 0 : capturedTransport.send(errorResponse).catch(error => this._onerror(new Error(`Failed to send an error response: ${error}`))); } return; } const abortController = new AbortController(); this._requestHandlerAbortControllers.set(request.id, abortController); const taskCreationParams = (_e = request.params) === null || _e === void 0 ? void 0 : _e.task; const taskStore = this._taskStore ? this.requestTaskStore(request, capturedTransport === null || capturedTransport === void 0 ? void 0 : capturedTransport.sessionId) : undefined; const fullExtra = { signal: abortController.signal, sessionId: capturedTransport === null || capturedTransport === void 0 ? void 0 : capturedTransport.sessionId, _meta: (_f = request.params) === null || _f === void 0 ? void 0 : _f._meta, sendNotification: async (notification) => { // Include related-task metadata if this request is part of a task const notificationOptions = { relatedRequestId: request.id }; if (relatedTaskId) { notificationOptions.relatedTask = { taskId: relatedTaskId }; } await this.notification(notification, notificationOptions); }, sendRequest: async (r, resultSchema, options) => { var _a, _b; // Include related-task metadata if this request is part of a task const 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 = (_b = (_a = requestOptions.relatedTask) === null || _a === void 0 ? void 0 : _a.taskId) !== null && _b !== void 0 ? _b : relatedTaskId; if (effectiveTaskId && taskStore) { await taskStore.updateTaskStatus(effectiveTaskId, 'input_required'); } return await this.request(r, resultSchema, requestOptions); }, authInfo: extra === null || extra === void 0 ? void 0 : extra.authInfo, requestId: request.id, requestInfo: extra === null || extra === void 0 ? void 0 : extra.requestInfo, taskId: relatedTaskId, taskStore: taskStore, taskRequestedTtl: taskCreationParams === null || taskCreationParams === void 0 ? void 0 : taskCreationParams.ttl, closeSSEStream: extra === null || extra === void 0 ? void 0 : extra.closeSSEStream, closeStandaloneSSEStream: extra === null || extra === void 0 ? void 0 : extra.closeStandaloneSSEStream }; // Starting with Promise.resolve() puts any synchronous errors into the monad as well. Promise.resolve() .then(() => { // If this request asked for task creation, check capability first if (taskCreationParams) { // Check if the request method supports task creation this.assertTaskHandlerCapability(request.method); } }) .then(() => handler(request, fullExtra)) .then(async (result) => { if (abortController.signal.aborted) { // Request was cancelled return; } const response = { result, jsonrpc: '2.0', id: request.id }; // Queue or send the response based on whether this is a task-related request if (relatedTaskId && this._taskMessageQueue) { await this._enqueueTaskMessage(relatedTaskId, { type: 'response', message: response, timestamp: Date.now() }, capturedTransport === null || capturedTransport === void 0 ? void 0 : capturedTransport.sessionId); } else { await (capturedTransport === null || capturedTransport === void 0 ? void 0 : capturedTransport.send(response)); } }, async (error) => { var _a; if (abortController.signal.aborted) { // Request was cancelled return; } const errorResponse = { jsonrpc: '2.0', id: request.id, error: { code: Number.isSafeInteger(error['code']) ? error['code'] : ErrorCode.InternalError, message: (_a = error.message) !== null && _a !== void 0 ? _a : 'Internal error', ...(error['data'] !== undefined && { data: error['data'] }) } }; // Queue or send the error response based on whether this is a task-related request if (relatedTaskId && this._taskMessageQueue) { await this._enqueueTaskMessage(relatedTaskId, { type: 'error', message: errorResponse, timestamp: Date.now() }, capturedTransport === null || capturedTransport === void 0 ? void 0 : capturedTransport.sessionId); } else { await (capturedTransport === null || capturedTransport === void 0 ? void 0 : capturedTransport.send(errorResponse)); } }) .catch(error => this._onerror(new Error(`Failed to send response: ${error}`))) .finally(() => { this._requestHandlerAbortControllers.delete(request.id); }); } _onprogress(notification) { const { progressToken, ...params } = notification.params; const messageId = Number(progressToken); const handler = this._progressHandlers.get(messageId); if (!handler) { this._onerror(new Error(`Received a progress notification for an unknown token: ${JSON.stringify(notification)}`)); return; } const responseHandler = this._responseHandlers.get(messageId); const timeoutInfo = this._timeoutInfo.get(messageId); if (timeoutInfo && responseHandler && timeoutInfo.resetTimeoutOnProgress) { try { this._resetTimeout(messageId); } catch (error) { // Clean up if maxTotalTimeout was exceeded this._responseHandlers.delete(messageId); this._progressHandlers.delete(messageId); this._cleanupTimeout(messageId); responseHandler(error); return; } } handler(params); } _onresponse(response) { const messageId = Number(response.id); // Check if this is a response to a queued request const resolver = this._requestResolvers.get(messageId); if (resolver) { this._requestResolvers.delete(messageId); if (isJSONRPCResponse(response)) { resolver(response); } else { const error = new McpError(response.error.code, response.error.message, response.error.data); resolver(error); } return; } const handler = this._responseHandlers.get(messageId); if (handler === undefined) { this._onerror(new Error(`Received a response for an unknown message ID: ${JSON.stringify(response)}`)); return; } this._responseHandlers.delete(messageId); this._cleanupTimeout(messageId); // Keep progress handler alive for CreateTaskResult responses let isTaskResponse = false; if (isJSONRPCResponse(response) && response.result && typeof response.result === 'object') { const result = response.result; if (result.task && typeof result.task === 'object') { const task = result.task; if (typeof task.taskId === 'string') { isTaskResponse = true; this._taskProgressTokens.set(task.taskId, messageId); } } } if (!isTaskResponse) { this._progressHandlers.delete(messageId); } if (isJSONRPCResponse(response)) { handler(response); } else { const error = McpError.fromError(response.error.code, response.error.message, response.error.data); handler(error); } } get transport() { return this._transport; } /** * Closes the connection. */ async close() { var _a; await ((_a = this._transport) === null || _a === void 0 ? void 0 : _a.close()); } /** * Sends a request and returns an AsyncGenerator that yields response messages. * The generator is guaranteed to end with either a 'result' or 'error' message. * * @example * ```typescript * const stream = protocol.requestStream(request, resultSchema, options); * for await (const message of stream) { * switch (message.type) { * case 'taskCreated': * console.log('Task created:', message.task.taskId); * break; * case 'taskStatus': * console.log('Task status:', message.task.status); * break; * case 'result': * console.log('Final result:', message.result); * break; * case 'error': * console.error('Error:', message.error); * break; * } * } * ``` * * @experimental Use `client.experimental.tasks.requestStream()` to access this method. */ async *requestStream(request, resultSchema, options) { var _a, _b, _c, _d; const { task } = options !== null && options !== void 0 ? options : {}; // For non-task requests, just yield the result if (!task) { try { const result = await this.request(request, resultSchema, options); yield { type: 'result', result }; } catch (error) { yield { type: 'error', error: error instanceof McpError ? error : new McpError(ErrorCode.InternalError, String(error)) }; } return; } // For task-augmented requests, we need to poll for status // First, make the request to create the task let taskId; try { // Send the request and get the CreateTaskResult const createResult = await this.request(request, CreateTaskResultSchema, options); // Extract taskId from the result if (createResult.task) { taskId = createResult.task.taskId; yield { type: 'taskCreated', task: createResult.task }; } else { throw new McpError(ErrorCode.InternalError, 'Task creation did not return a task'); } // Poll for task completion while (true) { // Get current task status const task = await this.getTask({ taskId }, options); yield { type: 'taskStatus', task }; // Check if task is terminal if (isTerminal(task.status)) { if (task.status === 'completed') { // Get the final result const result = await this.getTaskResult({ taskId }, resultSchema, options); yield { type: 'result', result }; } else if (task.status === 'failed') { yield { type: 'error', error: new McpError(ErrorCode.InternalError, `Task ${taskId} failed`) }; } else if (task.status === 'cancelled') { yield { type: 'error', error: new McpError(ErrorCode.InternalError, `Task ${taskId} was cancelled`) }; } return; } // When input_required, call tasks/result to deliver queued messages // (elicitation, sampling) via SSE and block until terminal if (task.status === 'input_required') { const result = await this.getTaskResult({ taskId }, resultSchema, options); yield { type: 'result', result }; return; } // Wait before polling again const pollInterval = (_c = (_a = task.pollInterval) !== null && _a !== void 0 ? _a : (_b = this._options) === null || _b === void 0 ? void 0 : _b.defaultTaskPollInterval) !== null && _c !== void 0 ? _c : 1000; await new Promise(resolve => setTimeout(resolve, pollInterval)); // Check if cancelled (_d = options === null || options === void 0 ? void 0 : options.signal) === null || _d === void 0 ? void 0 : _d.throwIfAborted(); } } catch (error) { yield { type: 'error', error: error instanceof McpError ? error : new McpError(ErrorCode.InternalError, String(error)) }; } } /** * Sends a request and waits for a response. * * Do not use this method to emit notifications! Use notification() instead. */ request(request, resultSchema, options) { const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options !== null && options !== void 0 ? options : {}; // Send the request return new Promise((resolve, reject) => { var _a, _b, _c, _d, _e, _f, _g; const earlyReject = (error) => { reject(error); }; if (!this._transport) { earlyReject(new Error('Not connected')); return; } if (((_a = this._options) === null || _a === void 0 ? void 0 : _a.enforceStrictCapabilities) === true) { try { this.assertCapabilityForMethod(request.method); // If task creation is requested, also check task capabilities if (task) { this.assertTaskCapability(request.method); } } catch (e) { earlyReject(e); return; } } (_b = options === null || options === void 0 ? void 0 : options.signal) === null || _b === void 0 ? void 0 : _b.throwIfAborted(); const messageId = this._requestMessageId++; const jsonrpcRequest = { ...request, jsonrpc: '2.0', id: messageId }; if (options === null || options === void 0 ? void 0 : options.onprogress) { this._progressHandlers.set(messageId, options.onprogress); jsonrpcRequest.params = { ...request.params, _meta: { ...(((_c = request.params) === null || _c === void 0 ? void 0 : _c._meta) || {}), progressToken: messageId } }; } // Augment with task creation parameters if provided if (task) { jsonrpcRequest.params = { ...jsonrpcRequest.params, task: task }; } // Augment with related task metadata if relatedTask is provided if (relatedTask) { jsonrpcRequest.params = { ...jsonrpcRequest.params, _meta: { ...(((_d = jsonrpcRequest.params) === null || _d === void 0 ? void 0 : _d._meta) || {}), [RELATED_TASK_META_KEY]: relatedTask } }; } const cancel = (reason) => { var _a; this._responseHandlers.delete(messageId); this._progressHandlers.delete(messageId); this._cleanupTimeout(messageId); (_a = this._transport) === null || _a === void 0 ? void 0 : _a.send({ jsonrpc: '2.0', method: 'notifications/cancelled', params: { requestId: messageId, reason: String(reason) } }, { relatedRequestId, resumptionToken, onresumptiontoken }).catch(error => this._onerror(new Error(`Failed to send cancellation: ${error}`))); // Wrap the reason in an McpError if it isn't already const error = reason instanceof McpError ? reason : new McpError(ErrorCode.RequestTimeout, String(reason)); reject(error); }; this._responseHandlers.set(messageId, response => { var _a; if ((_a = options === null || options === void 0 ? void 0 : options.signal) === null || _a === void 0 ? void 0 : _a.aborted) { return; } if (response instanceof Error) { return reject(response); } try { const parseResult = safeParse(resultSchema, response.result); if (!parseResult.success) { // Type guard: if success is false, error is guaranteed to exist reject(parseResult.error); } else { resolve(parseResult.data); } } catch (error) { reject(error); } }); (_e = options === null || options === void 0 ? void 0 : options.signal) === null || _e === void 0 ? void 0 : _e.addEventListener('abort', () => { var _a; cancel((_a = options === null || options === void 0 ? void 0 : options.signal) === null || _a === void 0 ? void 0 : _a.reason); }); const timeout = (_f = options === null || options === void 0 ? void 0 : options.timeout) !== null && _f !== void 0 ? _f : DEFAULT_REQUEST_TIMEOUT_MSEC; const timeoutHandler = () => cancel(McpError.fromError(ErrorCode.RequestTimeout, 'Request timed out', { timeout })); this._setupTimeout(messageId, timeout, options === null || options === void 0 ? void 0 : options.maxTotalTimeout, timeoutHandler, (_g = options === null || options === void 0 ? void 0 : options.resetTimeoutOnProgress) !== null && _g !== void 0 ? _g : false); // Queue request if related to a task const relatedTaskId = relatedTask === null || relatedTask === void 0 ? void 0 : relatedTask.taskId; if (relatedTaskId) { // Store the response resolver for this request so responses can be routed back const responseResolver = (response) => { const handler = this._responseHandlers.get(messageId); if (handler) { handler(response); } else { // Log error when resolver is missing, but don't fail this._onerror(new Error(`Response handler missing for side-channeled request ${messageId}`)); } }; this._requestResolvers.set(messageId, responseResolver); this._enqueueTaskMessage(relatedTaskId, { type: 'request', message: jsonrpcRequest, timestamp: Date.now() }).catch(error => { this._cleanupTimeout(messageId); reject(error); }); // Don't send through transport - queued messages are delivered via tasks/result only // This prevents duplicate delivery for bidirectional transports } else { // No related task - send through transport normally this._transport.send(jsonrpcRequest, { relatedRequestId, resumptionToken, onresumptiontoken }).catch(error => { this._cleanupTimeout(messageId); reject(error); }); } }); } /** * Gets the current status of a task. * * @experimental Use `client.experimental.tasks.getTask()` to access this method. */ async getTask(params, options) { // @ts-expect-error SendRequestT cannot directly contain GetTaskRequest, but we ensure all type instantiations contain it anyways return this.request({ method: 'tasks/get', params }, GetTaskResultSchema, options); } /** * Retrieves the result of a completed task. * * @experimental Use `client.experimental.tasks.getTaskResult()` to access this method. */ async getTaskResult(params, resultSchema, options) { // @ts-expect-error SendRequestT cannot directly contain GetTaskPayloadRequest, but we ensure all type instantiations contain it anyways return this.request({ method: 'tasks/result', params }, resultSchema, options); } /** * Lists tasks, optionally starting from a pagination cursor. * * @experimental Use `client.experimental.tasks.listTasks()` to access this method. */ async listTasks(params, options) { // @ts-expect-error SendRequestT cannot directly contain ListTasksRequest, but we ensure all type instantiations contain it anyways return this.request({ method: 'tasks/list', params }, ListTasksResultSchema, options); } /** * Cancels a specific task. * * @experimental Use `client.experimental.tasks.cancelTask()` to access this method. */ async cancelTask(params, options) { // @ts-expect-error SendRequestT cannot directly contain CancelTaskRequest, but we ensure all type instantiations contain it anyways return this.request({ method: 'tasks/cancel', params }, CancelTaskResultSchema, options); } /** * Emits a notification, which is a one-way message that does not expect a response. */ async notification(notification, options) { var _a, _b, _c, _d, _e; if (!this._transport) { throw new Error('Not connected'); } this.assertNotificationCapability(notification.method); // Queue notification if related to a task const relatedTaskId = (_a = options === null || options === void 0 ? void 0 : options.relatedTask) === null || _a === void 0 ? void 0 : _a.taskId; if (relatedTaskId) { // Build the JSONRPC notification with metadata const jsonrpcNotification = { ...notification, jsonrpc: '2.0', params: { ...notification.params, _meta: { ...(((_b = notification.params) === null || _b === void 0 ? void 0 : _b._meta) || {}), [RELATED_TASK_META_KEY]: options.relatedTask } } }; await this._enqueueTaskMessage(relatedTaskId, { type: 'notification', message: jsonrpcNotification, timestamp: Date.now() }); // Don't send through transport - queued messages are delivered via tasks/result only // This prevents duplicate delivery for bidirectional transports return; } const debouncedMethods = (_d = (_c = this._options) === null || _c === void 0 ? void 0 : _c.debouncedNotificationMethods) !== null && _d !== void 0 ? _d : []; // A notification can only be debounced if it's in the list AND it's "simple" // (i.e., has no parameters and no related request ID or related task that could be lost). const canDebounce = debouncedMethods.includes(notification.method) && !notification.params && !(options === null || options === void 0 ? void 0 : options.relatedRequestId) && !(options === null || options === void 0 ? void 0 : options.relatedTask); if (canDebounce) { // If a notification of this type is already scheduled, do nothing. if (this._pendingDebouncedNotifications.has(notification.method)) { return; } // Mark this notification type as pending. this._pendingDebouncedNotifications.add(notification.method); // Schedule the actual send to happen in the next microtask. // This allows all synchronous calls in the current event loop tick to be coalesced. Promise.resolve().then(() => { var _a, _b; // Un-mark the notification so the next one can be scheduled. this._pendingDebouncedNotifications.delete(notification.method); // SAFETY CHECK: If the connection was closed while this was pending, abort. if (!this._transport) { return; } let jsonrpcNotification = { ...notification, jsonrpc: '2.0' }; // Augment with related task metadata if relatedTask is provided if (options === null || options === void 0 ? void 0 : options.relatedTask) { jsonrpcNotification = { ...jsonrpcNotification, params: { ...jsonrpcNotification.params, _meta: { ...(((_a = jsonrpcNotification.params) === null || _a === void 0 ? void 0 : _a._meta) || {}), [RELATED_TASK_META_KEY]: options.relatedTask } } }; } // Send the notification, but don't await it here to avoid blocking. // Handle potential errors with a .catch(). (_b = this._transport) === null || _b === void 0 ? void 0 : _b.send(jsonrpcNotification, options).catch(error => this._onerror(error)); }); // Return immediately. return; } let jsonrpcNotification = { ...notification, jsonrpc: '2.0' }; // Augment with related task metadata if relatedTask is provided if (options === null || options === void 0 ? void 0 : options.relatedTask) { jsonrpcNotification = { ...jsonrpcNotification, params: { ...jsonrpcNotification.params, _meta: { ...(((_e = jsonrpcNotification.params) === null || _e === void 0 ? void 0 : _e._meta) || {}), [RELATED_TASK_META_KEY]: options.relatedTask } } }; } await this._transport.send(jsonrpcNotification, options); } /** * 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(requestSchema, handler) { const method = getMethodLiteral(requestSchema); this.assertRequestHandlerCapability(method); this._requestHandlers.set(method, (request, extra) => { const parsed = parseWithCompat(requestSchema, request); return Promise.resolve(handler(parsed, extra)); }); } /** * Removes the request handler for the given method. */ removeRequestHandler(method) { this._requestHandlers.delete(method); } /** * Asserts that a request handler has not already been set for the given method, in preparation for a new one being automatically installed. */ assertCanSetRequestHandler(method) { if (this._requestHandlers.has(method)) { throw new Error(`A request handler for ${method} already exists, which would be overridden`); } } /** * 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(notificationSchema, handler) { const method = getMethodLiteral(notificationSchema); this._notificationHandlers.set(method, notification => { const parsed = parseWithCompat(notificationSchema, notification); return Promise.resolve(handler(parsed)); }); } /** * Removes the notification handler for the given method. */ removeNotificationHandler(method) { this._notificationHandlers.delete(method); } /** * Cleans up the progress handler associated with a task. * This should be called when a task reaches a terminal status. */ _cleanupTaskProgressHandler(taskId) { const progressToken = this._taskProgressTokens.get(taskId); if (progressToken !== undefined) { this._progressHandlers.delete(progressToken); this._taskProgressTokens.delete(taskId); } } /** * Enqueues a task-related message for side-channel delivery via tasks/result. * @param taskId The task ID to associate the message with * @param message The message to enqueue * @param sessionId Optional session ID for binding the operation to a specific session * @throws Error if taskStore is not configured or if enqueue fails (e.g., queue overflow) * * Note: If enqueue fails, it's the TaskMessageQueue implementation's responsibility to handle * the error appropriately (e.g., by failing the task, logging, etc.). The Protocol layer * simply propagates the error. */ async _enqueueTaskMessage(taskId, message, sessionId) { var _a; // Task message queues are only used when taskStore is configured if (!this._taskStore || !this._taskMessageQueue) { throw new Error('Cannot enqueue task message: taskStore and taskMessageQueue are not configured'); } const maxQueueSize = (_a = this._options) === null || _a === void 0 ? void 0 : _a.maxTaskQueueSize; await this._taskMessageQueue.enqueue(taskId, message, sessionId, maxQueueSize); } /** * Clears the message queue for a task and rejects any pending request resolvers. * @param taskId The task ID whose queue should be cleared * @param sessionId Optional session ID for binding the operation to a specific session */ async _clearTaskQueue(taskId, sessionId) { if (this._taskMessageQueue) { // Reject any pending request resolvers const messages = await this._taskMessageQueue.dequeueAll(taskId, sessionId); for (const message of messages) { if (message.type === 'request' && isJSONRPCRequest(message.message)) { // Extract request ID from the message const requestId = message.message.id; const resolver = this._requestResolvers.get(requestId); if (resolver) { resolver(new McpError(ErrorCode.InternalError, 'Task cancelled or completed')); this._requestResolvers.delete(requestId); } else { // Log error when resolver is missing during cleanup for better observability this._onerror(new Error(`Resolver missing for request ${requestId} during task ${taskId} cleanup`)); } } } } } /** * Waits for a task update (new messages or status change) with abort signal support. * Uses polling to check for updates at the task's configured poll interval. * @param taskId The task ID to wait for * @param signal Abort signal to cancel the wait * @returns Promise that resolves when an update occurs or rejects if aborted */ async _waitForTaskUpdate(taskId, signal) { var _a, _b, _c; // Get the task's poll interval, falling back to default let interval = (_b = (_a = this._options) === null || _a === void 0 ? void 0 : _a.defaultTaskPollInterval) !== null && _b !== void 0 ? _b : 1000; try { const task = await ((_c = this._taskStore) === null || _c === void 0 ? void 0 : _c.getTask(taskId)); if (task === null || task === void 0 ? void 0 : task.pollInterval) { interval = task.pollInterval; } } catch (_d) { // Use default interval if task lookup fails } return new Promise((resolve, reject) => { if (signal.aborted) { reject(ne