| "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");
|
| |
| |
|
|
| exports.DEFAULT_REQUEST_TIMEOUT_MSEC = 60000;
|
| |
| |
| |
|
|
| 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();
|
|
|
| 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,
|
|
|
| _request => ({}));
|
|
|
| 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');
|
| }
|
|
|
|
|
|
|
| return {
|
| ...task
|
| };
|
| });
|
| this.setRequestHandler(types_js_1.GetTaskPayloadRequestSchema, async (request, extra) => {
|
| const handleTaskResult = async () => {
|
| const taskId = request.params.taskId;
|
|
|
| if (this._taskMessageQueue) {
|
| let queuedMessage;
|
| while ((queuedMessage = await this._taskMessageQueue.dequeue(taskId, extra.sessionId))) {
|
|
|
| if (queuedMessage.type === 'response' || queuedMessage.type === 'error') {
|
| const message = queuedMessage.message;
|
| const requestId = message.id;
|
|
|
| const resolver = this._requestResolvers.get(requestId);
|
| if (resolver) {
|
|
|
| this._requestResolvers.delete(requestId);
|
|
|
| if (queuedMessage.type === 'response') {
|
| resolver(message);
|
| }
|
| else {
|
|
|
| const errorMessage = message;
|
| const error = new types_js_1.McpError(errorMessage.error.code, errorMessage.error.message, errorMessage.error.data);
|
| resolver(error);
|
| }
|
| }
|
| else {
|
|
|
| const messageType = queuedMessage.type === 'response' ? 'Response' : 'Error';
|
| this._onerror(new Error(`${messageType} handler missing for request ${requestId}`));
|
| }
|
|
|
| continue;
|
| }
|
|
|
|
|
| await this._transport?.send(queuedMessage.message, { relatedRequestId: extra.requestId });
|
| }
|
| }
|
|
|
| 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}`);
|
| }
|
|
|
| if (!(0, interfaces_js_1.isTerminal)(task.status)) {
|
|
|
| await this._waitForTaskUpdate(taskId, extra.signal);
|
|
|
| return await handleTaskResult();
|
| }
|
|
|
| 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);
|
|
|
| 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 {
|
|
|
| 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}`);
|
| }
|
|
|
| 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) {
|
|
|
| throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidParams, `Task not found after cancellation: ${request.params.taskId}`);
|
| }
|
| return {
|
| _meta: {},
|
| ...cancelledTask
|
| };
|
| }
|
| catch (error) {
|
|
|
| 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;
|
| }
|
|
|
| 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);
|
| }
|
| }
|
| |
| |
| |
| |
|
|
| 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();
|
|
|
| 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;
|
|
|
| if (handler === undefined) {
|
| return;
|
| }
|
|
|
| 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;
|
|
|
| const capturedTransport = this._transport;
|
|
|
| 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'
|
| }
|
| };
|
|
|
| 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;
|
|
|
| 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');
|
| }
|
|
|
| const requestOptions = { ...options, relatedRequestId: request.id };
|
| if (relatedTaskId && !requestOptions.relatedTask) {
|
| requestOptions.relatedTask = { taskId: relatedTaskId };
|
| }
|
|
|
|
|
| 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
|
| };
|
|
|
| Promise.resolve()
|
| .then(() => {
|
|
|
| if (taskCreationParams) {
|
|
|
| this.assertTaskHandlerCapability(request.method);
|
| }
|
| })
|
| .then(() => handler(request, fullExtra))
|
| .then(async (result) => {
|
| if (abortController.signal.aborted) {
|
|
|
| return;
|
| }
|
| const response = {
|
| result,
|
| jsonrpc: '2.0',
|
| id: request.id
|
| };
|
|
|
| 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) {
|
|
|
| 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'] })
|
| }
|
| };
|
|
|
| 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(() => {
|
|
|
|
|
| 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) {
|
|
|
| this._responseHandlers.delete(messageId);
|
| this._progressHandlers.delete(messageId);
|
| this._cleanupTimeout(messageId);
|
| responseHandler(error);
|
| return;
|
| }
|
| }
|
| handler(params);
|
| }
|
| _onresponse(response) {
|
| const messageId = Number(response.id);
|
|
|
| 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);
|
|
|
| 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;
|
| }
|
| |
| |
|
|
| async close() {
|
| await this._transport?.close();
|
| }
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| async *requestStream(request, resultSchema, options) {
|
| const { task } = options ?? {};
|
|
|
| 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;
|
| }
|
|
|
|
|
| let taskId;
|
| try {
|
|
|
| const createResult = await this.request(request, types_js_1.CreateTaskResultSchema, options);
|
|
|
| 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');
|
| }
|
|
|
| while (true) {
|
|
|
| const task = await this.getTask({ taskId }, options);
|
| yield { type: 'taskStatus', task };
|
|
|
| if ((0, interfaces_js_1.isTerminal)(task.status)) {
|
| if (task.status === 'completed') {
|
|
|
| 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;
|
| }
|
|
|
|
|
| if (task.status === 'input_required') {
|
| const result = await this.getTaskResult({ taskId }, resultSchema, options);
|
| yield { type: 'result', result };
|
| return;
|
| }
|
|
|
| const pollInterval = task.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1000;
|
| await new Promise(resolve => setTimeout(resolve, pollInterval));
|
|
|
| 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))
|
| };
|
| }
|
| }
|
| |
| |
| |
| |
|
|
| request(request, resultSchema, options) {
|
| const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
|
|
|
| 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) {
|
| 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
|
| }
|
| };
|
| }
|
|
|
| if (task) {
|
| jsonrpcRequest.params = {
|
| ...jsonrpcRequest.params,
|
| task: task
|
| };
|
| }
|
|
|
| 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}`)));
|
|
|
| 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) {
|
|
|
| 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);
|
|
|
| const relatedTaskId = relatedTask?.taskId;
|
| if (relatedTaskId) {
|
|
|
| const responseResolver = (response) => {
|
| const handler = this._responseHandlers.get(messageId);
|
| if (handler) {
|
| handler(response);
|
| }
|
| else {
|
|
|
| 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);
|
| });
|
|
|
|
|
| }
|
| else {
|
|
|
| this._transport.send(jsonrpcRequest, { relatedRequestId, resumptionToken, onresumptiontoken }).catch(error => {
|
| this._cleanupTimeout(messageId);
|
| reject(error);
|
| });
|
| }
|
| });
|
| }
|
| |
| |
| |
| |
|
|
| async getTask(params, options) {
|
|
|
| return this.request({ method: 'tasks/get', params }, types_js_1.GetTaskResultSchema, options);
|
| }
|
| |
| |
| |
| |
|
|
| async getTaskResult(params, resultSchema, options) {
|
|
|
| return this.request({ method: 'tasks/result', params }, resultSchema, options);
|
| }
|
| |
| |
| |
| |
|
|
| async listTasks(params, options) {
|
|
|
| return this.request({ method: 'tasks/list', params }, types_js_1.ListTasksResultSchema, options);
|
| }
|
| |
| |
| |
| |
|
|
| async cancelTask(params, options) {
|
|
|
| return this.request({ method: 'tasks/cancel', params }, types_js_1.CancelTaskResultSchema, options);
|
| }
|
| |
| |
|
|
| async notification(notification, options) {
|
| if (!this._transport) {
|
| throw new Error('Not connected');
|
| }
|
| this.assertNotificationCapability(notification.method);
|
|
|
| const relatedTaskId = options?.relatedTask?.taskId;
|
| if (relatedTaskId) {
|
|
|
| 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()
|
| });
|
|
|
|
|
| return;
|
| }
|
| const debouncedMethods = this._options?.debouncedNotificationMethods ?? [];
|
|
|
|
|
| const canDebounce = debouncedMethods.includes(notification.method) && !notification.params && !options?.relatedRequestId && !options?.relatedTask;
|
| if (canDebounce) {
|
|
|
| if (this._pendingDebouncedNotifications.has(notification.method)) {
|
| return;
|
| }
|
|
|
| this._pendingDebouncedNotifications.add(notification.method);
|
|
|
|
|
| Promise.resolve().then(() => {
|
|
|
| this._pendingDebouncedNotifications.delete(notification.method);
|
|
|
| if (!this._transport) {
|
| return;
|
| }
|
| let jsonrpcNotification = {
|
| ...notification,
|
| jsonrpc: '2.0'
|
| };
|
|
|
| if (options?.relatedTask) {
|
| jsonrpcNotification = {
|
| ...jsonrpcNotification,
|
| params: {
|
| ...jsonrpcNotification.params,
|
| _meta: {
|
| ...(jsonrpcNotification.params?._meta || {}),
|
| [types_js_1.RELATED_TASK_META_KEY]: options.relatedTask
|
| }
|
| }
|
| };
|
| }
|
|
|
|
|
| this._transport?.send(jsonrpcNotification, options).catch(error => this._onerror(error));
|
| });
|
|
|
| return;
|
| }
|
| let jsonrpcNotification = {
|
| ...notification,
|
| jsonrpc: '2.0'
|
| };
|
|
|
| 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);
|
| }
|
| |
| |
| |
| |
|
|
| 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));
|
| });
|
| }
|
| |
| |
|
|
| removeRequestHandler(method) {
|
| this._requestHandlers.delete(method);
|
| }
|
| |
| |
|
|
| assertCanSetRequestHandler(method) {
|
| if (this._requestHandlers.has(method)) {
|
| throw new Error(`A request handler for ${method} already exists, which would be overridden`);
|
| }
|
| }
|
| |
| |
| |
| |
|
|
| 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));
|
| });
|
| }
|
| |
| |
|
|
| removeNotificationHandler(method) {
|
| this._notificationHandlers.delete(method);
|
| }
|
| |
| |
| |
|
|
| _cleanupTaskProgressHandler(taskId) {
|
| const progressToken = this._taskProgressTokens.get(taskId);
|
| if (progressToken !== undefined) {
|
| this._progressHandlers.delete(progressToken);
|
| this._taskProgressTokens.delete(taskId);
|
| }
|
| }
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| async _enqueueTaskMessage(taskId, message, sessionId) {
|
|
|
| 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);
|
| }
|
| |
| |
| |
| |
|
|
| async _clearTaskQueue(taskId, sessionId) {
|
| if (this._taskMessageQueue) {
|
|
|
| const messages = await this._taskMessageQueue.dequeueAll(taskId, sessionId);
|
| for (const message of messages) {
|
| if (message.type === 'request' && (0, types_js_1.isJSONRPCRequest)(message.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 {
|
|
|
| this._onerror(new Error(`Resolver missing for request ${requestId} during task ${taskId} cleanup`));
|
| }
|
| }
|
| }
|
| }
|
| }
|
| |
| |
| |
| |
| |
| |
|
|
| async _waitForTaskUpdate(taskId, signal) {
|
|
|
| let interval = this._options?.defaultTaskPollInterval ?? 1000;
|
| try {
|
| const task = await this._taskStore?.getTask(taskId);
|
| if (task?.pollInterval) {
|
| interval = task.pollInterval;
|
| }
|
| }
|
| catch {
|
|
|
| }
|
| return new Promise((resolve, reject) => {
|
| if (signal.aborted) {
|
| reject(new types_js_1.McpError(types_js_1.ErrorCode.InvalidRequest, 'Request cancelled'));
|
| return;
|
| }
|
|
|
| const timeoutId = setTimeout(resolve, interval);
|
|
|
| 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);
|
|
|
| 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);
|
|
|
| }
|
| }
|
| },
|
| getTaskResult: taskId => {
|
| return taskStore.getTaskResult(taskId, sessionId);
|
| },
|
| updateTaskStatus: async (taskId, status, statusMessage) => {
|
|
|
| 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`);
|
| }
|
|
|
| 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);
|
|
|
| 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);
|
|
|
| }
|
| }
|
| },
|
| 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;
|
| }
|
| |