From be7ab37b8532a77b04db11999add0b5e1d337f9f Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Tue, 3 Feb 2026 12:24:37 +0200 Subject: [PATCH 1/3] progress manager split --- packages/core/src/index.ts | 1 + packages/core/src/shared/progressManager.ts | 172 ++++++++++++++++++++ packages/core/src/shared/protocol.ts | 39 ++--- packages/core/test/shared/protocol.test.ts | 12 +- 4 files changed, 193 insertions(+), 31 deletions(-) create mode 100644 packages/core/src/shared/progressManager.ts diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index b7980fadb..0c631c346 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -2,6 +2,7 @@ export * from './auth/errors.js'; export * from './shared/auth.js'; export * from './shared/authUtils.js'; export * from './shared/metadataUtils.js'; +export * from './shared/progressManager.js'; export * from './shared/protocol.js'; export * from './shared/responseMessage.js'; export * from './shared/stdio.js'; diff --git a/packages/core/src/shared/progressManager.ts b/packages/core/src/shared/progressManager.ts new file mode 100644 index 000000000..7d9af19a6 --- /dev/null +++ b/packages/core/src/shared/progressManager.ts @@ -0,0 +1,172 @@ +/** + * Progress Manager + * + * Manages progress tracking for the Protocol class. + * Extracted from Protocol to follow Single Responsibility Principle. + */ + +import type { Progress, ProgressNotification } from '../types/types.js'; + +/** + * Callback for progress notifications. + */ +export type ProgressCallback = (progress: Progress) => void; + +/** + * Manages progress tracking for requests. + * + * This class handles registration, lookup, and invocation of progress callbacks, + * as well as task-to-progress-token associations for long-running task operations. + * + * @example + * ```typescript + * const progressManager = new ProgressManager(); + * + * // Register a progress handler for a request + * progressManager.registerHandler(messageId, (progress) => { + * console.log(`Progress: ${progress.progress}/${progress.total}`); + * }); + * + * // Handle incoming progress notification + * progressManager.handleProgress(notification); + * + * // Clean up when done + * progressManager.removeHandler(messageId); + * ``` + */ +export class ProgressManager { + /** + * Maps message IDs to progress callbacks. + */ + #progressHandlers: Map = new Map(); + + /** + * Maps task IDs to progress tokens to keep handlers alive after CreateTaskResult. + */ + #taskProgressTokens: Map = new Map(); + + /** + * Registers a progress callback for a message. + * + * @param messageId - The message ID (used as progress token) + * @param callback - The callback to invoke when progress is received + */ + registerHandler(messageId: number, callback: ProgressCallback): void { + this.#progressHandlers.set(messageId, callback); + } + + /** + * Gets the progress callback for a message. + * + * @param messageId - The message ID + * @returns The progress callback or undefined if not registered + */ + getHandler(messageId: number): ProgressCallback | undefined { + return this.#progressHandlers.get(messageId); + } + + /** + * Removes the progress callback for a message. + * + * @param messageId - The message ID + */ + removeHandler(messageId: number): void { + this.#progressHandlers.delete(messageId); + } + + /** + * Checks if a progress handler exists for the given message ID. + * + * @param messageId - The message ID + * @returns true if a handler is registered, false otherwise + */ + hasHandler(messageId: number): boolean { + return this.#progressHandlers.has(messageId); + } + + /** + * Handles an incoming progress notification by invoking the registered callback. + * Returns true if the progress was handled, false if no handler was found. + * + * @param notification - The progress notification + * @returns true if handled, false otherwise + */ + handleProgress(notification: ProgressNotification): boolean { + const token = notification.params.progressToken; + if (typeof token !== 'number') { + // Token must be a number for our internal tracking + return false; + } + + const callback = this.#progressHandlers.get(token); + if (callback) { + callback({ + progress: notification.params.progress, + total: notification.params.total, + message: notification.params.message + }); + return true; + } + + return false; + } + + /** + * Links a task ID to a progress token. + * This keeps the progress handler alive after CreateTaskResult is returned, + * allowing progress notifications to continue for long-running tasks. + * + * @param taskId - The task identifier + * @param progressToken - The progress token (message ID) + */ + linkTaskToProgressToken(taskId: string, progressToken: number): void { + this.#taskProgressTokens.set(taskId, progressToken); + } + + /** + * Gets the progress token associated with a task. + * + * @param taskId - The task identifier + * @returns The progress token or undefined if not linked + */ + getTaskProgressToken(taskId: string): number | undefined { + return this.#taskProgressTokens.get(taskId); + } + + /** + * Cleans up the progress handler associated with a task. + * Should be called when a task reaches a terminal status. + * + * @param taskId - The task identifier + */ + cleanupTaskProgressHandler(taskId: string): void { + const progressToken = this.#taskProgressTokens.get(taskId); + if (progressToken !== undefined) { + this.#progressHandlers.delete(progressToken); + this.#taskProgressTokens.delete(taskId); + } + } + + /** + * Clears all progress handlers and task progress tokens. + * Typically called when the connection is closed. + */ + clear(): void { + this.#progressHandlers.clear(); + this.#taskProgressTokens.clear(); + } + + /** + * Gets the number of active progress handlers. + */ + get handlerCount(): number { + return this.#progressHandlers.size; + } + + /** + * Gets the number of active task-to-progress-token links. + */ + get taskTokenCount(): number { + return this.#taskProgressTokens.size; + } +} diff --git a/packages/core/src/shared/protocol.ts b/packages/core/src/shared/protocol.ts index f537aa86c..c21f000a8 100644 --- a/packages/core/src/shared/protocol.ts +++ b/packages/core/src/shared/protocol.ts @@ -16,7 +16,6 @@ import type { Notification, NotificationMethod, NotificationTypeMap, - Progress, ProgressNotification, RelatedTaskMetadata, Request, @@ -51,14 +50,11 @@ import { import type { AnySchema, SchemaOutput } from '../util/zodCompat.js'; import { safeParse } from '../util/zodCompat.js'; import { parseWithCompat } from '../util/zodJsonSchemaCompat.js'; +import type { ProgressCallback } from './progressManager.js'; +import { ProgressManager } from './progressManager.js'; import type { ResponseMessage } from './responseMessage.js'; import type { Transport, TransportSendOptions } from './transport.js'; -/** - * Callback for progress notifications. - */ -export type ProgressCallback = (progress: Progress) => void; - /** * Additional initialization options. */ @@ -330,13 +326,10 @@ export abstract class Protocol = new Map(); private _notificationHandlers: Map Promise> = new Map(); private _responseHandlers: Map void> = new Map(); - private _progressHandlers: Map = new Map(); + private _progressManager: ProgressManager = new ProgressManager(); private _timeoutInfo: Map = new Map(); private _pendingDebouncedNotifications = new Set(); - // Maps task IDs to progress tokens to keep handlers alive after CreateTaskResult - private _taskProgressTokens: Map = new Map(); - private _taskStore?: TaskStore; private _taskMessageQueue?: TaskMessageQueue; @@ -639,8 +632,7 @@ export abstract class Protocol; if (typeof task.taskId === 'string') { isTaskResponse = true; - this._taskProgressTokens.set(task.taskId, messageId); + this._progressManager.linkTaskToProgressToken(task.taskId, messageId); } } } if (!isTaskResponse) { - this._progressHandlers.delete(messageId); + this._progressManager.removeHandler(messageId); } if (isJSONRPCResultResponse(response)) { @@ -1116,7 +1107,7 @@ export abstract class Protocol { this._responseHandlers.delete(messageId); - this._progressHandlers.delete(messageId); + this._progressManager.removeHandler(messageId); this._cleanupTimeout(messageId); this._transport @@ -1459,11 +1450,7 @@ export abstract class Protocol void>; _responseHandlers: Map void>; - _taskProgressTokens: Map; + _progressManager: { + getTaskProgressToken(taskId: string): number | undefined; + cleanupTaskProgressHandler(taskId: string): void; + }; _clearTaskQueue: (taskId: string, sessionId?: string) => Promise; requestTaskStore: (request: Request, authInfo: unknown) => TaskStore; // Protected task methods (exposed for testing) @@ -2564,9 +2567,8 @@ describe('Progress notification support for tasks', () => { expect(progressCallback).toHaveBeenCalledTimes(1); // Verify the task-progress association was created - const taskProgressTokens = (protocol as unknown as TestProtocol)._taskProgressTokens as Map; - expect(taskProgressTokens.has(taskId)).toBe(true); - expect(taskProgressTokens.get(taskId)).toBe(progressToken); + const progressManager = (protocol as unknown as TestProtocol)._progressManager; + expect(progressManager.getTaskProgressToken(taskId)).toBe(progressToken); // Simulate task completion by calling through the protocol's task store // This will trigger the cleanup logic @@ -2578,7 +2580,7 @@ describe('Progress notification support for tasks', () => { await new Promise(resolve => setTimeout(resolve, 50)); // Verify the association was cleaned up - expect(taskProgressTokens.has(taskId)).toBe(false); + expect(progressManager.getTaskProgressToken(taskId)).toBeUndefined(); // Try to send progress notification after task completion - should be ignored progressCallback.mockClear(); From 3f5f73e08e701c1f21aea576604d2ca40a394f50 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Tue, 3 Feb 2026 12:53:36 +0200 Subject: [PATCH 2/3] add timeout manager split --- packages/core/src/index.ts | 1 + packages/core/src/shared/protocol.ts | 108 +++------ packages/core/src/shared/timeoutManager.ts | 249 +++++++++++++++++++++ 3 files changed, 281 insertions(+), 77 deletions(-) create mode 100644 packages/core/src/shared/timeoutManager.ts diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 0c631c346..3e7eb6a46 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -6,6 +6,7 @@ export * from './shared/progressManager.js'; export * from './shared/protocol.js'; export * from './shared/responseMessage.js'; export * from './shared/stdio.js'; +export * from './shared/timeoutManager.js'; export * from './shared/toolNameValidation.js'; export * from './shared/transport.js'; export * from './shared/uriTemplate.js'; diff --git a/packages/core/src/shared/protocol.ts b/packages/core/src/shared/protocol.ts index c21f000a8..7f33e340f 100644 --- a/packages/core/src/shared/protocol.ts +++ b/packages/core/src/shared/protocol.ts @@ -53,6 +53,7 @@ import { parseWithCompat } from '../util/zodJsonSchemaCompat.js'; import type { ProgressCallback } from './progressManager.js'; import { ProgressManager } from './progressManager.js'; import type { ResponseMessage } from './responseMessage.js'; +import { TimeoutManager } from './timeoutManager.js'; import type { Transport, TransportSendOptions } from './transport.js'; /** @@ -300,18 +301,6 @@ export type RequestHandlerExtra void; }; -/** - * Information about a request's timeout state - */ -type TimeoutInfo = { - timeoutId: ReturnType; - startTime: number; - timeout: number; - maxTotalTimeout?: number; - resetTimeoutOnProgress: boolean; - onTimeout: () => void; -}; - /** * Implements MCP protocol framing on top of a pluggable transport, including * features like request/response linking, notifications, and progress. @@ -326,8 +315,8 @@ export abstract class Protocol = new Map(); private _notificationHandlers: Map Promise> = new Map(); private _responseHandlers: Map void> = new Map(); - private _progressManager: ProgressManager = new ProgressManager(); - private _timeoutInfo: Map = new Map(); + readonly #progressManager: ProgressManager = new ProgressManager(); + readonly #timeoutManager: TimeoutManager = new TimeoutManager(); private _pendingDebouncedNotifications = new Set(); private _taskStore?: TaskStore; @@ -550,49 +539,6 @@ export abstract class Protocol void, - resetTimeoutOnProgress: boolean = false - ) { - this._timeoutInfo.set(messageId, { - timeoutId: setTimeout(onTimeout, timeout), - startTime: Date.now(), - timeout, - maxTotalTimeout, - resetTimeoutOnProgress, - onTimeout - }); - } - - private _resetTimeout(messageId: number): boolean { - 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; - } - - private _cleanupTimeout(messageId: number) { - 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. * @@ -632,7 +578,7 @@ export abstract class Protocol; if (typeof task.taskId === 'string') { isTaskResponse = true; - this._progressManager.linkTaskToProgressToken(task.taskId, messageId); + this.#progressManager.linkTaskToProgressToken(task.taskId, messageId); } } } if (!isTaskResponse) { - this._progressManager.removeHandler(messageId); + this.#progressManager.removeHandler(messageId); } if (isJSONRPCResultResponse(response)) { @@ -1107,7 +1056,7 @@ export abstract class Protocol { this._responseHandlers.delete(messageId); - this._progressManager.removeHandler(messageId); - this._cleanupTimeout(messageId); + this.#progressManager.removeHandler(messageId); + this.#timeoutManager.cleanup(messageId); this._transport ?.send( @@ -1189,7 +1138,12 @@ export abstract class Protocol cancel(McpError.fromError(ErrorCode.RequestTimeout, 'Request timed out', { timeout })); - this._setupTimeout(messageId, timeout, options?.maxTotalTimeout, timeoutHandler, options?.resetTimeoutOnProgress ?? false); + this.#timeoutManager.setup(messageId, { + timeout, + maxTotalTimeout: options?.maxTotalTimeout, + resetTimeoutOnProgress: options?.resetTimeoutOnProgress ?? false, + onTimeout: timeoutHandler + }); // Queue request if related to a task const relatedTaskId = relatedTask?.taskId; @@ -1211,7 +1165,7 @@ export abstract class Protocol { - this._cleanupTimeout(messageId); + this.#timeoutManager.cleanup(messageId); reject(error); }); @@ -1220,7 +1174,7 @@ export abstract class Protocol { - this._cleanupTimeout(messageId); + this.#timeoutManager.cleanup(messageId); reject(error); }); } @@ -1450,7 +1404,7 @@ export abstract class Protocol; + + /** + * The time when the timeout was started (in milliseconds since epoch). + */ + startTime: number; + + /** + * The timeout duration in milliseconds. + */ + timeout: number; + + /** + * Maximum total time allowed in milliseconds (optional). + */ + maxTotalTimeout?: number; + + /** + * Whether to reset the timeout when progress is received. + */ + resetTimeoutOnProgress: boolean; + + /** + * Callback to invoke when the timeout expires. + */ + onTimeout: () => void; +} + +/** + * Options for setting up a timeout. + */ +export interface TimeoutOptions { + /** + * The timeout duration in milliseconds. + */ + timeout: number; + + /** + * Maximum total time allowed in milliseconds (optional). + * If set, the timeout cannot be reset beyond this total duration. + */ + maxTotalTimeout?: number; + + /** + * Whether to reset the timeout when progress is received. + * @default false + */ + resetTimeoutOnProgress?: boolean; + + /** + * Callback to invoke when the timeout expires. + */ + onTimeout: () => void; +} + +/** + * Result of a timeout reset attempt. + */ +export interface TimeoutResetResult { + /** + * Whether the reset was successful. + */ + success: boolean; + + /** + * If reset failed due to max total timeout being exceeded, this contains + * the elapsed time and max total timeout for error reporting. + */ + maxTotalTimeoutExceeded?: { + elapsed: number; + maxTotalTimeout: number; + }; +} + +/** + * Manages request timeouts for outgoing requests. + * + * This class handles setting up, resetting, and cleaning up timeouts for + * individual messages. It supports both simple timeouts and progress-aware + * timeouts that can be reset when progress notifications are received. + * + * @example + * ```typescript + * const timeoutManager = new TimeoutManager(); + * + * // Set up a timeout + * timeoutManager.setup(messageId, { + * timeout: 30000, + * maxTotalTimeout: 300000, + * resetTimeoutOnProgress: true, + * onTimeout: () => console.log('Request timed out') + * }); + * + * // Reset timeout when progress is received + * const result = timeoutManager.reset(messageId); + * if (!result.success && result.maxTotalTimeoutExceeded) { + * // Handle max total timeout exceeded + * } + * + * // Clean up when response received + * timeoutManager.cleanup(messageId); + * ``` + */ +export class TimeoutManager { + /** + * Maps message IDs to their timeout information. + */ + #timeoutInfo: Map = new Map(); + + /** + * Sets up a timeout for a message. + * + * @param messageId - The unique identifier for the message + * @param options - Timeout configuration options + */ + setup(messageId: number, options: TimeoutOptions): void { + const { timeout, maxTotalTimeout, resetTimeoutOnProgress, onTimeout } = options; + + this.#timeoutInfo.set(messageId, { + timeoutId: setTimeout(onTimeout, timeout), + startTime: Date.now(), + timeout, + maxTotalTimeout, + resetTimeoutOnProgress: resetTimeoutOnProgress ?? false, + onTimeout + }); + } + + /** + * Resets the timeout for a message (e.g., when progress is received). + * + * The reset will fail if: + * - No timeout exists for the message + * - The timeout is not configured for reset on progress + * - The max total timeout has been exceeded + * + * When reset succeeds, the timeout is reset to its original duration. + * The maxTotalTimeout check happens when reset is called, not by setting + * a shorter timeout - this allows progress notifications to be processed + * and the caller to handle the max total timeout exceeded condition. + * + * @param messageId - The message ID whose timeout should be reset + * @returns A result object indicating success or failure with details + */ + reset(messageId: number): TimeoutResetResult { + const info = this.#timeoutInfo.get(messageId); + if (!info || !info.resetTimeoutOnProgress) { + return { success: false }; + } + + const elapsed = Date.now() - info.startTime; + + // Check if max total timeout has been exceeded + if (info.maxTotalTimeout !== undefined && elapsed >= info.maxTotalTimeout) { + return { + success: false, + maxTotalTimeoutExceeded: { + elapsed, + maxTotalTimeout: info.maxTotalTimeout + } + }; + } + + // Reset to the original timeout duration + clearTimeout(info.timeoutId); + info.timeoutId = setTimeout(info.onTimeout, info.timeout); + + return { success: true }; + } + + /** + * Cleans up the timeout for a message (e.g., when a response is received). + * + * @param messageId - The message ID whose timeout should be cleaned up + */ + cleanup(messageId: number): void { + const info = this.#timeoutInfo.get(messageId); + if (info) { + clearTimeout(info.timeoutId); + this.#timeoutInfo.delete(messageId); + } + } + + /** + * Gets the timeout info for a message. + * + * @param messageId - The message ID + * @returns The timeout info or undefined if not found + */ + get(messageId: number): TimeoutInfo | undefined { + return this.#timeoutInfo.get(messageId); + } + + /** + * Checks if a timeout exists for a message. + * + * @param messageId - The message ID + * @returns true if a timeout exists + */ + has(messageId: number): boolean { + return this.#timeoutInfo.has(messageId); + } + + /** + * Gets the elapsed time for a message's timeout. + * + * @param messageId - The message ID + * @returns The elapsed time in milliseconds, or undefined if not found + */ + getElapsed(messageId: number): number | undefined { + const info = this.#timeoutInfo.get(messageId); + if (!info) { + return undefined; + } + return Date.now() - info.startTime; + } + + /** + * Clears all timeouts. + * Typically called when the connection is closed. + */ + clearAll(): void { + for (const info of this.#timeoutInfo.values()) { + clearTimeout(info.timeoutId); + } + this.#timeoutInfo.clear(); + } + + /** + * Gets the number of active timeouts. + */ + get size(): number { + return this.#timeoutInfo.size; + } +} From 45d94d219daa7a9a8dc7510cfe81639eec7ff9a4 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Tue, 3 Feb 2026 16:25:17 +0200 Subject: [PATCH 3/3] fix tests --- packages/core/src/shared/protocol.ts | 16 +++ packages/core/test/shared/protocol.test.ts | 124 ++++++++++----------- 2 files changed, 78 insertions(+), 62 deletions(-) diff --git a/packages/core/src/shared/protocol.ts b/packages/core/src/shared/protocol.ts index 7f33e340f..af536775a 100644 --- a/packages/core/src/shared/protocol.ts +++ b/packages/core/src/shared/protocol.ts @@ -848,6 +848,22 @@ export abstract class Protocol void>; _responseHandlers: Map void>; - _progressManager: { + getProgressManager(): { getTaskProgressToken(taskId: string): number | undefined; cleanupTaskProgressHandler(taskId: string): void; }; @@ -1037,7 +1037,7 @@ describe('Task-based execution', () => { void protocol .request(request, resultSchema, { task: { - ttl: 30000, + ttl: 30_000, pollInterval: 1000 } }) @@ -1051,7 +1051,7 @@ describe('Task-based execution', () => { params: { name: 'test-tool', task: { - ttl: 30000, + ttl: 30_000, pollInterval: 1000 } } @@ -1080,7 +1080,7 @@ describe('Task-based execution', () => { void protocol .request(request, resultSchema, { task: { - ttl: 60000 + ttl: 60_000 } }) .catch(() => { @@ -1095,7 +1095,7 @@ describe('Task-based execution', () => { customField: 'customValue' }, task: { - ttl: 60000 + ttl: 60_000 } } }), @@ -1117,7 +1117,7 @@ describe('Task-based execution', () => { const resultPromise = protocol.request(request, resultSchema, { task: { - ttl: 30000 + ttl: 30_000 } }); @@ -1207,7 +1207,7 @@ describe('Task-based execution', () => { void protocol .request(request, resultSchema, { task: { - ttl: 60000, + ttl: 60_000, pollInterval: 1000 }, relatedTask: { @@ -1235,7 +1235,7 @@ describe('Task-based execution', () => { expect(queuedMessage.message.params).toMatchObject({ name: 'test-tool', task: { - ttl: 60000, + ttl: 60_000, pollInterval: 1000 }, _meta: { @@ -1272,7 +1272,7 @@ describe('Task-based execution', () => { protocol.setRequestHandler('tools/call', async request => { // Tool implementor can access task creation parameters from request.params.task expect(request.params.task).toEqual({ - ttl: 60000, + ttl: 60_000, pollInterval: 1000 }); return { content: [{ type: 'text', text: 'success' }] }; @@ -1286,7 +1286,7 @@ describe('Task-based execution', () => { name: 'test', arguments: {}, task: { - ttl: 60000, + ttl: 60_000, pollInterval: 1000 } } @@ -1318,7 +1318,7 @@ describe('Task-based execution', () => { const task2 = await mockTaskStore.createTask( { - ttl: 60000, + ttl: 60_000, pollInterval: 1000 }, 2, @@ -1364,7 +1364,7 @@ describe('Task-based execution', () => { { taskId: task2.taskId, status: 'working', - ttl: 60000, + ttl: 60_000, createdAt: expect.any(String), lastUpdatedAt: expect.any(String), pollInterval: 1000 @@ -1495,7 +1495,7 @@ describe('Task-based execution', () => { expect(sentMessage.jsonrpc).toBe('2.0'); expect(sentMessage.id).toBe(4); expect(sentMessage.error).toBeDefined(); - expect(sentMessage.error.code).toBe(-32602); // InvalidParams error code + expect(sentMessage.error.code).toBe(-32_602); // InvalidParams error code expect(sentMessage.error.message).toContain('Failed to list tasks'); expect(sentMessage.error.message).toContain('Invalid cursor'); }); @@ -1555,7 +1555,7 @@ describe('Task-based execution', () => { { taskId: 'task-11', status: 'working', - ttl: 30000, + ttl: 30_000, createdAt: '2024-01-01T00:00:00Z', lastUpdatedAt: '2024-01-01T00:00:00Z', pollInterval: 1000 @@ -1674,7 +1674,7 @@ describe('Task-based execution', () => { expect(sentMessage.jsonrpc).toBe('2.0'); expect(sentMessage.id).toBe(6); expect(sentMessage.error).toBeDefined(); - expect(sentMessage.error.code).toBe(-32602); // InvalidParams error code + expect(sentMessage.error.code).toBe(-32_602); // InvalidParams error code expect(sentMessage.error.message).toContain('Task not found'); }); @@ -1722,7 +1722,7 @@ describe('Task-based execution', () => { expect(sentMessage.jsonrpc).toBe('2.0'); expect(sentMessage.id).toBe(7); expect(sentMessage.error).toBeDefined(); - expect(sentMessage.error.code).toBe(-32602); // InvalidParams error code + expect(sentMessage.error.code).toBe(-32_602); // InvalidParams error code expect(sentMessage.error.message).toContain('Cannot cancel task in terminal status'); }); @@ -1740,7 +1740,7 @@ describe('Task-based execution', () => { _meta: {}, taskId: 'task-to-delete', status: 'cancelled', - ttl: 60000, + ttl: 60_000, createdAt: new Date().toISOString(), lastUpdatedAt: new Date().toISOString() } @@ -2140,7 +2140,7 @@ describe('Request Cancellation vs Task Cancellation', () => { await protocol.connect(transport); // Create a task - const task = await taskStore.createTask({ ttl: 60000 }, 'req-1', { + const task = await taskStore.createTask({ ttl: 60_000 }, 'req-1', { method: 'test/method', params: {} }); @@ -2172,7 +2172,7 @@ describe('Request Cancellation vs Task Cancellation', () => { await protocol.connect(transport); // Create a task - const task = await taskStore.createTask({ ttl: 60000 }, 'req-1', { + const task = await taskStore.createTask({ ttl: 60_000 }, 'req-1', { method: 'test/method', params: {} }); @@ -2206,7 +2206,7 @@ describe('Request Cancellation vs Task Cancellation', () => { const sendSpy = vi.spyOn(transport, 'send'); // Create a task and mark it as completed - const task = await taskStore.createTask({ ttl: 60000 }, 'req-1', { + const task = await taskStore.createTask({ ttl: 60_000 }, 'req-1', { method: 'test/method', params: {} }); @@ -2278,7 +2278,7 @@ describe('Request Cancellation vs Task Cancellation', () => { await protocol.connect(transport); // Create a task - const task = await taskStore.createTask({ ttl: 60000 }, 'req-1', { + const task = await taskStore.createTask({ ttl: 60_000 }, 'req-1', { method: 'test/method', params: {} }); @@ -2314,7 +2314,7 @@ describe('Request Cancellation vs Task Cancellation', () => { }); // Create a task (simulating a long-running tools/call) - const task = await taskStore.createTask({ ttl: 60000 }, 'req-1', { + const task = await taskStore.createTask({ ttl: 60_000 }, 'req-1', { method: 'tools/call', params: { name: 'long-running-tool', arguments: {} } }); @@ -2407,7 +2407,7 @@ describe('Progress notification support for tasks', () => { // Start a task-augmented request with progress callback void protocol .request(request, resultSchema, { - task: { ttl: 60000 }, + task: { ttl: 60_000 }, onprogress: progressCallback }) .catch(() => { @@ -2434,7 +2434,7 @@ describe('Progress notification support for tasks', () => { task: { taskId, status: 'working', - ttl: 60000, + ttl: 60_000, createdAt: new Date().toISOString() } } @@ -2485,7 +2485,7 @@ describe('Progress notification support for tasks', () => { // Set up a request handler that will complete the task protocol.setRequestHandler('tools/call', async (_request, extra) => { if (extra.taskStore) { - const task = await extra.taskStore.createTask({ ttl: 60000 }); + const task = await extra.taskStore.createTask({ ttl: 60_000 }); // Simulate async work then complete the task setTimeout(async () => { @@ -2517,7 +2517,7 @@ describe('Progress notification support for tasks', () => { // Start a task-augmented request with progress callback void protocol .request(request, resultSchema, { - task: { ttl: 60000 }, + task: { ttl: 60_000 }, onprogress: progressCallback }) .catch(() => { @@ -2532,7 +2532,7 @@ describe('Progress notification support for tasks', () => { const progressToken = sentRequest.params._meta.progressToken; // Create a task in the mock store first so it exists when we try to get it later - const createdTask = await taskStore.createTask({ ttl: 60000 }, messageId, request); + const createdTask = await taskStore.createTask({ ttl: 60_000 }, messageId, request); const taskId = createdTask.taskId; // Simulate CreateTaskResult response @@ -2567,7 +2567,7 @@ describe('Progress notification support for tasks', () => { expect(progressCallback).toHaveBeenCalledTimes(1); // Verify the task-progress association was created - const progressManager = (protocol as unknown as TestProtocol)._progressManager; + const progressManager = (protocol as unknown as TestProtocol).getProgressManager(); expect(progressManager.getTaskProgressToken(taskId)).toBe(progressToken); // Simulate task completion by calling through the protocol's task store @@ -2632,7 +2632,7 @@ describe('Progress notification support for tasks', () => { }); void protocol.request(request, resultSchema, { - task: { ttl: 60000 }, + task: { ttl: 60_000 }, onprogress: progressCallback }); @@ -2650,7 +2650,7 @@ describe('Progress notification support for tasks', () => { task: { taskId, status: 'working', - ttl: 60000, + ttl: 60_000, createdAt: new Date().toISOString() } } @@ -2673,7 +2673,7 @@ describe('Progress notification support for tasks', () => { params: { taskId, status: 'failed', - ttl: 60000, + ttl: 60_000, createdAt: new Date().toISOString(), lastUpdatedAt: new Date().toISOString(), statusMessage: 'Task failed' @@ -2730,7 +2730,7 @@ describe('Progress notification support for tasks', () => { }); void protocol.request(request, resultSchema, { - task: { ttl: 60000 }, + task: { ttl: 60_000 }, onprogress: progressCallback }); @@ -2748,7 +2748,7 @@ describe('Progress notification support for tasks', () => { task: { taskId, status: 'working', - ttl: 60000, + ttl: 60_000, createdAt: new Date().toISOString() } } @@ -2768,7 +2768,7 @@ describe('Progress notification support for tasks', () => { params: { taskId, status: 'cancelled', - ttl: 60000, + ttl: 60_000, createdAt: new Date().toISOString(), lastUpdatedAt: new Date().toISOString(), statusMessage: 'User cancelled' @@ -2825,7 +2825,7 @@ describe('Progress notification support for tasks', () => { }); void protocol.request(request, resultSchema, { - task: { ttl: 60000 }, + task: { ttl: 60_000 }, onprogress: progressCallback }); @@ -2843,7 +2843,7 @@ describe('Progress notification support for tasks', () => { task: { taskId, status: 'working', - ttl: 60000, + ttl: 60_000, createdAt: new Date().toISOString() } } @@ -2897,7 +2897,7 @@ describe('Progress notification support for tasks', () => { void protocol.request(request, resultSchema, { task: { - ttl: 60000 + ttl: 60_000 }, onprogress: onProgressMock }); @@ -2922,7 +2922,7 @@ describe('Progress notification support for tasks', () => { void protocol.request(request, resultSchema, { task: { - ttl: 30000 + ttl: 30_000 }, onprogress: onProgressMock }); @@ -2972,7 +2972,7 @@ describe('Progress notification support for tasks', () => { void protocol.request(request, resultSchema, { task: { - ttl: 30000 + ttl: 30_000 }, onprogress: onProgressMock }); @@ -2989,7 +2989,7 @@ describe('Progress notification support for tasks', () => { task: { taskId: 'task-123', status: 'working', - ttl: 30000, + ttl: 30_000, createdAt: new Date().toISOString() } } @@ -3077,7 +3077,7 @@ describe('Message interception for task-related notifications', () => { await server.connect(transport); // Create a task first - const task = await taskStore.createTask({ ttl: 60000 }, 'test-request-1', { method: 'tools/call', params: {} }); + const task = await taskStore.createTask({ ttl: 60_000 }, 'test-request-1', { method: 'tools/call', params: {} }); // Send a notification with related task metadata await server.notification( @@ -3141,7 +3141,7 @@ describe('Message interception for task-related notifications', () => { await server.connect(transport); // Create a task - const task = await taskStore.createTask({ ttl: 60000 }, 'test-request-1', { method: 'tools/call', params: {} }); + const task = await taskStore.createTask({ ttl: 60_000 }, 'test-request-1', { method: 'tools/call', params: {} }); // Fill the queue to max capacity (100 messages) for (let i = 0; i < 100; i++) { @@ -3221,7 +3221,7 @@ describe('Message interception for task-related notifications', () => { await server.connect(transport); // Create a task - const task = await taskStore.createTask({ ttl: 60000 }, 'test-request-1', { method: 'tools/call', params: {} }); + const task = await taskStore.createTask({ ttl: 60_000 }, 'test-request-1', { method: 'tools/call', params: {} }); // Send multiple notifications for (let i = 0; i < 5; i++) { @@ -3263,7 +3263,7 @@ describe('Message interception for task-related requests', () => { await server.connect(transport); // Create a task first - const task = await taskStore.createTask({ ttl: 60000 }, 'test-request-1', { method: 'tools/call', params: {} }); + const task = await taskStore.createTask({ ttl: 60_000 }, 'test-request-1', { method: 'tools/call', params: {} }); // Send a request with related task metadata (don't await - we're testing queuing) const requestPromise = server.request( @@ -3354,7 +3354,7 @@ describe('Message interception for task-related requests', () => { await server.connect(transport); // Create a task - const task = await taskStore.createTask({ ttl: 60000 }, 'test-request-1', { method: 'tools/call', params: {} }); + const task = await taskStore.createTask({ ttl: 60_000 }, 'test-request-1', { method: 'tools/call', params: {} }); // Send a request with related task metadata const requestPromise = server.request( @@ -3407,7 +3407,7 @@ describe('Message interception for task-related requests', () => { await server.connect(transport); // Create a task - const task = await taskStore.createTask({ ttl: 60000 }, 'test-request-1', { method: 'tools/call', params: {} }); + const task = await taskStore.createTask({ ttl: 60_000 }, 'test-request-1', { method: 'tools/call', params: {} }); // Send a request with related task metadata const requestPromise = server.request( @@ -3477,7 +3477,7 @@ describe('Message interception for task-related requests', () => { await server.connect(transport); // Create a task - const task = await taskStore.createTask({ ttl: 60000 }, 'test-request-1', { method: 'tools/call', params: {} }); + const task = await taskStore.createTask({ ttl: 60_000 }, 'test-request-1', { method: 'tools/call', params: {} }); // Send a request with related task metadata void server.request( @@ -3548,7 +3548,7 @@ describe('Message interception for task-related requests', () => { await server.connect(transport); // Create a task - const task = await taskStore.createTask({ ttl: 60000 }, 'test-request-1', { method: 'tools/call', params: {} }); + const task = await taskStore.createTask({ ttl: 60_000 }, 'test-request-1', { method: 'tools/call', params: {} }); // Fill the queue to max capacity (100 messages) const promises: Promise[] = []; @@ -4888,7 +4888,7 @@ describe('Error handling for missing resolvers', () => { await protocol.connect(transport); // Create a task - const task = await taskStore.createTask({ ttl: 60000 }, 1, { method: 'test', params: {} }); + const task = await taskStore.createTask({ ttl: 60_000 }, 1, { method: 'test', params: {} }); // Enqueue a response message without a corresponding resolver await taskMessageQueue.enqueue(task.taskId, { @@ -4933,7 +4933,7 @@ describe('Error handling for missing resolvers', () => { await protocol.connect(transport); // Create a task - const task = await taskStore.createTask({ ttl: 60000 }, 1, { method: 'test', params: {} }); + const task = await taskStore.createTask({ ttl: 60_000 }, 1, { method: 'test', params: {} }); // Enqueue a response with missing resolver, then a valid notification await taskMessageQueue.enqueue(task.taskId, { @@ -4974,7 +4974,7 @@ describe('Error handling for missing resolvers', () => { await protocol.connect(transport); // Create a task - const task = await taskStore.createTask({ ttl: 60000 }, 1, { method: 'test', params: {} }); + const task = await taskStore.createTask({ ttl: 60_000 }, 1, { method: 'test', params: {} }); // Enqueue a request without storing a resolver await taskMessageQueue.enqueue(task.taskId, { @@ -5004,7 +5004,7 @@ describe('Error handling for missing resolvers', () => { await protocol.connect(transport); // Create a task - const task = await taskStore.createTask({ ttl: 60000 }, 1, { method: 'test', params: {} }); + const task = await taskStore.createTask({ ttl: 60_000 }, 1, { method: 'test', params: {} }); const requestId = 42; const resolverMock = vi.fn(); @@ -5044,7 +5044,7 @@ describe('Error handling for missing resolvers', () => { await protocol.connect(transport); // Create a task - const task = await taskStore.createTask({ ttl: 60000 }, 1, { method: 'test', params: {} }); + const task = await taskStore.createTask({ ttl: 60_000 }, 1, { method: 'test', params: {} }); const testProtocol = protocol as unknown as TestProtocol; @@ -5148,7 +5148,7 @@ describe('Error handling for missing resolvers', () => { it('should not throw when processing response with missing resolver', async () => { await protocol.connect(transport); - const task = await taskStore.createTask({ ttl: 60000 }, 1, { method: 'test', params: {} }); + const task = await taskStore.createTask({ ttl: 60_000 }, 1, { method: 'test', params: {} }); await taskMessageQueue.enqueue(task.taskId, { type: 'response', @@ -5180,7 +5180,7 @@ describe('Error handling for missing resolvers', () => { it('should not throw during task cleanup with missing resolvers', async () => { await protocol.connect(transport); - const task = await taskStore.createTask({ ttl: 60000 }, 1, { method: 'test', params: {} }); + const task = await taskStore.createTask({ ttl: 60_000 }, 1, { method: 'test', params: {} }); await taskMessageQueue.enqueue(task.taskId, { type: 'request', @@ -5204,7 +5204,7 @@ describe('Error handling for missing resolvers', () => { it('should route error messages to resolvers correctly', async () => { await protocol.connect(transport); - const task = await taskStore.createTask({ ttl: 60000 }, 1, { method: 'test', params: {} }); + const task = await taskStore.createTask({ ttl: 60_000 }, 1, { method: 'test', params: {} }); const requestId = 42; const resolverMock = vi.fn(); @@ -5257,7 +5257,7 @@ describe('Error handling for missing resolvers', () => { it('should log error for unknown request ID in error messages', async () => { await protocol.connect(transport); - const task = await taskStore.createTask({ ttl: 60000 }, 1, { method: 'test', params: {} }); + const task = await taskStore.createTask({ ttl: 60_000 }, 1, { method: 'test', params: {} }); // Enqueue an error message without a corresponding resolver await taskMessageQueue.enqueue(task.taskId, { @@ -5301,7 +5301,7 @@ describe('Error handling for missing resolvers', () => { it('should handle error messages with data field', async () => { await protocol.connect(transport); - const task = await taskStore.createTask({ ttl: 60000 }, 1, { method: 'test', params: {} }); + const task = await taskStore.createTask({ ttl: 60_000 }, 1, { method: 'test', params: {} }); const requestId = 42; const resolverMock = vi.fn(); @@ -5350,7 +5350,7 @@ describe('Error handling for missing resolvers', () => { it('should not throw when processing error with missing resolver', async () => { await protocol.connect(transport); - const task = await taskStore.createTask({ ttl: 60000 }, 1, { method: 'test', params: {} }); + const task = await taskStore.createTask({ ttl: 60_000 }, 1, { method: 'test', params: {} }); await taskMessageQueue.enqueue(task.taskId, { type: 'error', @@ -5387,7 +5387,7 @@ describe('Error handling for missing resolvers', () => { it('should handle mixed response and error messages in queue', async () => { await protocol.connect(transport); - const task = await taskStore.createTask({ ttl: 60000 }, 1, { method: 'test', params: {} }); + const task = await taskStore.createTask({ ttl: 60_000 }, 1, { method: 'test', params: {} }); const testProtocol = protocol as unknown as TestProtocol; // Set up resolvers for multiple requests @@ -5473,7 +5473,7 @@ describe('Error handling for missing resolvers', () => { it('should maintain FIFO order when processing responses and errors', async () => { await protocol.connect(transport); - const task = await taskStore.createTask({ ttl: 60000 }, 1, { method: 'test', params: {} }); + const task = await taskStore.createTask({ ttl: 60_000 }, 1, { method: 'test', params: {} }); const testProtocol = protocol as unknown as TestProtocol; const callOrder: number[] = []; @@ -5497,7 +5497,7 @@ describe('Error handling for missing resolvers', () => { message: { jsonrpc: '2.0', id: 2, - error: { code: -32600, message: 'Error' } + error: { code: -32_600, message: 'Error' } }, timestamp: 2000 });