| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112 |
- "use strict";
- Object.defineProperty(exports, "__esModule", { value: true });
- exports.Protocol = exports.DEFAULT_REQUEST_TIMEOUT_MSEC = void 0;
- exports.mergeCapabilities = mergeCapabilities;
- const zod_compat_js_1 = require("../server/zod-compat.js");
- const types_js_1 = require("../types.js");
- const interfaces_js_1 = require("../experimental/tasks/interfaces.js");
- const zod_json_schema_compat_js_1 = require("../server/zod-json-schema-compat.js");
- /**
- * The default request timeout, in miliseconds.
- */
- exports.DEFAULT_REQUEST_TIMEOUT_MSEC = 60000;
- /**
- * Implements MCP protocol framing on top of a pluggable transport, including
- * features like request/response linking, notifications, and progress.
- */
- 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(types_js_1.CancelledNotificationSchema, notification => {
- this._oncancel(notification);
- });
- this.setNotificationHandler(types_js_1.ProgressNotificationSchema, notification => {
- this._onprogress(notification);
- });
- this.setRequestHandler(types_js_1.PingRequestSchema,
- // Automatic pong by default.
- _request => ({}));
- // Install task handlers if TaskStore is provided
- this._taskStore = _options?.taskStore;
- this._taskMessageQueue = _options?.taskMessageQueue;
- if (this._taskStore) {
- this.setRequestHandler(types_js_1.GetTaskRequestSchema, async (request, extra) => {
- const task = await this._taskStore.getTask(request.params.taskId, extra.sessionId);
- if (!task) {
- throw new types_js_1.McpError(types_js_1.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(types_js_1.GetTaskPayloadRequestSchema, async (request, extra) => {
- const handleTaskResult = async () => {
- 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 types_js_1.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 this._transport?.send(queuedMessage.message, { relatedRequestId: extra.requestId });
- }
- }
- // Now check task status
- const task = await this._taskStore.getTask(taskId, extra.sessionId);
- if (!task) {
- throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidParams, `Task not found: ${taskId}`);
- }
- // Block if task is not terminal (we've already delivered all queued messages above)
- if (!(0, interfaces_js_1.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 ((0, interfaces_js_1.isTerminal)(task.status)) {
- const result = await this._taskStore.getTaskResult(taskId, extra.sessionId);
- this._clearTaskQueue(taskId);
- return {
- ...result,
- _meta: {
- ...result._meta,
- [types_js_1.RELATED_TASK_META_KEY]: {
- taskId: taskId
- }
- }
- };
- }
- return await handleTaskResult();
- };
- return await handleTaskResult();
- });
- this.setRequestHandler(types_js_1.ListTasksRequestSchema, async (request, extra) => {
- try {
- const { tasks, nextCursor } = await this._taskStore.listTasks(request.params?.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 types_js_1.McpError(types_js_1.ErrorCode.InvalidParams, `Failed to list tasks: ${error instanceof Error ? error.message : String(error)}`);
- }
- });
- this.setRequestHandler(types_js_1.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 types_js_1.McpError(types_js_1.ErrorCode.InvalidParams, `Task not found: ${request.params.taskId}`);
- }
- // Reject cancellation of terminal tasks
- if ((0, interfaces_js_1.isTerminal)(task.status)) {
- throw new types_js_1.McpError(types_js_1.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 types_js_1.McpError(types_js_1.ErrorCode.InvalidParams, `Task not found after cancellation: ${request.params.taskId}`);
- }
- return {
- _meta: {},
- ...cancelledTask
- };
- }
- catch (error) {
- // Re-throw McpError as-is
- if (error instanceof types_js_1.McpError) {
- throw error;
- }
- throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidRequest, `Failed to cancel task: ${error instanceof Error ? error.message : String(error)}`);
- }
- });
- }
- }
- async _oncancel(notification) {
- if (!notification.params.requestId) {
- return;
- }
- // Handle request cancellation
- const controller = this._requestHandlerAbortControllers.get(notification.params.requestId);
- 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 types_js_1.McpError.fromError(types_js_1.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) {
- if (this._transport) {
- throw new Error('Already connected to a transport. Call close() before connecting to a new transport, or use a separate Protocol instance per connection.');
- }
- this._transport = transport;
- const _onclose = this.transport?.onclose;
- this._transport.onclose = () => {
- _onclose?.();
- this._onclose();
- };
- const _onerror = this.transport?.onerror;
- this._transport.onerror = (error) => {
- _onerror?.(error);
- this._onerror(error);
- };
- const _onmessage = this._transport?.onmessage;
- this._transport.onmessage = (message, extra) => {
- _onmessage?.(message, extra);
- if ((0, types_js_1.isJSONRPCResultResponse)(message) || (0, types_js_1.isJSONRPCErrorResponse)(message)) {
- this._onresponse(message);
- }
- else if ((0, types_js_1.isJSONRPCRequest)(message)) {
- this._onrequest(message, extra);
- }
- else if ((0, types_js_1.isJSONRPCNotification)(message)) {
- this._onnotification(message);
- }
- else {
- this._onerror(new Error(`Unknown message type: ${JSON.stringify(message)}`));
- }
- };
- await this._transport.start();
- }
- _onclose() {
- const responseHandlers = this._responseHandlers;
- this._responseHandlers = new Map();
- this._progressHandlers.clear();
- this._taskProgressTokens.clear();
- this._pendingDebouncedNotifications.clear();
- for (const info of this._timeoutInfo.values()) {
- clearTimeout(info.timeoutId);
- }
- this._timeoutInfo.clear();
- // Abort all in-flight request handlers so they stop sending messages
- for (const controller of this._requestHandlerAbortControllers.values()) {
- controller.abort();
- }
- this._requestHandlerAbortControllers.clear();
- const error = types_js_1.McpError.fromError(types_js_1.ErrorCode.ConnectionClosed, 'Connection closed');
- this._transport = undefined;
- this.onclose?.();
- for (const handler of responseHandlers.values()) {
- handler(error);
- }
- }
- _onerror(error) {
- this.onerror?.(error);
- }
- _onnotification(notification) {
- const handler = this._notificationHandlers.get(notification.method) ?? 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) {
- const handler = this._requestHandlers.get(request.method) ?? 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 = request.params?._meta?.[types_js_1.RELATED_TASK_META_KEY]?.taskId;
- if (handler === undefined) {
- const errorResponse = {
- jsonrpc: '2.0',
- id: request.id,
- error: {
- code: types_js_1.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?.sessionId).catch(error => this._onerror(new Error(`Failed to enqueue error response: ${error}`)));
- }
- else {
- 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 = (0, types_js_1.isTaskAugmentedRequestParams)(request.params) ? request.params.task : undefined;
- const taskStore = this._taskStore ? this.requestTaskStore(request, capturedTransport?.sessionId) : undefined;
- const fullExtra = {
- signal: abortController.signal,
- sessionId: capturedTransport?.sessionId,
- _meta: request.params?._meta,
- sendNotification: async (notification) => {
- if (abortController.signal.aborted)
- return;
- // 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) => {
- if (abortController.signal.aborted) {
- throw new types_js_1.McpError(types_js_1.ErrorCode.ConnectionClosed, 'Request was cancelled');
- }
- // 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 = requestOptions.relatedTask?.taskId ?? relatedTaskId;
- if (effectiveTaskId && taskStore) {
- await taskStore.updateTaskStatus(effectiveTaskId, 'input_required');
- }
- return await this.request(r, resultSchema, requestOptions);
- },
- authInfo: extra?.authInfo,
- requestId: request.id,
- requestInfo: extra?.requestInfo,
- taskId: relatedTaskId,
- taskStore: taskStore,
- taskRequestedTtl: taskCreationParams?.ttl,
- closeSSEStream: extra?.closeSSEStream,
- closeStandaloneSSEStream: extra?.closeStandaloneSSEStream
- };
- // 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?.sessionId);
- }
- else {
- await capturedTransport?.send(response);
- }
- }, async (error) => {
- if (abortController.signal.aborted) {
- // Request was cancelled
- return;
- }
- const errorResponse = {
- jsonrpc: '2.0',
- id: request.id,
- error: {
- code: Number.isSafeInteger(error['code']) ? error['code'] : types_js_1.ErrorCode.InternalError,
- message: error.message ?? '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?.sessionId);
- }
- else {
- await capturedTransport?.send(errorResponse);
- }
- })
- .catch(error => this._onerror(new Error(`Failed to send response: ${error}`)))
- .finally(() => {
- // Only delete if the stored controller is still ours; after close()+connect(),
- // a new connection may have reused the same request ID with a different controller.
- if (this._requestHandlerAbortControllers.get(request.id) === abortController) {
- 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 ((0, types_js_1.isJSONRPCResultResponse)(response)) {
- resolver(response);
- }
- else {
- const error = new types_js_1.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 ((0, types_js_1.isJSONRPCResultResponse)(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 ((0, types_js_1.isJSONRPCResultResponse)(response)) {
- handler(response);
- }
- else {
- const error = types_js_1.McpError.fromError(response.error.code, response.error.message, response.error.data);
- handler(error);
- }
- }
- get transport() {
- return this._transport;
- }
- /**
- * Closes the connection.
- */
- async close() {
- await this._transport?.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) {
- const { task } = 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 types_js_1.McpError ? error : new types_js_1.McpError(types_js_1.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, types_js_1.CreateTaskResultSchema, options);
- // Extract taskId from the result
- if (createResult.task) {
- taskId = createResult.task.taskId;
- yield { type: 'taskCreated', task: createResult.task };
- }
- else {
- throw new types_js_1.McpError(types_js_1.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 ((0, interfaces_js_1.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 types_js_1.McpError(types_js_1.ErrorCode.InternalError, `Task ${taskId} failed`)
- };
- }
- else if (task.status === 'cancelled') {
- yield {
- type: 'error',
- error: new types_js_1.McpError(types_js_1.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 = task.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1000;
- await new Promise(resolve => setTimeout(resolve, pollInterval));
- // Check if cancelled
- options?.signal?.throwIfAborted();
- }
- }
- catch (error) {
- yield {
- type: 'error',
- error: error instanceof types_js_1.McpError ? error : new types_js_1.McpError(types_js_1.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 ?? {};
- // Send the request
- return new Promise((resolve, reject) => {
- const earlyReject = (error) => {
- reject(error);
- };
- if (!this._transport) {
- earlyReject(new Error('Not connected'));
- return;
- }
- if (this._options?.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;
- }
- }
- options?.signal?.throwIfAborted();
- const messageId = this._requestMessageId++;
- const jsonrpcRequest = {
- ...request,
- jsonrpc: '2.0',
- id: messageId
- };
- if (options?.onprogress) {
- this._progressHandlers.set(messageId, options.onprogress);
- jsonrpcRequest.params = {
- ...request.params,
- _meta: {
- ...(request.params?._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: {
- ...(jsonrpcRequest.params?._meta || {}),
- [types_js_1.RELATED_TASK_META_KEY]: relatedTask
- }
- };
- }
- const cancel = (reason) => {
- this._responseHandlers.delete(messageId);
- this._progressHandlers.delete(messageId);
- this._cleanupTimeout(messageId);
- this._transport
- ?.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 types_js_1.McpError ? reason : new types_js_1.McpError(types_js_1.ErrorCode.RequestTimeout, String(reason));
- reject(error);
- };
- this._responseHandlers.set(messageId, response => {
- if (options?.signal?.aborted) {
- return;
- }
- if (response instanceof Error) {
- return reject(response);
- }
- try {
- const parseResult = (0, zod_compat_js_1.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);
- }
- });
- options?.signal?.addEventListener('abort', () => {
- cancel(options?.signal?.reason);
- });
- const timeout = options?.timeout ?? exports.DEFAULT_REQUEST_TIMEOUT_MSEC;
- const timeoutHandler = () => cancel(types_js_1.McpError.fromError(types_js_1.ErrorCode.RequestTimeout, 'Request timed out', { timeout }));
- this._setupTimeout(messageId, timeout, options?.maxTotalTimeout, timeoutHandler, options?.resetTimeoutOnProgress ?? false);
- // Queue request if related to a task
- const relatedTaskId = 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 }, types_js_1.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 }, types_js_1.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 }, types_js_1.CancelTaskResultSchema, options);
- }
- /**
- * Emits a notification, which is a one-way message that does not expect a response.
- */
- async notification(notification, options) {
- if (!this._transport) {
- throw new Error('Not connected');
- }
- this.assertNotificationCapability(notification.method);
- // Queue notification if related to a task
- const relatedTaskId = options?.relatedTask?.taskId;
- if (relatedTaskId) {
- // Build the JSONRPC notification with metadata
- const jsonrpcNotification = {
- ...notification,
- jsonrpc: '2.0',
- params: {
- ...notification.params,
- _meta: {
- ...(notification.params?._meta || {}),
- [types_js_1.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 = this._options?.debouncedNotificationMethods ?? [];
- // 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?.relatedRequestId && !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(() => {
- // 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?.relatedTask) {
- jsonrpcNotification = {
- ...jsonrpcNotification,
- params: {
- ...jsonrpcNotification.params,
- _meta: {
- ...(jsonrpcNotification.params?._meta || {}),
- [types_js_1.RELATED_TASK_META_KEY]: options.relatedTask
- }
- }
- };
- }
- // Send the notification, but don't await it here to avoid blocking.
- // Handle potential errors with a .catch().
- this._transport?.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?.relatedTask) {
- jsonrpcNotification = {
- ...jsonrpcNotification,
- params: {
- ...jsonrpcNotification.params,
- _meta: {
- ...(jsonrpcNotification.params?._meta || {}),
- [types_js_1.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 = (0, zod_json_schema_compat_js_1.getMethodLiteral)(requestSchema);
- this.assertRequestHandlerCapability(method);
- this._requestHandlers.set(method, (request, extra) => {
- const parsed = (0, zod_json_schema_compat_js_1.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 = (0, zod_json_schema_compat_js_1.getMethodLiteral)(notificationSchema);
- this._notificationHandlers.set(method, notification => {
- const parsed = (0, zod_json_schema_compat_js_1.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) {
- // 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 = this._options?.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' && (0, types_js_1.isJSONRPCRequest)(message.message)) {
- // Extract request ID from the message
- const requestId = message.message.id;
- const resolver = this._requestResolvers.get(requestId);
- if (resolver) {
- resolver(new types_js_1.McpError(types_js_1.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) {
- // Get the task's poll interval, falling back to default
- let interval = this._options?.defaultTaskPollInterval ?? 1000;
- try {
- const task = await this._taskStore?.getTask(taskId);
- if (task?.pollInterval) {
- interval = task.pollInterval;
- }
- }
- catch {
- // Use default interval if task lookup fails
- }
- return new Promise((resolve, reject) => {
- if (signal.aborted) {
- reject(new types_js_1.McpError(types_js_1.ErrorCode.InvalidRequest, 'Request cancelled'));
- return;
- }
- // Wait for the poll interval, then resolve so caller can check for updates
- const timeoutId = setTimeout(resolve, interval);
- // Clean up timeout and reject if aborted
- signal.addEventListener('abort', () => {
- clearTimeout(timeoutId);
- reject(new types_js_1.McpError(types_js_1.ErrorCode.InvalidRequest, 'Request cancelled'));
- }, { once: true });
- });
- }
- requestTaskStore(request, sessionId) {
- const taskStore = this._taskStore;
- if (!taskStore) {
- throw new Error('No task store configured');
- }
- return {
- createTask: async (taskParams) => {
- if (!request) {
- throw new Error('No request provided');
- }
- return await taskStore.createTask(taskParams, request.id, {
- method: request.method,
- params: request.params
- }, sessionId);
- },
- getTask: async (taskId) => {
- const task = await taskStore.getTask(taskId, sessionId);
- if (!task) {
- throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidParams, 'Failed to retrieve task: Task not found');
- }
- return task;
- },
- storeTaskResult: async (taskId, status, result) => {
- await taskStore.storeTaskResult(taskId, status, result, sessionId);
- // Get updated task state and send notification
- const task = await taskStore.getTask(taskId, sessionId);
- if (task) {
- const notification = types_js_1.TaskStatusNotificationSchema.parse({
- method: 'notifications/tasks/status',
- params: task
- });
- await this.notification(notification);
- if ((0, interfaces_js_1.isTerminal)(task.status)) {
- this._cleanupTaskProgressHandler(taskId);
- // Don't clear queue here - it will be cleared after delivery via tasks/result
- }
- }
- },
- getTaskResult: taskId => {
- return taskStore.getTaskResult(taskId, sessionId);
- },
- updateTaskStatus: async (taskId, status, statusMessage) => {
- // Check if task exists
- const task = await taskStore.getTask(taskId, sessionId);
- if (!task) {
- throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidParams, `Task "${taskId}" not found - it may have been cleaned up`);
- }
- // Don't allow transitions from terminal states
- if ((0, interfaces_js_1.isTerminal)(task.status)) {
- throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidParams, `Cannot update task "${taskId}" from terminal status "${task.status}" to "${status}". Terminal states (completed, failed, cancelled) cannot transition to other states.`);
- }
- await taskStore.updateTaskStatus(taskId, status, statusMessage, sessionId);
- // Get updated task state and send notification
- const updatedTask = await taskStore.getTask(taskId, sessionId);
- if (updatedTask) {
- const notification = types_js_1.TaskStatusNotificationSchema.parse({
- method: 'notifications/tasks/status',
- params: updatedTask
- });
- await this.notification(notification);
- if ((0, interfaces_js_1.isTerminal)(updatedTask.status)) {
- this._cleanupTaskProgressHandler(taskId);
- // Don't clear queue here - it will be cleared after delivery via tasks/result
- }
- }
- },
- listTasks: cursor => {
- return taskStore.listTasks(cursor, sessionId);
- }
- };
- }
- }
- exports.Protocol = Protocol;
- function isPlainObject(value) {
- return value !== null && typeof value === 'object' && !Array.isArray(value);
- }
- function mergeCapabilities(base, additional) {
- const result = { ...base };
- for (const key in additional) {
- const k = key;
- const addValue = additional[k];
- if (addValue === undefined)
- continue;
- const baseValue = result[k];
- if (isPlainObject(baseValue) && isPlainObject(addValue)) {
- result[k] = { ...baseValue, ...addValue };
- }
- else {
- result[k] = addValue;
- }
- }
- return result;
- }
- //# sourceMappingURL=protocol.js.map
|