v2 - RFC Extract Tasks out of protocol.ts into TaskManager (#1673)

Co-authored-by: Felix Weinberger <fweinberger@anthropic.com>
This commit is contained in:
Konstantin Konstantinov 2026-03-25 21:58:01 +02:00 • committed by GitHub
commit 462c3fc47d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 2122 additions and 1942 deletions

View file

@ -0,0 +1,10 @@
---
"@modelcontextprotocol/core": minor
"@modelcontextprotocol/client": minor
"@modelcontextprotocol/server": minor
---
refactor: extract task orchestration from Protocol into TaskManager
**Breaking changes:**
- `taskStore`, `taskMessageQueue`, `defaultTaskPollInterval`, and `maxTaskQueueSize` moved from `ProtocolOptions` to `capabilities.tasks` on `ClientOptions`/`ServerOptions`

View file

@ -265,14 +265,14 @@ async function connect(url?: string): Promise<void> {
form: {}
},
tasks: {
taskStore: clientTaskStore,
requests: {
elicitation: {
create: {}
}
}
}
},
taskStore: clientTaskStore
}
}
);
client.onerror = error => {

View file

@ -41,9 +41,14 @@ const getServer = () => {
websiteUrl: 'https://github.com/modelcontextprotocol/typescript-sdk'
},
{
capabilities: { logging: {}, tasks: { requests: { tools: { call: {} } } } },
taskStore, // Enable task support
taskMessageQueue: new InMemoryTaskMessageQueue()
capabilities: {
logging: {},
tasks: {
requests: { tools: { call: {} } },
taskStore,
taskMessageQueue: new InMemoryTaskMessageQueue()
}
}
}
);

View file

@ -30,6 +30,7 @@ import type {
ResultTypeMap,
ServerCapabilities,
SubscribeRequest,
TaskManagerOptions,
Tool,
Transport,
UnsubscribeRequest
@ -46,6 +47,7 @@ import {
ElicitRequestSchema,
ElicitResultSchema,
EmptyResultSchema,
extractTaskManagerOptions,
GetPromptResultSchema,
InitializeResultSchema,
LATEST_PROTOCOL_VERSION,
@ -140,11 +142,19 @@ export function getSupportedElicitationModes(capabilities: ClientCapabilities['e
return { supportsFormMode, supportsUrlMode };
}
/**
* Extended tasks capability that includes runtime configuration (store, messageQueue).
* The runtime-only fields are stripped before advertising capabilities to servers.
*/
export type ClientTasksCapabilityWithRuntime = NonNullable<ClientCapabilities['tasks']> & TaskManagerOptions;
export type ClientOptions = ProtocolOptions & {
/**
* Capabilities to advertise as being supported by this client.
*/
capabilities?: ClientCapabilities;
capabilities?: Omit<ClientCapabilities, 'tasks'> & {
tasks?: ClientTasksCapabilityWithRuntime;
};
/**
* JSON Schema validator for tool output validation.
@ -213,11 +223,22 @@ export class Client extends Protocol<ClientContext> {
private _clientInfo: Implementation,
options?: ClientOptions
) {
super(options);
this._capabilities = options?.capabilities ?? {};
super({
...options,
tasks: extractTaskManagerOptions(options?.capabilities?.tasks)
});
this._capabilities = options?.capabilities ? { ...options.capabilities } : {};
this._jsonSchemaValidator = options?.jsonSchemaValidator ?? new DefaultJsonSchemaValidator();
this._enforceStrictCapabilities = options?.enforceStrictCapabilities ?? false;
// Strip runtime-only fields from advertised capabilities
if (options?.capabilities?.tasks) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { taskStore, taskMessageQueue, defaultTaskPollInterval, maxTaskQueueSize, ...wireCapabilities } =
options.capabilities.tasks;
this._capabilities.tasks = wireCapabilities;
}
// Store list changed config for setup after connection (when we know server capabilities)
if (options?.listChanged) {
this._pendingListChangedConfig = options.listChanged;
@ -650,12 +671,6 @@ export class Client extends Protocol<ClientContext> {
}
protected assertRequestHandlerCapability(method: string): void {
// Task handlers are registered in Protocol constructor before _capabilities is initialized
// Skip capability check for task methods during initialization
if (!this._capabilities) {
return;
}
switch (method) {
case 'sampling/createMessage': {
if (!this._capabilities.sampling) {
@ -687,19 +702,6 @@ export class Client extends Protocol<ClientContext> {
break;
}
case 'tasks/get':
case 'tasks/list':
case 'tasks/result':
case 'tasks/cancel': {
if (!this._capabilities.tasks) {
throw new SdkError(
SdkErrorCode.CapabilityNotSupported,
`Client does not support tasks capability (required for ${method})`
);
}
break;
}
case 'ping': {
// No specific capability required for ping
break;
@ -712,16 +714,9 @@ export class Client extends Protocol<ClientContext> {
}
protected assertTaskHandlerCapability(method: string): void {
// Task handlers are registered in Protocol constructor before _capabilities is initialized
// Skip capability check for task methods during initialization
if (!this._capabilities) {
return;
}
assertClientRequestTaskCapability(this._capabilities.tasks?.requests, method, 'Client');
assertClientRequestTaskCapability(this._capabilities?.tasks?.requests, method, 'Client');
}
/** Sends a ping to the server to check connectivity. */
async ping(options?: RequestOptions) {
return this._requestWithSchema({ method: 'ping' }, EmptyResultSchema, options);
}

View file

@ -6,6 +6,7 @@
*/
import type {
AnyObjectSchema,
CallToolRequest,
CallToolResult,
CancelTaskResult,
@ -13,12 +14,19 @@ import type {
GetTaskPayloadResult,
GetTaskResult,
ListTasksResult,
Request,
RequestMethod,
RequestOptions,
ResponseMessage,
ResultTypeMap
} from '@modelcontextprotocol/core';
import { GetTaskPayloadResultSchema, ProtocolError, ProtocolErrorCode } from '@modelcontextprotocol/core';
import {
CallToolResultSchema,
getResultSchema,
GetTaskPayloadResultSchema,
ProtocolError,
ProtocolErrorCode
} from '@modelcontextprotocol/core';
import type { Client } from '../../client/client.js';
@ -27,10 +35,6 @@ import type { Client } from '../../client/client.js';
* @internal
*/
interface ClientInternal {
requestStream<M extends RequestMethod>(
request: { method: M; params?: Record<string, unknown> },
options?: RequestOptions
): AsyncGenerator<ResponseMessage<ResultTypeMap[M]>, void, void>;
isToolTask(toolName: string): boolean;
getToolOutputValidator(toolName: string): ((data: unknown) => { valid: boolean; errorMessage?: string }) | undefined;
}
@ -49,6 +53,10 @@ interface ClientInternal {
export class ExperimentalClientTasks {
constructor(private readonly _client: Client) {}
private get _module() {
return this._client.taskManager;
}
/**
* Calls a tool and returns an AsyncGenerator that yields response messages.
* The generator is guaranteed to end with either a `'result'` or `'error'` message.
@ -103,7 +111,7 @@ export class ExperimentalClientTasks {
task: options?.task ?? (clientInternal.isToolTask(params.name) ? {} : undefined)
};
const stream = clientInternal.requestStream({ method: 'tools/call', params }, optionsWithTask);
const stream = this._module.requestStream({ method: 'tools/call', params }, CallToolResultSchema, optionsWithTask);
// Get the validator for this tool (if it has an output schema)
const validator = clientInternal.getToolOutputValidator(params.name);
@ -175,9 +183,7 @@ export class ExperimentalClientTasks {
* @experimental
*/
async getTask(taskId: string, options?: RequestOptions): Promise<GetTaskResult> {
// Delegate to the client's underlying Protocol method
type ClientWithGetTask = { getTask(params: { taskId: string }, options?: RequestOptions): Promise<GetTaskResult> };
return (this._client as unknown as ClientWithGetTask).getTask({ taskId }, options);
return this._module.getTask({ taskId }, options);
}
/**
@ -191,15 +197,7 @@ export class ExperimentalClientTasks {
* @experimental
*/
async getTaskResult(taskId: string, options?: RequestOptions): Promise<GetTaskPayloadResult> {
return (
this._client as unknown as {
getTaskResult: (
params: { taskId: string },
resultSchema: typeof GetTaskPayloadResultSchema,
options?: RequestOptions
) => Promise<GetTaskPayloadResult>;
}
).getTaskResult({ taskId }, GetTaskPayloadResultSchema, options);
return this._module.getTaskResult({ taskId }, GetTaskPayloadResultSchema, options);
}
/**
@ -212,12 +210,7 @@ export class ExperimentalClientTasks {
* @experimental
*/
async listTasks(cursor?: string, options?: RequestOptions): Promise<ListTasksResult> {
// Delegate to the client's underlying Protocol method
return (
this._client as unknown as {
listTasks: (params?: { cursor?: string }, options?: RequestOptions) => Promise<ListTasksResult>;
}
).listTasks(cursor ? { cursor } : undefined, options);
return this._module.listTasks(cursor ? { cursor } : undefined, options);
}
/**
@ -229,12 +222,7 @@ export class ExperimentalClientTasks {
* @experimental
*/
async cancelTask(taskId: string, options?: RequestOptions): Promise<CancelTaskResult> {
// Delegate to the client's underlying Protocol method
return (
this._client as unknown as {
cancelTask: (params: { taskId: string }, options?: RequestOptions) => Promise<CancelTaskResult>;
}
).cancelTask({ taskId }, options);
return this._module.cancelTask({ taskId }, options);
}
/**
@ -279,7 +267,11 @@ export class ExperimentalClientTasks {
request: { method: M; params?: Record<string, unknown> },
options?: RequestOptions
): AsyncGenerator<ResponseMessage<ResultTypeMap[M]>, void, void> {
// Delegate to the client's underlying Protocol method
return (this._client as unknown as ClientInternal).requestStream(request, options);
const resultSchema = getResultSchema(request.method) as unknown as AnyObjectSchema;
return this._module.requestStream(request as Request, resultSchema, options) as AsyncGenerator<
ResponseMessage<ResultTypeMap[M]>,
void,
void
>;
}
}

View file

@ -17,7 +17,7 @@ interface TaskRequestsCapability {
/**
* Asserts that task creation is supported for `tools/call`.
* Used by {@linkcode @modelcontextprotocol/client!client/client.Client.assertTaskCapability | Client.assertTaskCapability} and {@linkcode @modelcontextprotocol/server!server/server.Server.assertTaskHandlerCapability | Server.assertTaskHandlerCapability}.
* Used to implement the `assertTaskCapability` or `assertTaskHandlerCapability` abstract methods on Protocol.
*
* @param requests - The task requests capability object
* @param method - The method being checked
@ -52,7 +52,7 @@ export function assertToolsCallTaskCapability(
/**
* Asserts that task creation is supported for `sampling/createMessage` or `elicitation/create`.
* Used by {@linkcode @modelcontextprotocol/server!server/server.Server.assertTaskCapability | Server.assertTaskCapability} and {@linkcode @modelcontextprotocol/client!client/client.Client.assertTaskHandlerCapability | Client.assertTaskHandlerCapability}.
* Used to implement the `assertTaskCapability` or `assertTaskHandlerCapability` abstract methods on Protocol.
*
* @param requests - The task requests capability object
* @param method - The method being checked

View file

@ -3,7 +3,8 @@
* WARNING: These APIs are experimental and may change without notice.
*/
import type { RequestTaskStore, ServerContext } from '../../shared/protocol.js';
import type { ServerContext } from '../../shared/protocol.js';
import type { RequestTaskStore } from '../../shared/taskManager.js';
import type {
JSONRPCErrorResponse,
JSONRPCNotification,

View file

@ -6,6 +6,8 @@ export * from './shared/metadataUtils.js';
export * from './shared/protocol.js';
export * from './shared/responseMessage.js';
export * from './shared/stdio.js';
export type { RequestTaskStore, TaskContext, TaskManagerOptions, TaskRequestOptions } from './shared/taskManager.js';
export { extractTaskManagerOptions, NullTaskManager, TaskManager } from './shared/taskManager.js';
export * from './shared/toolNameValidation.js';
export * from './shared/transport.js';
export * from './shared/uriTemplate.js';

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,900 @@
import type { CreateTaskOptions, QueuedMessage, TaskMessageQueue, TaskStore } from '../experimental/tasks/interfaces.js';
import { isTerminal } from '../experimental/tasks/interfaces.js';
import type {
GetTaskPayloadRequest,
GetTaskRequest,
GetTaskResult,
JSONRPCErrorResponse,
JSONRPCNotification,
JSONRPCRequest,
JSONRPCResponse,
JSONRPCResultResponse,
Notification,
Request,
RequestId,
Result,
Task,
TaskCreationParams,
TaskStatusNotification
} from '../types/types.js';
import {
CancelTaskResultSchema,
CreateTaskResultSchema,
GetTaskResultSchema,
isJSONRPCErrorResponse,
isJSONRPCRequest,
isJSONRPCResultResponse,
isTaskAugmentedRequestParams,
ListTasksResultSchema,
ProtocolError,
ProtocolErrorCode,
RELATED_TASK_META_KEY,
TaskStatusNotificationSchema
} from '../types/types.js';
import type { AnyObjectSchema, AnySchema, SchemaOutput } from '../util/schema.js';
import type { BaseContext, NotificationOptions, RequestOptions } from './protocol.js';
import type { ResponseMessage } from './responseMessage.js';
/**
* Host interface for TaskManager to call back into Protocol. @internal
*/
export interface TaskManagerHost {
request<T extends AnySchema>(request: Request, resultSchema: T, options?: RequestOptions): Promise<SchemaOutput<T>>;
notification(notification: Notification, options?: NotificationOptions): Promise<void>;
reportError(error: Error): void;
removeProgressHandler(token: number): void;
registerHandler(method: string, handler: (request: JSONRPCRequest, ctx: BaseContext) => Promise<Result>): void;
sendOnResponseStream(message: JSONRPCNotification | JSONRPCRequest, relatedRequestId: RequestId): Promise<void>;
enforceStrictCapabilities: boolean;
assertTaskCapability(method: string): void;
assertTaskHandlerCapability(method: string): void;
}
/**
* Context provided to TaskManager when processing an inbound request.
* @internal
*/
export interface InboundContext {
sessionId?: string;
sendNotification: (notification: Notification, options?: NotificationOptions) => Promise<void>;
sendRequest: <U extends AnySchema>(request: Request, resultSchema: U, options?: RequestOptions) => Promise<SchemaOutput<U>>;
}
/**
* Result returned by TaskManager after processing an inbound request.
* @internal
*/
export interface InboundResult {
taskContext?: BaseContext['task'];
sendNotification: (notification: Notification) => Promise<void>;
sendRequest: <U extends AnySchema>(
request: Request,
resultSchema: U,
options?: Omit<RequestOptions, 'relatedTask'>
) => Promise<SchemaOutput<U>>;
routeResponse: (message: JSONRPCResponse | JSONRPCErrorResponse) => Promise<boolean>;
hasTaskCreationParams: boolean;
/**
* Optional validation to run inside the async handler chain (before the request handler).
* Throwing here produces a proper JSON-RPC error response, matching the behavior of
* capability checks on main.
*/
validateInbound?: () => void;
}
/**
* Options that can be given per request.
*/
// relatedTask is excluded as the SDK controls if this is sent according to if the source is a task.
export type TaskRequestOptions = Omit<RequestOptions, 'relatedTask'>;
/**
* Request-scoped TaskStore interface.
*/
export interface RequestTaskStore {
/**
* Creates a new task with the given creation parameters.
* The implementation generates a unique taskId and createdAt timestamp.
*
* @param taskParams - The task creation parameters from the request
* @returns The created task object
*/
createTask(taskParams: CreateTaskOptions): Promise<Task>;
/**
* Gets the current status of a task.
*
* @param taskId - The task identifier
* @returns The task object
* @throws If the task does not exist
*/
getTask(taskId: string): Promise<Task>;
/**
* Stores the result of a task and sets its final status.
*
* @param taskId - The task identifier
* @param status - The final status: 'completed' for success, 'failed' for errors
* @param result - The result to store
*/
storeTaskResult(taskId: string, status: 'completed' | 'failed', result: Result): Promise<void>;
/**
* Retrieves the stored result of a task.
*
* @param taskId - The task identifier
* @returns The stored result
*/
getTaskResult(taskId: string): Promise<Result>;
/**
* Updates a task's status (e.g., to 'cancelled', 'failed', 'completed').
*
* @param taskId - The task identifier
* @param status - The new status
* @param statusMessage - Optional diagnostic message for failed tasks or other status information
*/
updateTaskStatus(taskId: string, status: Task['status'], statusMessage?: string): Promise<void>;
/**
* Lists tasks, optionally starting from a pagination cursor.
*
* @param cursor - Optional cursor for pagination
* @returns An object containing the tasks array and an optional nextCursor
*/
listTasks(cursor?: string): Promise<{ tasks: Task[]; nextCursor?: string }>;
}
/**
* Task context provided to request handlers when task storage is configured.
*/
export type TaskContext = {
id?: string;
store: RequestTaskStore;
requestedTtl?: number | null;
};
export type TaskManagerOptions = {
/**
* Task storage implementation. Required for handling incoming task requests (server-side).
* Not required for sending task requests (client-side outbound API).
*/
taskStore?: TaskStore;
/**
* Optional task message queue implementation for managing server-initiated messages
* that will be delivered through the tasks/result response stream.
*/
taskMessageQueue?: TaskMessageQueue;
/**
* Default polling interval (in milliseconds) for task status checks when no pollInterval
* is provided by the server. Defaults to 1000ms if not specified.
*/
defaultTaskPollInterval?: number;
/**
* Maximum number of messages that can be queued per task for side-channel delivery.
* If undefined, the queue size is unbounded.
*/
maxTaskQueueSize?: number;
};
/**
* Extracts {@linkcode TaskManagerOptions} from a capability object that mixes in runtime fields.
* Returns `undefined` when no task capability is configured.
*/
export function extractTaskManagerOptions(tasksCapability: TaskManagerOptions | undefined): TaskManagerOptions | undefined {
if (!tasksCapability) return undefined;
const { taskStore, taskMessageQueue, defaultTaskPollInterval, maxTaskQueueSize } = tasksCapability;
return { taskStore, taskMessageQueue, defaultTaskPollInterval, maxTaskQueueSize };
}
/**
* Manages task orchestration: state, message queuing, and polling.
* Capability checking is delegated to the Protocol host.
* @internal
*/
export class TaskManager {
private _taskStore?: TaskStore;
private _taskMessageQueue?: TaskMessageQueue;
private _taskProgressTokens: Map<string, number> = new Map();
private _requestResolvers: Map<RequestId, (response: JSONRPCResultResponse | Error) => void> = new Map();
private _options: TaskManagerOptions;
private _host?: TaskManagerHost;
constructor(options: TaskManagerOptions) {
this._options = options;
this._taskStore = options.taskStore;
this._taskMessageQueue = options.taskMessageQueue;
}
bind(host: TaskManagerHost): void {
this._host = host;
if (this._taskStore) {
host.registerHandler('tasks/get', async (request, ctx) => {
const params = request.params as { taskId: string };
const task = await this.handleGetTask(params.taskId, ctx.sessionId);
// Per spec: tasks/get responses SHALL NOT include related-task metadata
// as the taskId parameter is the source of truth
return {
...task
} as Result;
});
host.registerHandler('tasks/result', async (request, ctx) => {
const params = request.params as { taskId: string };
return await this.handleGetTaskPayload(params.taskId, ctx.sessionId, ctx.mcpReq.signal, async message => {
// 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 host.sendOnResponseStream(message, ctx.mcpReq.id);
});
});
host.registerHandler('tasks/list', async (request, ctx) => {
const params = request.params as { cursor?: string } | undefined;
return (await this.handleListTasks(params?.cursor, ctx.sessionId)) as Result;
});
host.registerHandler('tasks/cancel', async (request, ctx) => {
const params = request.params as { taskId: string };
return await this.handleCancelTask(params.taskId, ctx.sessionId);
});
}
}
protected get _requireHost(): TaskManagerHost {
if (!this._host) {
throw new ProtocolError(ProtocolErrorCode.InternalError, 'TaskManager is not bound to a Protocol host — call bind() first');
}
return this._host;
}
get taskStore(): TaskStore | undefined {
return this._taskStore;
}
private get _requireTaskStore(): TaskStore {
if (!this._taskStore) {
throw new ProtocolError(ProtocolErrorCode.InternalError, 'TaskStore is not configured');
}
return this._taskStore;
}
get taskMessageQueue(): TaskMessageQueue | undefined {
return this._taskMessageQueue;
}
// -- Public API (client-facing) --
async *requestStream<T extends AnyObjectSchema>(
request: Request,
resultSchema: T,
options?: RequestOptions
): AsyncGenerator<ResponseMessage<SchemaOutput<T>>, void, void> {
const host = this._requireHost;
const { task } = options ?? {};
if (!task) {
try {
const result = await host.request(request, resultSchema, options);
yield { type: 'result', result };
} catch (error) {
yield {
type: 'error',
error: error instanceof Error ? error : new Error(String(error))
};
}
return;
}
let taskId: string | undefined;
try {
const createResult = await host.request(request, CreateTaskResultSchema, options);
if (createResult.task) {
taskId = createResult.task.taskId;
yield { type: 'taskCreated', task: createResult.task };
} else {
throw new ProtocolError(ProtocolErrorCode.InternalError, 'Task creation did not return a task');
}
while (true) {
const task = await this.getTask({ taskId }, options);
yield { type: 'taskStatus', task };
if (isTerminal(task.status)) {
switch (task.status) {
case 'completed': {
const result = await this.getTaskResult({ taskId }, resultSchema, options);
yield { type: 'result', result };
break;
}
case 'failed': {
yield { type: 'error', error: new ProtocolError(ProtocolErrorCode.InternalError, `Task ${taskId} failed`) };
break;
}
case 'cancelled': {
yield {
type: 'error',
error: new ProtocolError(ProtocolErrorCode.InternalError, `Task ${taskId} was cancelled`)
};
break;
}
}
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 Error ? error : new Error(String(error))
};
}
}
async getTask(params: GetTaskRequest['params'], options?: RequestOptions): Promise<GetTaskResult> {
return this._requireHost.request({ method: 'tasks/get', params }, GetTaskResultSchema, options);
}
async getTaskResult<T extends AnySchema>(
params: GetTaskPayloadRequest['params'],
resultSchema: T,
options?: RequestOptions
): Promise<SchemaOutput<T>> {
return this._requireHost.request({ method: 'tasks/result', params }, resultSchema, options);
}
async listTasks(params?: { cursor?: string }, options?: RequestOptions): Promise<SchemaOutput<typeof ListTasksResultSchema>> {
return this._requireHost.request({ method: 'tasks/list', params }, ListTasksResultSchema, options);
}
async cancelTask(params: { taskId: string }, options?: RequestOptions): Promise<SchemaOutput<typeof CancelTaskResultSchema>> {
return this._requireHost.request({ method: 'tasks/cancel', params }, CancelTaskResultSchema, options);
}
// -- Handler bodies (delegated from Protocol's registered handlers) --
private async handleGetTask(taskId: string, sessionId?: string): Promise<Task> {
const task = await this._requireTaskStore.getTask(taskId, sessionId);
if (!task) {
throw new ProtocolError(ProtocolErrorCode.InvalidParams, 'Failed to retrieve task: Task not found');
}
return task;
}
private async handleGetTaskPayload(
taskId: string,
sessionId: string | undefined,
signal: AbortSignal,
sendOnResponseStream: (message: JSONRPCNotification | JSONRPCRequest) => Promise<void>
): Promise<Result> {
const handleTaskResult = async (): Promise<Result> => {
if (this._taskMessageQueue) {
let queuedMessage: QueuedMessage | undefined;
while ((queuedMessage = await this._taskMessageQueue.dequeue(taskId, sessionId))) {
if (queuedMessage.type === 'response' || queuedMessage.type === 'error') {
const message = queuedMessage.message;
const requestId = message.id;
const resolver = this._requestResolvers.get(requestId as RequestId);
if (resolver) {
this._requestResolvers.delete(requestId as RequestId);
if (queuedMessage.type === 'response') {
resolver(message as JSONRPCResultResponse);
} else {
const errorMessage = message as JSONRPCErrorResponse;
resolver(new ProtocolError(errorMessage.error.code, errorMessage.error.message, errorMessage.error.data));
}
} else {
const messageType = queuedMessage.type === 'response' ? 'Response' : 'Error';
this._host?.reportError(new Error(`${messageType} handler missing for request ${requestId}`));
}
continue;
}
await sendOnResponseStream(queuedMessage.message as JSONRPCNotification | JSONRPCRequest);
}
}
const task = await this._requireTaskStore.getTask(taskId, sessionId);
if (!task) {
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Task not found: ${taskId}`);
}
if (!isTerminal(task.status)) {
await this._waitForTaskUpdate(task.pollInterval, signal);
return await handleTaskResult();
}
const result = await this._requireTaskStore.getTaskResult(taskId, sessionId);
await this._clearTaskQueue(taskId);
return {
...result,
_meta: {
...result._meta,
[RELATED_TASK_META_KEY]: { taskId }
}
};
};
return await handleTaskResult();
}
private async handleListTasks(
cursor: string | undefined,
sessionId?: string
): Promise<{ tasks: Task[]; nextCursor?: string; _meta: Record<string, unknown> }> {
try {
const { tasks, nextCursor } = await this._requireTaskStore.listTasks(cursor, sessionId);
return { tasks, nextCursor, _meta: {} };
} catch (error) {
throw new ProtocolError(
ProtocolErrorCode.InvalidParams,
`Failed to list tasks: ${error instanceof Error ? error.message : String(error)}`
);
}
}
private async handleCancelTask(taskId: string, sessionId?: string): Promise<Result> {
try {
const task = await this._requireTaskStore.getTask(taskId, sessionId);
if (!task) {
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Task not found: ${taskId}`);
}
if (isTerminal(task.status)) {
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Cannot cancel task in terminal status: ${task.status}`);
}
await this._requireTaskStore.updateTaskStatus(taskId, 'cancelled', 'Client cancelled task execution.', sessionId);
await this._clearTaskQueue(taskId);
const cancelledTask = await this._requireTaskStore.getTask(taskId, sessionId);
if (!cancelledTask) {
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Task not found after cancellation: ${taskId}`);
}
return { _meta: {}, ...cancelledTask };
} catch (error) {
if (error instanceof ProtocolError) throw error;
throw new ProtocolError(
ProtocolErrorCode.InvalidRequest,
`Failed to cancel task: ${error instanceof Error ? error.message : String(error)}`
);
}
}
// -- Internal delegation methods --
private prepareOutboundRequest(
jsonrpcRequest: JSONRPCRequest,
options: RequestOptions | undefined,
messageId: number,
responseHandler: (response: JSONRPCResultResponse | Error) => void,
onError: (error: unknown) => void
): boolean {
const { task, relatedTask } = options ?? {};
if (task) {
jsonrpcRequest.params = {
...jsonrpcRequest.params,
task: task
};
}
if (relatedTask) {
jsonrpcRequest.params = {
...jsonrpcRequest.params,
_meta: {
...jsonrpcRequest.params?._meta,
[RELATED_TASK_META_KEY]: relatedTask
}
};
}
const relatedTaskId = relatedTask?.taskId;
if (relatedTaskId) {
this._requestResolvers.set(messageId, responseHandler);
this._enqueueTaskMessage(relatedTaskId, {
type: 'request',
message: jsonrpcRequest,
timestamp: Date.now()
}).catch(error => {
onError(error);
});
return true;
}
return false;
}
private extractInboundTaskContext(
request: JSONRPCRequest,
sessionId?: string
): {
relatedTaskId?: string;
taskCreationParams?: TaskCreationParams;
taskContext?: TaskContext;
} {
const relatedTaskId = (request.params?._meta as Record<string, { taskId?: string }> | undefined)?.[RELATED_TASK_META_KEY]?.taskId;
const taskCreationParams = isTaskAugmentedRequestParams(request.params) ? request.params.task : undefined;
// Provide task context whenever a task store is configured,
// not just for task-related requests — tools need ctx.task.store
let taskContext: TaskContext | undefined;
if (this._taskStore) {
const store = this.createRequestTaskStore(request, sessionId);
taskContext = {
id: relatedTaskId,
store,
requestedTtl: taskCreationParams?.ttl
};
}
if (!relatedTaskId && !taskCreationParams && !taskContext) {
return {};
}
return {
relatedTaskId,
taskCreationParams,
taskContext
};
}
private wrapSendNotification(
relatedTaskId: string,
originalSendNotification: (notification: Notification, options?: NotificationOptions) => Promise<void>
): (notification: Notification) => Promise<void> {
return async (notification: Notification) => {
const notificationOptions: NotificationOptions = { relatedTask: { taskId: relatedTaskId } };
await originalSendNotification(notification, notificationOptions);
};
}
private wrapSendRequest(
relatedTaskId: string,
taskStore: RequestTaskStore | undefined,
originalSendRequest: <V extends AnySchema>(request: Request, resultSchema: V, options?: RequestOptions) => Promise<SchemaOutput<V>>
): <V extends AnySchema>(request: Request, resultSchema: V, options?: TaskRequestOptions) => Promise<SchemaOutput<V>> {
return async <V extends AnySchema>(request: Request, resultSchema: V, options?: TaskRequestOptions) => {
const requestOptions: RequestOptions = { ...options };
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 originalSendRequest(request, resultSchema, requestOptions);
};
}
private handleResponse(response: JSONRPCResponse | JSONRPCErrorResponse): boolean {
const messageId = Number(response.id);
const resolver = this._requestResolvers.get(messageId);
if (resolver) {
this._requestResolvers.delete(messageId);
if (isJSONRPCResultResponse(response)) {
resolver(response);
} else {
resolver(new ProtocolError(response.error.code, response.error.message, response.error.data));
}
return true;
}
return false;
}
private shouldPreserveProgressHandler(response: JSONRPCResponse | JSONRPCErrorResponse, messageId: number): boolean {
if (isJSONRPCResultResponse(response) && response.result && typeof response.result === 'object') {
const result = response.result as Record<string, unknown>;
if (result.task && typeof result.task === 'object') {
const task = result.task as Record<string, unknown>;
if (typeof task.taskId === 'string') {
this._taskProgressTokens.set(task.taskId, messageId);
return true;
}
}
}
return false;
}
private async routeNotification(notification: Notification, options?: NotificationOptions): Promise<boolean> {
const relatedTaskId = options?.relatedTask?.taskId;
if (!relatedTaskId) return false;
const jsonrpcNotification: JSONRPCNotification = {
...notification,
jsonrpc: '2.0',
params: {
...notification.params,
_meta: {
...notification.params?._meta,
[RELATED_TASK_META_KEY]: options!.relatedTask
}
}
};
await this._enqueueTaskMessage(relatedTaskId, {
type: 'notification',
message: jsonrpcNotification,
timestamp: Date.now()
});
return true;
}
private async routeResponse(
relatedTaskId: string | undefined,
message: JSONRPCResponse | JSONRPCErrorResponse,
sessionId?: string
): Promise<boolean> {
if (!relatedTaskId || !this._taskMessageQueue) return false;
await (isJSONRPCErrorResponse(message)
? this._enqueueTaskMessage(relatedTaskId, { type: 'error', message, timestamp: Date.now() }, sessionId)
: this._enqueueTaskMessage(
relatedTaskId,
{ type: 'response', message: message as JSONRPCResultResponse, timestamp: Date.now() },
sessionId
));
return true;
}
private createRequestTaskStore(request?: JSONRPCRequest, sessionId?: string): RequestTaskStore {
const taskStore = this._requireTaskStore;
const host = this._host;
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 ProtocolError(ProtocolErrorCode.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: TaskStatusNotification = TaskStatusNotificationSchema.parse({
method: 'notifications/tasks/status',
params: task
});
await host?.notification(notification as Notification);
if (isTerminal(task.status)) {
this._cleanupTaskProgressHandler(taskId);
}
}
},
getTaskResult: taskId => taskStore.getTaskResult(taskId, sessionId),
updateTaskStatus: async (taskId, status, statusMessage) => {
const task = await taskStore.getTask(taskId, sessionId);
if (!task) {
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Task "${taskId}" not found - it may have been cleaned up`);
}
if (isTerminal(task.status)) {
throw new ProtocolError(
ProtocolErrorCode.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: TaskStatusNotification = TaskStatusNotificationSchema.parse({
method: 'notifications/tasks/status',
params: updatedTask
});
await host?.notification(notification as Notification);
if (isTerminal(updatedTask.status)) {
this._cleanupTaskProgressHandler(taskId);
}
}
},
listTasks: cursor => taskStore.listTasks(cursor, sessionId)
};
}
// -- Lifecycle methods (called by Protocol directly) --
processInboundRequest(request: JSONRPCRequest, ctx: InboundContext): InboundResult {
const taskInfo = this.extractInboundTaskContext(request, ctx.sessionId);
const relatedTaskId = taskInfo?.relatedTaskId;
const sendNotification = relatedTaskId
? this.wrapSendNotification(relatedTaskId, ctx.sendNotification)
: (notification: Notification) => ctx.sendNotification(notification);
const sendRequest = relatedTaskId
? this.wrapSendRequest(relatedTaskId, taskInfo?.taskContext?.store, ctx.sendRequest)
: taskInfo?.taskContext
? this.wrapSendRequest('', taskInfo.taskContext.store, ctx.sendRequest)
: ctx.sendRequest;
const hasTaskCreationParams = !!taskInfo?.taskCreationParams;
return {
taskContext: taskInfo?.taskContext,
sendNotification,
sendRequest,
routeResponse: async (message: JSONRPCResponse | JSONRPCErrorResponse) => {
if (relatedTaskId) {
return this.routeResponse(relatedTaskId, message, ctx.sessionId);
}
return false;
},
hasTaskCreationParams,
// Deferred validation: runs inside the async handler chain so errors
// produce proper JSON-RPC error responses (matching main's behavior).
validateInbound: hasTaskCreationParams ? () => this._requireHost.assertTaskHandlerCapability(request.method) : undefined
};
}
processOutboundRequest(
jsonrpcRequest: JSONRPCRequest,
options: RequestOptions | undefined,
messageId: number,
responseHandler: (response: JSONRPCResultResponse | Error) => void,
onError: (error: unknown) => void
): { queued: boolean } {
// Check task capability when sending a task-augmented request (matches main's enforceStrictCapabilities gate)
if (this._requireHost.enforceStrictCapabilities && options?.task) {
this._requireHost.assertTaskCapability(jsonrpcRequest.method);
}
const queued = this.prepareOutboundRequest(jsonrpcRequest, options, messageId, responseHandler, onError);
return { queued };
}
processInboundResponse(
response: JSONRPCResponse | JSONRPCErrorResponse,
messageId: number
): { consumed: boolean; preserveProgress: boolean } {
const consumed = this.handleResponse(response);
if (consumed) {
return { consumed: true, preserveProgress: false };
}
const preserveProgress = this.shouldPreserveProgressHandler(response, messageId);
return { consumed: false, preserveProgress };
}
async processOutboundNotification(
notification: Notification,
options?: NotificationOptions
): Promise<{ queued: boolean; jsonrpcNotification?: JSONRPCNotification }> {
// Try queuing first
const queued = await this.routeNotification(notification, options);
if (queued) return { queued: true };
// Build JSONRPC notification with optional relatedTask metadata
let jsonrpcNotification: JSONRPCNotification = { ...notification, jsonrpc: '2.0' };
if (options?.relatedTask) {
jsonrpcNotification = {
...jsonrpcNotification,
params: {
...jsonrpcNotification.params,
_meta: {
...jsonrpcNotification.params?._meta,
[RELATED_TASK_META_KEY]: options.relatedTask
}
}
};
}
return { queued: false, jsonrpcNotification };
}
onClose(): void {
this._taskProgressTokens.clear();
}
// -- Private helpers --
private async _enqueueTaskMessage(taskId: string, message: QueuedMessage, sessionId?: string): Promise<void> {
if (!this._taskStore || !this._taskMessageQueue) {
throw new Error('Cannot enqueue task message: taskStore and taskMessageQueue are not configured');
}
await this._taskMessageQueue.enqueue(taskId, message, sessionId, this._options.maxTaskQueueSize);
}
private async _clearTaskQueue(taskId: string, sessionId?: string): Promise<void> {
if (this._taskMessageQueue) {
const messages = await this._taskMessageQueue.dequeueAll(taskId, sessionId);
for (const message of messages) {
if (message.type === 'request' && isJSONRPCRequest(message.message)) {
const requestId = message.message.id as RequestId;
const resolver = this._requestResolvers.get(requestId);
if (resolver) {
resolver(new ProtocolError(ProtocolErrorCode.InternalError, 'Task cancelled or completed'));
this._requestResolvers.delete(requestId);
} else {
this._host?.reportError(new Error(`Resolver missing for request ${requestId} during task ${taskId} cleanup`));
}
}
}
}
}
private async _waitForTaskUpdate(pollInterval: number | undefined, signal: AbortSignal): Promise<void> {
const interval = pollInterval ?? this._options.defaultTaskPollInterval ?? 1000;
return new Promise((resolve, reject) => {
if (signal.aborted) {
reject(new ProtocolError(ProtocolErrorCode.InvalidRequest, 'Request cancelled'));
return;
}
const timeoutId = setTimeout(resolve, interval);
signal.addEventListener(
'abort',
() => {
clearTimeout(timeoutId);
reject(new ProtocolError(ProtocolErrorCode.InvalidRequest, 'Request cancelled'));
},
{ once: true }
);
});
}
private _cleanupTaskProgressHandler(taskId: string): void {
const progressToken = this._taskProgressTokens.get(taskId);
if (progressToken !== undefined) {
this._host?.removeProgressHandler(progressToken);
this._taskProgressTokens.delete(taskId);
}
}
}
/**
* No-op TaskManager used when tasks capability is not configured.
* Provides passthrough implementations for the hot paths, avoiding
* unnecessary task extraction logic on every request.
*/
export class NullTaskManager extends TaskManager {
constructor() {
super({});
}
override processInboundRequest(request: JSONRPCRequest, ctx: InboundContext): InboundResult {
const hasTaskCreationParams = isTaskAugmentedRequestParams(request.params) && !!request.params.task;
return {
taskContext: undefined,
sendNotification: (notification: Notification) => ctx.sendNotification(notification),
sendRequest: ctx.sendRequest,
routeResponse: async () => false,
hasTaskCreationParams,
validateInbound: hasTaskCreationParams ? () => this._requireHost.assertTaskHandlerCapability(request.method) : undefined
};
}
// processOutboundRequest is inherited - it handles task/relatedTask augmentation
// and only queues if relatedTask is set (which won't happen without a task store)
// processInboundResponse is inherited - it checks _requestResolvers (empty for NullTaskManager)
// and _taskProgressTokens (empty for NullTaskManager)
override async processOutboundNotification(
notification: Notification,
_options?: NotificationOptions
): Promise<{ queued: boolean; jsonrpcNotification?: JSONRPCNotification }> {
return { queued: false, jsonrpcNotification: { ...notification, jsonrpc: '2.0' } };
}
override onClose(): void {
// No-op
}
}

File diff suppressed because it is too large Load diff

View file

@ -39,10 +39,10 @@ describe('Protocol transport handling bug', () => {
protected assertNotificationCapability(): void {}
protected assertRequestHandlerCapability(): void {}
protected assertTaskCapability(): void {}
protected assertTaskHandlerCapability(): void {}
protected buildContext(ctx: BaseContext): BaseContext {
return ctx;
}
protected assertTaskHandlerCapability(): void {}
})();
transportA = new MockTransport('A');

View file

@ -6,6 +6,7 @@
*/
import type {
AnyObjectSchema,
CancelTaskResult,
CreateMessageRequestParams,
CreateMessageResult,
@ -15,12 +16,13 @@ import type {
GetTaskPayloadResult,
GetTaskResult,
ListTasksResult,
Request,
RequestMethod,
RequestOptions,
ResponseMessage,
ResultTypeMap
} from '@modelcontextprotocol/core';
import { GetTaskPayloadResultSchema } from '@modelcontextprotocol/core';
import { getResultSchema, GetTaskPayloadResultSchema } from '@modelcontextprotocol/core';
import type { Server } from '../../server/server.js';
@ -39,6 +41,10 @@ import type { Server } from '../../server/server.js';
export class ExperimentalServerTasks {
constructor(private readonly _server: Server) {}
private get _module() {
return this._server.taskManager;
}
/**
* Sends a request and returns an AsyncGenerator that yields response messages.
* The generator is guaranteed to end with either a `'result'` or `'error'` message.
@ -56,14 +62,12 @@ export class ExperimentalServerTasks {
request: { method: M; params?: Record<string, unknown> },
options?: RequestOptions
): AsyncGenerator<ResponseMessage<ResultTypeMap[M]>, void, void> {
// Delegate to the server's underlying Protocol method
type ServerWithRequestStream = {
requestStream<N extends RequestMethod>(
request: { method: N; params?: Record<string, unknown> },
options?: RequestOptions
): AsyncGenerator<ResponseMessage<ResultTypeMap[N]>, void, void>;
};
return (this._server as unknown as ServerWithRequestStream).requestStream(request, options);
const resultSchema = getResultSchema(request.method) as unknown as AnyObjectSchema;
return this._module.requestStream(request as Request, resultSchema, options) as AsyncGenerator<
ResponseMessage<ResultTypeMap[M]>,
void,
void
>;
}
/**
@ -250,8 +254,7 @@ export class ExperimentalServerTasks {
* @experimental
*/
async getTask(taskId: string, options?: RequestOptions): Promise<GetTaskResult> {
type ServerWithGetTask = { getTask(params: { taskId: string }, options?: RequestOptions): Promise<GetTaskResult> };
return (this._server as unknown as ServerWithGetTask).getTask({ taskId }, options);
return this._module.getTask({ taskId }, options);
}
/**
@ -265,15 +268,7 @@ export class ExperimentalServerTasks {
* @experimental
*/
async getTaskResult(taskId: string, options?: RequestOptions): Promise<GetTaskPayloadResult> {
return (
this._server as unknown as {
getTaskResult: (
params: { taskId: string },
resultSchema: typeof GetTaskPayloadResultSchema,
options?: RequestOptions
) => Promise<GetTaskPayloadResult>;
}
).getTaskResult({ taskId }, GetTaskPayloadResultSchema, options);
return this._module.getTaskResult({ taskId }, GetTaskPayloadResultSchema, options);
}
/**
@ -286,11 +281,7 @@ export class ExperimentalServerTasks {
* @experimental
*/
async listTasks(cursor?: string, options?: RequestOptions): Promise<ListTasksResult> {
return (
this._server as unknown as {
listTasks: (params?: { cursor?: string }, options?: RequestOptions) => Promise<ListTasksResult>;
}
).listTasks(cursor ? { cursor } : undefined, options);
return this._module.listTasks(cursor ? { cursor } : undefined, options);
}
/**
@ -302,10 +293,6 @@ export class ExperimentalServerTasks {
* @experimental
*/
async cancelTask(taskId: string, options?: RequestOptions): Promise<CancelTaskResult> {
return (
this._server as unknown as {
cancelTask: (params: { taskId: string }, options?: RequestOptions) => Promise<CancelTaskResult>;
}
).cancelTask({ taskId }, options);
return this._module.cancelTask({ taskId }, options);
}
}

View file

@ -29,6 +29,7 @@ import type {
ServerCapabilities,
ServerContext,
ServerResult,
TaskManagerOptions,
ToolResultContent,
ToolUseContent
} from '@modelcontextprotocol/core';
@ -42,6 +43,7 @@ import {
CreateTaskResultSchema,
ElicitResultSchema,
EmptyResultSchema,
extractTaskManagerOptions,
LATEST_PROTOCOL_VERSION,
ListRootsResultSchema,
LoggingLevelSchema,
@ -57,11 +59,19 @@ import { DefaultJsonSchemaValidator } from '@modelcontextprotocol/server/_shims'
import { ExperimentalServerTasks } from '../experimental/tasks/server.js';
/**
* Extended tasks capability that includes runtime configuration (store, messageQueue).
* The runtime-only fields are stripped before advertising capabilities to clients.
*/
export type ServerTasksCapabilityWithRuntime = NonNullable<ServerCapabilities['tasks']> & TaskManagerOptions;
export type ServerOptions = ProtocolOptions & {
/**
* Capabilities to advertise as being supported by this server.
*/
capabilities?: ServerCapabilities;
capabilities?: Omit<ServerCapabilities, 'tasks'> & {
tasks?: ServerTasksCapabilityWithRuntime;
};
/**
* Optional instructions describing how to use the server and its features.
@ -106,11 +116,22 @@ export class Server extends Protocol<ServerContext> {
private _serverInfo: Implementation,
options?: ServerOptions
) {
super(options);
this._capabilities = options?.capabilities ?? {};
super({
...options,
tasks: extractTaskManagerOptions(options?.capabilities?.tasks)
});
this._capabilities = options?.capabilities ? { ...options.capabilities } : {};
this._instructions = options?.instructions;
this._jsonSchemaValidator = options?.jsonSchemaValidator ?? new DefaultJsonSchemaValidator();
// Strip runtime-only fields from advertised capabilities
if (options?.capabilities?.tasks) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { taskStore, taskMessageQueue, defaultTaskPollInterval, maxTaskQueueSize, ...wireCapabilities } =
options.capabilities.tasks;
this._capabilities.tasks = wireCapabilities;
}
this.setRequestHandler('initialize', request => this._oninitialize(request));
this.setNotificationHandler('notifications/initialized', () => this.oninitialized?.());
@ -347,12 +368,6 @@ export class Server extends Protocol<ServerContext> {
}
protected assertRequestHandlerCapability(method: string): void {
// Task handlers are registered in Protocol constructor before _capabilities is initialized
// Skip capability check for task methods during initialization
if (!this._capabilities) {
return;
}
switch (method) {
case 'completion/complete': {
if (!this._capabilities.completions) {
@ -393,19 +408,6 @@ export class Server extends Protocol<ServerContext> {
break;
}
case 'tasks/get':
case 'tasks/list':
case 'tasks/result':
case 'tasks/cancel': {
if (!this._capabilities.tasks) {
throw new SdkError(
SdkErrorCode.CapabilityNotSupported,
`Server does not support tasks capability (required for ${method})`
);
}
break;
}
case 'ping':
case 'initialize': {
// No specific capability required for these methods
@ -419,13 +421,7 @@ export class Server extends Protocol<ServerContext> {
}
protected assertTaskHandlerCapability(method: string): void {
// Task handlers are registered in Protocol constructor before _capabilities is initialized
// Skip capability check for task methods during initialization
if (!this._capabilities) {
return;
}
assertToolsCallTaskCapability(this._capabilities.tasks?.requests, method, 'Server');
assertToolsCallTaskCapability(this._capabilities?.tasks?.requests, method, 'Server');
}
private async _oninitialize(request: InitializeRequest): Promise<InitializeResult> {

View file

@ -2258,10 +2258,13 @@ describe('outputSchema validation', () => {
throw new Error('Unknown tool');
});
const client = new Client({
name: 'test-client',
version: '1.0.0'
});
const client = new Client(
{
name: 'test-client',
version: '1.0.0'
},
{ capabilities: { tasks: { requests: { tools: { call: {} } } } } }
);
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
@ -2302,10 +2305,11 @@ describe('Task-based execution', () => {
tools: {
call: {}
}
}
},
taskStore: serverTaskStore
}
},
taskStore: serverTaskStore
}
}
);
@ -2342,10 +2346,13 @@ describe('Task-based execution', () => {
}
);
const client = new Client({
name: 'test-client',
version: '1.0.0'
});
const client = new Client(
{
name: 'test-client',
version: '1.0.0'
},
{ capabilities: { tasks: { requests: { tools: { call: {} } } } } }
);
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
@ -2381,10 +2388,11 @@ describe('Task-based execution', () => {
tools: {
call: {}
}
}
},
taskStore: serverTaskStore
}
},
taskStore: serverTaskStore
}
}
);
@ -2421,10 +2429,13 @@ describe('Task-based execution', () => {
}
);
const client = new Client({
name: 'test-client',
version: '1.0.0'
});
const client = new Client(
{
name: 'test-client',
version: '1.0.0'
},
{ capabilities: { tasks: { requests: { tools: { call: {} } } } } }
);
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
@ -2461,10 +2472,11 @@ describe('Task-based execution', () => {
call: {},
list: {}
}
}
},
taskStore: serverTaskStore
}
},
taskStore: serverTaskStore
}
}
);
@ -2501,10 +2513,13 @@ describe('Task-based execution', () => {
}
);
const client = new Client({
name: 'test-client',
version: '1.0.0'
});
const client = new Client(
{
name: 'test-client',
version: '1.0.0'
},
{ capabilities: { tasks: { requests: { tools: { call: {} } } } } }
);
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
@ -2545,10 +2560,11 @@ describe('Task-based execution', () => {
tools: {
call: {}
}
}
},
taskStore: serverTaskStore
}
},
taskStore: serverTaskStore
}
}
);
@ -2585,10 +2601,13 @@ describe('Task-based execution', () => {
}
);
const client = new Client({
name: 'test-client',
version: '1.0.0'
});
const client = new Client(
{
name: 'test-client',
version: '1.0.0'
},
{ capabilities: { tasks: { requests: { tools: { call: {} } } } } }
);
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
@ -2652,10 +2671,11 @@ describe('Task-based execution', () => {
elicitation: {
create: {}
}
}
},
taskStore: clientTaskStore
}
},
taskStore: clientTaskStore
}
}
);
@ -2744,10 +2764,11 @@ describe('Task-based execution', () => {
elicitation: {
create: {}
}
}
},
taskStore: clientTaskStore
}
},
taskStore: clientTaskStore
}
}
);
@ -2835,10 +2856,11 @@ describe('Task-based execution', () => {
elicitation: {
create: {}
}
}
},
taskStore: clientTaskStore
}
},
taskStore: clientTaskStore
}
}
);
@ -2925,10 +2947,11 @@ describe('Task-based execution', () => {
elicitation: {
create: {}
}
}
},
taskStore: clientTaskStore
}
},
taskStore: clientTaskStore
}
}
);
@ -3027,10 +3050,11 @@ describe('Task-based execution', () => {
tools: {
call: {}
}
}
},
taskStore: serverTaskStore
}
},
taskStore: serverTaskStore
}
}
);
@ -3152,10 +3176,11 @@ describe('Task-based execution', () => {
tools: {
call: {}
}
}
},
taskStore: serverTaskStore
}
},
taskStore: serverTaskStore
}
}
);
@ -3199,10 +3224,11 @@ describe('Task-based execution', () => {
tools: {
call: {}
}
}
},
taskStore: serverTaskStore
}
},
taskStore: serverTaskStore
}
}
);
@ -3246,10 +3272,11 @@ describe('Task-based execution', () => {
elicitation: {
create: {}
}
}
},
taskStore: clientTaskStore
}
},
taskStore: clientTaskStore
}
}
);
@ -3300,10 +3327,11 @@ test('should respect server task capabilities', async () => {
tools: {
call: {}
}
}
},
taskStore: serverTaskStore
}
},
taskStore: serverTaskStore
}
}
);
@ -3346,7 +3374,16 @@ test('should respect server task capabilities', async () => {
version: '1.0.0'
},
{
enforceStrictCapabilities: true
enforceStrictCapabilities: true,
capabilities: {
tasks: {
requests: {
tools: {
call: {}
}
}
}
}
}
);
@ -3419,7 +3456,7 @@ test('should expose requestStream() method for streaming responses', async () =>
version: '1.0.0'
},
{
capabilities: {}
capabilities: { tasks: { requests: { tools: { call: {} } } } }
}
);
@ -3481,7 +3518,7 @@ test('should expose callToolStream() method for streaming tool calls', async ()
version: '1.0.0'
},
{
capabilities: {}
capabilities: { tasks: { requests: { tools: { call: {} } } } }
}
);
@ -3559,7 +3596,7 @@ test('should validate structured output in callToolStream()', async () => {
version: '1.0.0'
},
{
capabilities: {}
capabilities: { tasks: { requests: { tools: { call: {} } } } }
}
);
@ -3637,7 +3674,7 @@ test('callToolStream() should yield error when structuredContent does not match
version: '1.0.0'
},
{
capabilities: {}
capabilities: { tasks: { requests: { tools: { call: {} } } } }
}
);
@ -3710,7 +3747,7 @@ test('callToolStream() should yield error when tool with outputSchema returns no
version: '1.0.0'
},
{
capabilities: {}
capabilities: { tasks: { requests: { tools: { call: {} } } } }
}
);
@ -3775,7 +3812,7 @@ test('callToolStream() should handle tools without outputSchema normally', async
version: '1.0.0'
},
{
capabilities: {}
capabilities: { tasks: { requests: { tools: { call: {} } } } }
}
);
@ -3870,7 +3907,7 @@ test('callToolStream() should handle complex JSON schema validation', async () =
version: '1.0.0'
},
{
capabilities: {}
capabilities: { tasks: { requests: { tools: { call: {} } } } }
}
);
@ -3949,7 +3986,7 @@ test('callToolStream() should yield error with additional properties when not al
version: '1.0.0'
},
{
capabilities: {}
capabilities: { tasks: { requests: { tools: { call: {} } } } }
}
);
@ -4023,7 +4060,7 @@ test('callToolStream() should not validate structuredContent when isError is tru
version: '1.0.0'
},
{
capabilities: {}
capabilities: { tasks: { requests: { tools: { call: {} } } } }
}
);

View file

@ -50,11 +50,11 @@ export async function createInMemoryTaskEnvironment(options?: {
tools: {
call: {}
}
}
},
taskStore,
taskMessageQueue: new InMemoryTaskMessageQueue()
}
},
taskStore,
taskMessageQueue: new InMemoryTaskMessageQueue()
}
}
);

View file

@ -1875,182 +1875,195 @@ describe('createMessageStream', () => {
}).toThrow('tool_result blocks are not matching any tool_use from the previous message');
});
describe('terminal message guarantees', () => {
test('should yield exactly one terminal message for successful request', async () => {
const server = new Server({ name: 'test server', version: '1.0' }, { capabilities: {} });
const client = new Client({ name: 'test client', version: '1.0' }, { capabilities: { sampling: {} } });
describe('with tasks', () => {
let server: Server;
let client: Client;
let clientTransport: ReturnType<typeof InMemoryTransport.createLinkedPair>[0];
let serverTransport: ReturnType<typeof InMemoryTransport.createLinkedPair>[1];
client.setRequestHandler('sampling/createMessage', async () => ({
role: 'assistant',
content: { type: 'text', text: 'Response' },
model: 'test-model'
}));
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
const stream = server.experimental.tasks.createMessageStream({
messages: [{ role: 'user', content: { type: 'text', text: 'Hello' } }],
maxTokens: 100
});
const allMessages = await toArrayAsync(stream);
expect(allMessages.length).toBe(1);
expect(allMessages[0].type).toBe('result');
const taskMessages = allMessages.filter(m => m.type === 'taskCreated' || m.type === 'taskStatus');
expect(taskMessages.length).toBe(0);
});
test('should yield error as terminal message when client returns error', async () => {
const server = new Server({ name: 'test server', version: '1.0' }, { capabilities: {} });
const client = new Client({ name: 'test client', version: '1.0' }, { capabilities: { sampling: {} } });
client.setRequestHandler('sampling/createMessage', async () => {
throw new Error('Simulated client error');
});
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
const stream = server.experimental.tasks.createMessageStream({
messages: [{ role: 'user', content: { type: 'text', text: 'Hello' } }],
maxTokens: 100
});
const allMessages = await toArrayAsync(stream);
expect(allMessages.length).toBe(1);
expect(allMessages[0].type).toBe('error');
});
test('should yield exactly one terminal message with result', async () => {
const server = new Server({ name: 'test server', version: '1.0' }, { capabilities: {} });
const client = new Client({ name: 'test client', version: '1.0' }, { capabilities: { sampling: {} } });
client.setRequestHandler('sampling/createMessage', () => ({
model: 'test-model',
role: 'assistant' as const,
content: { type: 'text' as const, text: 'Response' }
}));
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
const stream = server.experimental.tasks.createMessageStream({
messages: [{ role: 'user', content: { type: 'text', text: 'Message' } }],
maxTokens: 100
});
const messages = await toArrayAsync(stream);
const terminalMessages = messages.filter(m => m.type === 'result' || m.type === 'error');
expect(terminalMessages.length).toBe(1);
const lastMessage = messages.at(-1);
expect(lastMessage.type === 'result' || lastMessage.type === 'error').toBe(true);
if (lastMessage.type === 'result') {
expect((lastMessage.result as CreateMessageResult).content).toBeDefined();
}
});
});
describe('non-task request minimality', () => {
test('should yield only result message for non-task request', async () => {
const server = new Server({ name: 'test server', version: '1.0' }, { capabilities: {} });
const client = new Client({ name: 'test client', version: '1.0' }, { capabilities: { sampling: {} } });
client.setRequestHandler('sampling/createMessage', () => ({
model: 'test-model',
role: 'assistant' as const,
content: { type: 'text' as const, text: 'Response' }
}));
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
const stream = server.experimental.tasks.createMessageStream({
messages: [{ role: 'user', content: { type: 'text', text: 'Message' } }],
maxTokens: 100
});
const messages = await toArrayAsync(stream);
const taskMessages = messages.filter(m => m.type === 'taskCreated' || m.type === 'taskStatus');
expect(taskMessages.length).toBe(0);
const resultMessages = messages.filter(m => m.type === 'result');
expect(resultMessages.length).toBe(1);
expect(messages.length).toBe(1);
});
});
describe('task-augmented request handling', () => {
test('should yield taskCreated and result for task-augmented request', async () => {
const clientTaskStore = new InMemoryTaskStore();
const server = new Server({ name: 'test server', version: '1.0' }, { capabilities: {} });
const client = new Client(
{ name: 'test client', version: '1.0' },
beforeEach(async () => {
server = new Server(
{ name: 'test server', version: '1.0' },
{
capabilities: {
sampling: {},
tasks: {
requests: {
sampling: { createMessage: {} }
}
taskStore: new InMemoryTaskStore()
}
},
taskStore: clientTaskStore
}
}
);
client.setRequestHandler('sampling/createMessage', async (request, extra) => {
const result = {
model: 'test-model',
role: 'assistant' as const,
content: { type: 'text' as const, text: 'Task response' }
};
client = new Client({ name: 'test client', version: '1.0' }, { capabilities: { sampling: {} } });
if (request.params.task && extra.task?.store) {
const task = await extra.task.store.createTask({ ttl: extra.task.requestedTtl });
await extra.task.store.storeTaskResult(task.taskId, 'completed', result);
return { task };
}
return result;
[clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
});
afterEach(async () => {
await server.close().catch(() => {});
await client.close().catch(() => {});
});
describe('terminal message guarantees', () => {
test('should yield exactly one terminal message for successful request', async () => {
client.setRequestHandler('sampling/createMessage', async () => ({
role: 'assistant',
content: { type: 'text', text: 'Response' },
model: 'test-model'
}));
await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
const stream = server.experimental.tasks.createMessageStream({
messages: [{ role: 'user', content: { type: 'text', text: 'Hello' } }],
maxTokens: 100
});
const allMessages = await toArrayAsync(stream);
expect(allMessages.length).toBe(1);
expect(allMessages[0].type).toBe('result');
const taskMessages = allMessages.filter(m => m.type === 'taskCreated' || m.type === 'taskStatus');
expect(taskMessages.length).toBe(0);
});
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
test('should yield error as terminal message when client returns error', async () => {
client.setRequestHandler('sampling/createMessage', async () => {
throw new Error('Simulated client error');
});
const stream = server.experimental.tasks.createMessageStream(
{
messages: [{ role: 'user', content: { type: 'text', text: 'Task-augmented message' } }],
await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
const stream = server.experimental.tasks.createMessageStream({
messages: [{ role: 'user', content: { type: 'text', text: 'Hello' } }],
maxTokens: 100
},
{ task: { ttl: 60_000 } }
);
});
const messages = await toArrayAsync(stream);
const allMessages = await toArrayAsync(stream);
// Should have taskCreated and result
expect(messages.length).toBeGreaterThanOrEqual(2);
expect(allMessages.length).toBe(1);
expect(allMessages[0].type).toBe('error');
});
// First message should be taskCreated
expect(messages[0].type).toBe('taskCreated');
const taskCreated = messages[0] as { type: 'taskCreated'; task: Task };
expect(taskCreated.task.taskId).toBeDefined();
test('should yield exactly one terminal message with result', async () => {
client.setRequestHandler('sampling/createMessage', () => ({
model: 'test-model',
role: 'assistant' as const,
content: { type: 'text' as const, text: 'Response' }
}));
// Last message should be result
const lastMessage = messages.at(-1);
expect(lastMessage.type).toBe('result');
if (lastMessage.type === 'result') {
expect((lastMessage.result as CreateMessageResult).model).toBe('test-model');
}
await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
clientTaskStore.cleanup();
const stream = server.experimental.tasks.createMessageStream({
messages: [{ role: 'user', content: { type: 'text', text: 'Message' } }],
maxTokens: 100
});
const messages = await toArrayAsync(stream);
const terminalMessages = messages.filter(m => m.type === 'result' || m.type === 'error');
expect(terminalMessages.length).toBe(1);
const lastMessage = messages.at(-1);
expect(lastMessage.type === 'result' || lastMessage.type === 'error').toBe(true);
if (lastMessage.type === 'result') {
expect((lastMessage.result as CreateMessageResult).content).toBeDefined();
}
});
});
describe('non-task request minimality', () => {
test('should yield only result message for non-task request', async () => {
client.setRequestHandler('sampling/createMessage', () => ({
model: 'test-model',
role: 'assistant' as const,
content: { type: 'text' as const, text: 'Response' }
}));
await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
const stream = server.experimental.tasks.createMessageStream({
messages: [{ role: 'user', content: { type: 'text', text: 'Message' } }],
maxTokens: 100
});
const messages = await toArrayAsync(stream);
const taskMessages = messages.filter(m => m.type === 'taskCreated' || m.type === 'taskStatus');
expect(taskMessages.length).toBe(0);
const resultMessages = messages.filter(m => m.type === 'result');
expect(resultMessages.length).toBe(1);
expect(messages.length).toBe(1);
});
});
describe('task-augmented request handling', () => {
test('should yield taskCreated and result for task-augmented request', async () => {
const clientTaskStore = new InMemoryTaskStore();
const taskClient = new Client(
{ name: 'test client', version: '1.0' },
{
capabilities: {
sampling: {},
tasks: {
taskStore: clientTaskStore,
requests: {
sampling: { createMessage: {} }
}
}
}
}
);
taskClient.setRequestHandler('sampling/createMessage', async (request, extra) => {
const result = {
model: 'test-model',
role: 'assistant' as const,
content: { type: 'text' as const, text: 'Task response' }
};
if (request.params.task && extra.task?.store) {
const task = await extra.task.store.createTask({ ttl: extra.task.requestedTtl });
await extra.task.store.storeTaskResult(task.taskId, 'completed', result);
return { task };
}
return result;
});
const [taskClientTransport, taskServerTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([taskClient.connect(taskClientTransport), server.connect(taskServerTransport)]);
const stream = server.experimental.tasks.createMessageStream(
{
messages: [{ role: 'user', content: { type: 'text', text: 'Task-augmented message' } }],
maxTokens: 100
},
{ task: { ttl: 60_000 } }
);
const messages = await toArrayAsync(stream);
// Should have taskCreated and result
expect(messages.length).toBeGreaterThanOrEqual(2);
// First message should be taskCreated
expect(messages[0].type).toBe('taskCreated');
const taskCreated = messages[0] as { type: 'taskCreated'; task: Task };
expect(taskCreated.task.taskId).toBeDefined();
// Last message should be result
const lastMessage = messages.at(-1);
expect(lastMessage.type).toBe('result');
if (lastMessage.type === 'result') {
expect((lastMessage.result as CreateMessageResult).model).toBe('test-model');
}
clientTaskStore.cleanup();
await taskClient.close().catch(() => {});
});
});
});
});
@ -2362,10 +2375,11 @@ describe('Task-based execution', () => {
tools: {
call: {}
}
}
},
taskStore
}
},
taskStore
}
}
);
@ -2542,10 +2556,11 @@ describe('Task-based execution', () => {
tools: {
call: {}
}
}
},
taskStore
}
},
taskStore
}
}
);
@ -2719,10 +2734,11 @@ describe('Task-based execution', () => {
elicitation: {
create: {}
}
}
},
taskStore: clientTaskStore
}
},
taskStore: clientTaskStore
}
}
);
@ -2746,10 +2762,23 @@ describe('Task-based execution', () => {
return result;
});
const server = new Server({
name: 'test-server',
version: '1.0.0'
});
const server = new Server(
{
name: 'test-server',
version: '1.0.0'
},
{
capabilities: {
tasks: {
requests: {
elicitation: {
create: {}
}
}
}
}
}
);
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
@ -2799,10 +2828,11 @@ describe('Task-based execution', () => {
elicitation: {
create: {}
}
}
},
taskStore: clientTaskStore
}
},
taskStore: clientTaskStore
}
}
);
@ -2826,10 +2856,21 @@ describe('Task-based execution', () => {
return result;
});
const server = new Server({
name: 'test-server',
version: '1.0.0'
});
const server = new Server(
{
name: 'test-server',
version: '1.0.0'
},
{
capabilities: {
tasks: {
requests: {
elicitation: { create: {} }
}
}
}
}
);
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
@ -2877,10 +2918,11 @@ describe('Task-based execution', () => {
elicitation: {
create: {}
}
}
},
taskStore: clientTaskStore
}
},
taskStore: clientTaskStore
}
}
);
@ -2904,10 +2946,21 @@ describe('Task-based execution', () => {
return result;
});
const server = new Server({
name: 'test-server',
version: '1.0.0'
});
const server = new Server(
{
name: 'test-server',
version: '1.0.0'
},
{
capabilities: {
tasks: {
requests: {
elicitation: { create: {} }
}
}
}
}
);
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
@ -2957,10 +3010,11 @@ describe('Task-based execution', () => {
elicitation: {
create: {}
}
}
},
taskStore: clientTaskStore
}
},
taskStore: clientTaskStore
}
}
);
@ -3059,10 +3113,11 @@ describe('Task-based execution', () => {
tools: {
call: {}
}
}
},
taskStore
}
},
taskStore
}
}
);
@ -3198,10 +3253,11 @@ describe('Task-based execution', () => {
tools: {
call: {}
}
}
},
taskStore
}
},
taskStore
}
}
);
@ -3245,10 +3301,11 @@ describe('Task-based execution', () => {
elicitation: {
create: {}
}
}
},
taskStore: clientTaskStore
}
},
taskStore: clientTaskStore
}
}
);
@ -3302,10 +3359,11 @@ test('should respect client task capabilities', async () => {
elicitation: {
create: {}
}
}
},
taskStore: clientTaskStore
}
},
taskStore: clientTaskStore
}
}
);
@ -3414,7 +3472,16 @@ describe('elicitInputStream', () => {
let serverTransport: ReturnType<typeof InMemoryTransport.createLinkedPair>[1];
beforeEach(async () => {
server = new Server({ name: 'test server', version: '1.0' }, { capabilities: {} });
server = new Server(
{ name: 'test server', version: '1.0' },
{
capabilities: {
tasks: {
taskStore: new InMemoryTaskStore()
}
}
}
);
client = new Client(
{ name: 'test client', version: '1.0' },
@ -3643,12 +3710,12 @@ describe('elicitInputStream', () => {
capabilities: {
elicitation: { form: {} },
tasks: {
taskStore: clientTaskStore,
requests: {
elicitation: { create: {} }
}
}
},
taskStore: clientTaskStore
}
}
);

View file

@ -2035,10 +2035,11 @@ describe('Zod v4', () => {
tools: {
call: {}
}
}
},
taskStore
}
},
taskStore
}
}
);
@ -2104,10 +2105,11 @@ describe('Zod v4', () => {
tools: {
call: {}
}
}
},
taskStore
}
},
taskStore
}
}
);
@ -6367,10 +6369,11 @@ describe('Zod v4', () => {
tools: {
call: {}
}
}
},
taskStore
}
},
taskStore
}
}
);
@ -6469,10 +6472,11 @@ describe('Zod v4', () => {
tools: {
call: {}
}
}
},
taskStore
}
},
taskStore
}
}
);
@ -6574,10 +6578,11 @@ describe('Zod v4', () => {
tools: {
call: {}
}
}
},
taskStore
}
},
taskStore
}
}
);
@ -6694,10 +6699,11 @@ describe('Zod v4', () => {
tools: {
call: {}
}
}
},
taskStore
}
},
taskStore
}
}
);
@ -6797,10 +6803,11 @@ describe('Zod v4', () => {
tools: {
call: {}
}
}
},
taskStore
}
},
taskStore
}
}
);
@ -6895,10 +6902,11 @@ describe('Zod v4', () => {
tools: {
call: {}
}
}
},
taskStore
}
},
taskStore
}
}
);

View file

@ -39,11 +39,11 @@ describe('Task Lifecycle Integration Tests', () => {
}
},
list: {},
cancel: {}
cancel: {},
taskStore,
taskMessageQueue: new InMemoryTaskMessageQueue()
}
},
taskStore,
taskMessageQueue: new InMemoryTaskMessageQueue()
}
}
);
@ -300,10 +300,15 @@ describe('Task Lifecycle Integration Tests', () => {
describe('Task Cancellation', () => {
it('should cancel a working task and return the cancelled task', async () => {
const client = new Client({
name: 'test-client',
version: '1.0.0'
});
const client = new Client(
{
name: 'test-client',
version: '1.0.0'
},
{
capabilities: { tasks: {} }
}
);
const transport = new StreamableHTTPClientTransport(baseUrl);
await client.connect(transport);
@ -346,10 +351,15 @@ describe('Task Lifecycle Integration Tests', () => {
});
it('should reject cancellation of completed task with error code -32602', async () => {
const client = new Client({
name: 'test-client',
version: '1.0.0'
});
const client = new Client(
{
name: 'test-client',
version: '1.0.0'
},
{
capabilities: { tasks: {} }
}
);
const transport = new StreamableHTTPClientTransport(baseUrl);
await client.connect(transport);
@ -734,10 +744,15 @@ describe('Task Lifecycle Integration Tests', () => {
describe('Error Handling', () => {
it('should return error code -32602 for non-existent task in tasks/get', async () => {
const client = new Client({
name: 'test-client',
version: '1.0.0'
});
const client = new Client(
{
name: 'test-client',
version: '1.0.0'
},
{
capabilities: { tasks: {} }
}
);
const transport = new StreamableHTTPClientTransport(baseUrl);
await client.connect(transport);
@ -754,10 +769,15 @@ describe('Task Lifecycle Integration Tests', () => {
});
it('should return error code -32602 for non-existent task in tasks/cancel', async () => {
const client = new Client({
name: 'test-client',
version: '1.0.0'
});
const client = new Client(
{
name: 'test-client',
version: '1.0.0'
},
{
capabilities: { tasks: {} }
}
);
const transport = new StreamableHTTPClientTransport(baseUrl);
await client.connect(transport);
@ -1491,7 +1511,8 @@ describe('Task Lifecycle Integration Tests', () => {
},
{
capabilities: {
elicitation: {}
elicitation: {},
tasks: {}
}
}
);