[SEP-2663] refactor!: remove 2025-11 experimental tasks (#2128)
This commit is contained in:
parent
600ba75f34
commit
c8d7401b46
44 changed files with 423 additions and 18280 deletions
|
|
@ -29,24 +29,19 @@ import type {
|
|||
Result,
|
||||
ServerCapabilities,
|
||||
SubscribeRequest,
|
||||
TaskManagerOptions,
|
||||
Tool,
|
||||
Transport,
|
||||
UnsubscribeRequest
|
||||
} from '@modelcontextprotocol/core';
|
||||
import {
|
||||
assertClientRequestTaskCapability,
|
||||
assertToolsCallTaskCapability,
|
||||
CallToolResultSchema,
|
||||
CompleteResultSchema,
|
||||
CreateMessageRequestSchema,
|
||||
CreateMessageResultSchema,
|
||||
CreateMessageResultWithToolsSchema,
|
||||
CreateTaskResultSchema,
|
||||
ElicitRequestSchema,
|
||||
ElicitResultSchema,
|
||||
EmptyResultSchema,
|
||||
extractTaskManagerOptions,
|
||||
GetPromptResultSchema,
|
||||
InitializeResultSchema,
|
||||
LATEST_PROTOCOL_VERSION,
|
||||
|
|
@ -65,8 +60,6 @@ import {
|
|||
SdkErrorCode
|
||||
} from '@modelcontextprotocol/core';
|
||||
|
||||
import { ExperimentalClientTasks } from '../experimental/tasks/client.js';
|
||||
|
||||
/**
|
||||
* Elicitation default application helper. Applies defaults to the `data` based on the `schema`.
|
||||
*
|
||||
|
|
@ -141,19 +134,11 @@ 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?: Omit<ClientCapabilities, 'tasks'> & {
|
||||
tasks?: ClientTasksCapabilityWithRuntime;
|
||||
};
|
||||
capabilities?: ClientCapabilities;
|
||||
|
||||
/**
|
||||
* JSON Schema validator for tool output validation.
|
||||
|
|
@ -230,9 +215,6 @@ export class Client extends Protocol<ClientContext> {
|
|||
private _instructions?: string;
|
||||
private _jsonSchemaValidator: jsonSchemaValidator;
|
||||
private _cachedToolOutputValidators: Map<string, JsonSchemaValidator<unknown>> = new Map();
|
||||
private _cachedKnownTaskTools: Set<string> = new Set();
|
||||
private _cachedRequiredTaskTools: Set<string> = new Set();
|
||||
private _experimental?: { tasks: ExperimentalClientTasks };
|
||||
private _listChangedDebounceTimers: Map<string, ReturnType<typeof setTimeout>> = new Map();
|
||||
private _pendingListChangedConfig?: ListChangedHandlers;
|
||||
private _enforceStrictCapabilities: boolean;
|
||||
|
|
@ -244,22 +226,11 @@ export class Client extends Protocol<ClientContext> {
|
|||
private _clientInfo: Implementation,
|
||||
options?: ClientOptions
|
||||
) {
|
||||
super({
|
||||
...options,
|
||||
tasks: extractTaskManagerOptions(options?.capabilities?.tasks)
|
||||
});
|
||||
super(options);
|
||||
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;
|
||||
|
|
@ -299,22 +270,6 @@ export class Client extends Protocol<ClientContext> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Access experimental features.
|
||||
*
|
||||
* WARNING: These APIs are experimental and may change without notice.
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
get experimental(): { tasks: ExperimentalClientTasks } {
|
||||
if (!this._experimental) {
|
||||
this._experimental = {
|
||||
tasks: new ExperimentalClientTasks(this)
|
||||
};
|
||||
}
|
||||
return this._experimental;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers new capabilities. This can only be called before connecting to a transport.
|
||||
*
|
||||
|
|
@ -360,20 +315,6 @@ export class Client extends Protocol<ClientContext> {
|
|||
|
||||
const result = await handler(request, ctx);
|
||||
|
||||
// When task creation is requested, validate and return CreateTaskResult
|
||||
if (params.task) {
|
||||
const taskValidationResult = parseSchema(CreateTaskResultSchema, result);
|
||||
if (!taskValidationResult.success) {
|
||||
const errorMessage =
|
||||
taskValidationResult.error instanceof Error
|
||||
? taskValidationResult.error.message
|
||||
: String(taskValidationResult.error);
|
||||
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Invalid task creation result: ${errorMessage}`);
|
||||
}
|
||||
return taskValidationResult.data;
|
||||
}
|
||||
|
||||
// For non-task requests, validate against ElicitResultSchema
|
||||
const validationResult = parseSchema(ElicitResultSchema, result);
|
||||
if (!validationResult.success) {
|
||||
// Type guard: if success is false, error is guaranteed to exist
|
||||
|
|
@ -416,20 +357,6 @@ export class Client extends Protocol<ClientContext> {
|
|||
|
||||
const result = await handler(request, ctx);
|
||||
|
||||
// When task creation is requested, validate and return CreateTaskResult
|
||||
if (params.task) {
|
||||
const taskValidationResult = parseSchema(CreateTaskResultSchema, result);
|
||||
if (!taskValidationResult.success) {
|
||||
const errorMessage =
|
||||
taskValidationResult.error instanceof Error
|
||||
? taskValidationResult.error.message
|
||||
: String(taskValidationResult.error);
|
||||
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Invalid task creation result: ${errorMessage}`);
|
||||
}
|
||||
return taskValidationResult.data;
|
||||
}
|
||||
|
||||
// For non-task requests, validate against appropriate schema based on tools presence
|
||||
const hasTools = params.tools || params.toolChoice;
|
||||
const resultSchema = hasTools ? CreateMessageResultWithToolsSchema : CreateMessageResultSchema;
|
||||
const validationResult = parseSchema(resultSchema, result);
|
||||
|
|
@ -701,14 +628,6 @@ export class Client extends Protocol<ClientContext> {
|
|||
}
|
||||
}
|
||||
|
||||
protected assertTaskCapability(method: string): void {
|
||||
assertToolsCallTaskCapability(this._serverCapabilities?.tasks?.requests, method, 'Server');
|
||||
}
|
||||
|
||||
protected assertTaskHandlerCapability(method: string): void {
|
||||
assertClientRequestTaskCapability(this._capabilities?.tasks?.requests, method, 'Client');
|
||||
}
|
||||
|
||||
async ping(options?: RequestOptions) {
|
||||
return this._requestWithSchema({ method: 'ping' }, EmptyResultSchema, options);
|
||||
}
|
||||
|
|
@ -828,8 +747,6 @@ export class Client extends Protocol<ClientContext> {
|
|||
* a problem), and thrown {@linkcode ProtocolError} for protocol-level failures or {@linkcode SdkError} for
|
||||
* SDK-level issues (timeouts, missing capabilities).
|
||||
*
|
||||
* For task-based execution with streaming behavior, use {@linkcode ExperimentalClientTasks.callToolStream | client.experimental.tasks.callToolStream()} instead.
|
||||
*
|
||||
* @example Basic usage
|
||||
* ```ts source="./client.examples.ts#Client_callTool_basic"
|
||||
* const result = await client.callTool({
|
||||
|
|
@ -860,14 +777,6 @@ export class Client extends Protocol<ClientContext> {
|
|||
* ```
|
||||
*/
|
||||
async callTool(params: CallToolRequest['params'], options?: RequestOptions) {
|
||||
// Guard: required-task tools need experimental API
|
||||
if (this.isToolTaskRequired(params.name)) {
|
||||
throw new ProtocolError(
|
||||
ProtocolErrorCode.InvalidRequest,
|
||||
`Tool "${params.name}" requires task-based execution. Use client.experimental.tasks.callToolStream() instead.`
|
||||
);
|
||||
}
|
||||
|
||||
const result = await this._requestWithSchema({ method: 'tools/call', params }, CallToolResultSchema, options);
|
||||
|
||||
// Check if the tool has an outputSchema
|
||||
|
|
@ -908,30 +817,12 @@ export class Client extends Protocol<ClientContext> {
|
|||
return result;
|
||||
}
|
||||
|
||||
private isToolTask(toolName: string): boolean {
|
||||
if (!this._serverCapabilities?.tasks?.requests?.tools?.call) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this._cachedKnownTaskTools.has(toolName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a tool requires task-based execution.
|
||||
* Unlike {@linkcode isToolTask} which includes `'optional'` tools, this only checks for `'required'`.
|
||||
*/
|
||||
private isToolTaskRequired(toolName: string): boolean {
|
||||
return this._cachedRequiredTaskTools.has(toolName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache validators for tool output schemas.
|
||||
* Called after {@linkcode listTools | listTools()} to pre-compile validators for better performance.
|
||||
*/
|
||||
private cacheToolMetadata(tools: Tool[]): void {
|
||||
this._cachedToolOutputValidators.clear();
|
||||
this._cachedKnownTaskTools.clear();
|
||||
this._cachedRequiredTaskTools.clear();
|
||||
|
||||
for (const tool of tools) {
|
||||
// If the tool has an outputSchema, create and cache the validator
|
||||
|
|
@ -939,15 +830,6 @@ export class Client extends Protocol<ClientContext> {
|
|||
const toolValidator = this._jsonSchemaValidator.getValidator(tool.outputSchema as JsonSchemaType);
|
||||
this._cachedToolOutputValidators.set(tool.name, toolValidator);
|
||||
}
|
||||
|
||||
// If the tool supports task-based execution, cache that information
|
||||
const taskSupport = tool.execution?.taskSupport;
|
||||
if (taskSupport === 'required' || taskSupport === 'optional') {
|
||||
this._cachedKnownTaskTools.add(tool.name);
|
||||
}
|
||||
if (taskSupport === 'required') {
|
||||
this._cachedRequiredTaskTools.add(tool.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ declare const platformBackgroundTask: {
|
|||
};
|
||||
|
||||
/**
|
||||
* Example: Using a platform background-task API to schedule reconnections.
|
||||
* Example: Using a platform background-scheduler API to schedule reconnections.
|
||||
*/
|
||||
function ReconnectionScheduler_basicUsage() {
|
||||
//#region ReconnectionScheduler_basicUsage
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
/**
|
||||
* Experimental MCP SDK features.
|
||||
* WARNING: These APIs are experimental and may change without notice.
|
||||
*
|
||||
* Import experimental features from this module:
|
||||
* ```typescript
|
||||
* import { TaskStore, InMemoryTaskStore } from '@modelcontextprotocol/sdk/experimental';
|
||||
* ```
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
|
||||
export * from './tasks/client.js';
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
/**
|
||||
* Type-checked examples for `client.ts`.
|
||||
*
|
||||
* These examples are synced into JSDoc comments via the sync-snippets script.
|
||||
* Each function's region markers define the code snippet that appears in the docs.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import type { RequestOptions } from '@modelcontextprotocol/core';
|
||||
|
||||
import type { Client } from '../../client/client.js';
|
||||
|
||||
/**
|
||||
* Example: Using callToolStream to execute a tool with task lifecycle events.
|
||||
*/
|
||||
async function ExperimentalClientTasks_callToolStream(client: Client) {
|
||||
//#region ExperimentalClientTasks_callToolStream
|
||||
const stream = client.experimental.tasks.callToolStream({ name: 'myTool', arguments: {} });
|
||||
for await (const message of stream) {
|
||||
switch (message.type) {
|
||||
case 'taskCreated': {
|
||||
console.log('Tool execution started:', message.task.taskId);
|
||||
break;
|
||||
}
|
||||
case 'taskStatus': {
|
||||
console.log('Tool status:', message.task.status);
|
||||
break;
|
||||
}
|
||||
case 'result': {
|
||||
console.log('Tool result:', message.result);
|
||||
break;
|
||||
}
|
||||
case 'error': {
|
||||
console.error('Tool error:', message.error);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
//#endregion ExperimentalClientTasks_callToolStream
|
||||
}
|
||||
|
||||
/**
|
||||
* Example: Using requestStream to consume task lifecycle events for any request type.
|
||||
*/
|
||||
async function ExperimentalClientTasks_requestStream(client: Client, options: RequestOptions) {
|
||||
//#region ExperimentalClientTasks_requestStream
|
||||
const stream = client.experimental.tasks.requestStream({ method: 'tools/call', params: { name: 'my-tool', arguments: {} } }, options);
|
||||
for await (const message of stream) {
|
||||
switch (message.type) {
|
||||
case 'taskCreated': {
|
||||
console.log('Task created:', message.task.taskId);
|
||||
break;
|
||||
}
|
||||
case 'taskStatus': {
|
||||
console.log('Task status:', message.task.status);
|
||||
break;
|
||||
}
|
||||
case 'result': {
|
||||
console.log('Final result:', message.result);
|
||||
break;
|
||||
}
|
||||
case 'error': {
|
||||
console.error('Error:', message.error);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
//#endregion ExperimentalClientTasks_requestStream
|
||||
}
|
||||
|
|
@ -1,277 +0,0 @@
|
|||
/**
|
||||
* Experimental client task features for MCP SDK.
|
||||
* WARNING: These APIs are experimental and may change without notice.
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
|
||||
import type {
|
||||
AnyObjectSchema,
|
||||
CallToolRequest,
|
||||
CallToolResult,
|
||||
CancelTaskResult,
|
||||
CreateTaskResult,
|
||||
GetTaskPayloadResult,
|
||||
GetTaskResult,
|
||||
ListTasksResult,
|
||||
Request,
|
||||
RequestMethod,
|
||||
RequestOptions,
|
||||
ResponseMessage,
|
||||
ResultTypeMap
|
||||
} from '@modelcontextprotocol/core';
|
||||
import {
|
||||
CallToolResultSchema,
|
||||
getResultSchema,
|
||||
GetTaskPayloadResultSchema,
|
||||
ProtocolError,
|
||||
ProtocolErrorCode
|
||||
} from '@modelcontextprotocol/core';
|
||||
|
||||
import type { Client } from '../../client/client.js';
|
||||
|
||||
/**
|
||||
* Internal interface for accessing {@linkcode Client}'s private methods.
|
||||
* @internal
|
||||
*/
|
||||
interface ClientInternal {
|
||||
isToolTask(toolName: string): boolean;
|
||||
getToolOutputValidator(toolName: string): ((data: unknown) => { valid: boolean; errorMessage?: string }) | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Experimental task features for MCP clients.
|
||||
*
|
||||
* Access via `client.experimental.tasks`:
|
||||
* ```typescript
|
||||
* const stream = client.experimental.tasks.callToolStream({ name: 'tool', arguments: {} });
|
||||
* const task = await client.experimental.tasks.getTask(taskId);
|
||||
* ```
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* This method provides streaming access to tool execution, allowing you to
|
||||
* observe intermediate task status updates for long-running tool calls.
|
||||
* Automatically validates structured output if the tool has an `outputSchema`.
|
||||
*
|
||||
* @example
|
||||
* ```ts source="./client.examples.ts#ExperimentalClientTasks_callToolStream"
|
||||
* const stream = client.experimental.tasks.callToolStream({ name: 'myTool', arguments: {} });
|
||||
* for await (const message of stream) {
|
||||
* switch (message.type) {
|
||||
* case 'taskCreated': {
|
||||
* console.log('Tool execution started:', message.task.taskId);
|
||||
* break;
|
||||
* }
|
||||
* case 'taskStatus': {
|
||||
* console.log('Tool status:', message.task.status);
|
||||
* break;
|
||||
* }
|
||||
* case 'result': {
|
||||
* console.log('Tool result:', message.result);
|
||||
* break;
|
||||
* }
|
||||
* case 'error': {
|
||||
* console.error('Tool error:', message.error);
|
||||
* break;
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param params - Tool call parameters (name and arguments)
|
||||
* @param options - Optional request options (timeout, signal, task creation params, etc.)
|
||||
* @returns AsyncGenerator that yields {@linkcode ResponseMessage} objects
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
async *callToolStream(
|
||||
params: CallToolRequest['params'],
|
||||
options?: RequestOptions
|
||||
): AsyncGenerator<ResponseMessage<CallToolResult | CreateTaskResult>, void, void> {
|
||||
// Access Client's internal methods
|
||||
const clientInternal = this._client as unknown as ClientInternal;
|
||||
|
||||
// Add task creation parameters if server supports it and not explicitly provided
|
||||
const optionsWithTask = {
|
||||
...options,
|
||||
// We check if the tool is known to be a task during auto-configuration, but assume
|
||||
// the caller knows what they're doing if they pass this explicitly
|
||||
task: options?.task ?? (clientInternal.isToolTask(params.name) ? {} : undefined)
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
// Iterate through the stream and validate the final result if needed
|
||||
for await (const message of stream) {
|
||||
// If this is a result message and the tool has an output schema, validate it
|
||||
// Only validate CallToolResult (has 'content'), not CreateTaskResult (has 'task')
|
||||
if (message.type === 'result' && validator && 'content' in message.result) {
|
||||
const result = message.result as CallToolResult;
|
||||
|
||||
// If tool has outputSchema, it MUST return structuredContent (unless it's an error)
|
||||
if (!result.structuredContent && !result.isError) {
|
||||
yield {
|
||||
type: 'error',
|
||||
error: new ProtocolError(
|
||||
ProtocolErrorCode.InvalidRequest,
|
||||
`Tool ${params.name} has an output schema but did not return structured content`
|
||||
)
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
// Only validate structured content if present (not when there's an error)
|
||||
if (result.structuredContent) {
|
||||
try {
|
||||
// Validate the structured content against the schema
|
||||
const validationResult = validator(result.structuredContent);
|
||||
|
||||
if (!validationResult.valid) {
|
||||
yield {
|
||||
type: 'error',
|
||||
error: new ProtocolError(
|
||||
ProtocolErrorCode.InvalidParams,
|
||||
`Structured content does not match the tool's output schema: ${validationResult.errorMessage}`
|
||||
)
|
||||
};
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof ProtocolError) {
|
||||
yield { type: 'error', error };
|
||||
return;
|
||||
}
|
||||
yield {
|
||||
type: 'error',
|
||||
error: new ProtocolError(
|
||||
ProtocolErrorCode.InvalidParams,
|
||||
`Failed to validate structured content: ${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
};
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Yield the message (either validated result or any other message type)
|
||||
yield message;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current status of a task.
|
||||
*
|
||||
* @param taskId - The task identifier
|
||||
* @param options - Optional request options
|
||||
* @returns The task status
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
async getTask(taskId: string, options?: RequestOptions): Promise<GetTaskResult> {
|
||||
return this._module.getTask({ taskId }, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the result of a completed task.
|
||||
*
|
||||
* @param taskId - The task identifier
|
||||
* @param options - Optional request options
|
||||
* @returns The task result. The payload structure matches the result type of the
|
||||
* original request (e.g., a `tools/call` task returns a `CallToolResult`).
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
async getTaskResult(taskId: string, options?: RequestOptions): Promise<GetTaskPayloadResult> {
|
||||
return this._module.getTaskResult({ taskId }, GetTaskPayloadResultSchema, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists tasks with optional pagination.
|
||||
*
|
||||
* @param cursor - Optional pagination cursor
|
||||
* @param options - Optional request options
|
||||
* @returns List of tasks with optional next cursor
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
async listTasks(cursor?: string, options?: RequestOptions): Promise<ListTasksResult> {
|
||||
return this._module.listTasks(cursor ? { cursor } : undefined, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels a running task.
|
||||
*
|
||||
* @param taskId - The task identifier
|
||||
* @param options - Optional request options
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
async cancelTask(taskId: string, options?: RequestOptions): Promise<CancelTaskResult> {
|
||||
return this._module.cancelTask({ taskId }, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request and returns an AsyncGenerator that yields response messages.
|
||||
* The generator is guaranteed to end with either a `'result'` or `'error'` message.
|
||||
*
|
||||
* This method provides streaming access to request processing, allowing you to
|
||||
* observe intermediate task status updates for task-augmented requests.
|
||||
*
|
||||
* @example
|
||||
* ```ts source="./client.examples.ts#ExperimentalClientTasks_requestStream"
|
||||
* const stream = client.experimental.tasks.requestStream({ method: 'tools/call', params: { name: 'my-tool', arguments: {} } }, options);
|
||||
* for await (const message of stream) {
|
||||
* switch (message.type) {
|
||||
* case 'taskCreated': {
|
||||
* console.log('Task created:', message.task.taskId);
|
||||
* break;
|
||||
* }
|
||||
* case 'taskStatus': {
|
||||
* console.log('Task status:', message.task.status);
|
||||
* break;
|
||||
* }
|
||||
* case 'result': {
|
||||
* console.log('Final result:', message.result);
|
||||
* break;
|
||||
* }
|
||||
* case 'error': {
|
||||
* console.error('Error:', message.error);
|
||||
* break;
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param request - The request to send
|
||||
* @param options - Optional request options (timeout, signal, task creation params, etc.)
|
||||
* @returns AsyncGenerator that yields {@linkcode ResponseMessage} objects
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
requestStream<M extends RequestMethod>(
|
||||
request: { method: M; params?: Record<string, unknown> },
|
||||
options?: RequestOptions
|
||||
): AsyncGenerator<ResponseMessage<ResultTypeMap[M]>, void, void> {
|
||||
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
|
||||
>;
|
||||
}
|
||||
}
|
||||
|
|
@ -71,9 +71,6 @@ export type {
|
|||
} from './client/streamableHttp.js';
|
||||
export { StreamableHTTPClientTransport } from './client/streamableHttp.js';
|
||||
|
||||
// experimental exports
|
||||
export { ExperimentalClientTasks } from './experimental/tasks/client.js';
|
||||
|
||||
// runtime-aware wrapper (shadows core/public's fromJsonSchema with optional validator)
|
||||
export { fromJsonSchema } from './fromJsonSchema.js';
|
||||
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@ export const SPEC_SCHEMA_NAMES: ReadonlySet<string> = new Set([
|
|||
'CallToolRequestParamsSchema',
|
||||
'CallToolRequestSchema',
|
||||
'CallToolResultSchema',
|
||||
'CancelTaskRequestSchema',
|
||||
'CancelTaskResultSchema',
|
||||
'CancelledNotificationParamsSchema',
|
||||
'CancelledNotificationSchema',
|
||||
'ClientCapabilitiesSchema',
|
||||
|
|
@ -25,7 +23,6 @@ export const SPEC_SCHEMA_NAMES: ReadonlySet<string> = new Set([
|
|||
'CreateMessageRequestSchema',
|
||||
'CreateMessageResultSchema',
|
||||
'CreateMessageResultWithToolsSchema',
|
||||
'CreateTaskResultSchema',
|
||||
'CursorSchema',
|
||||
'ElicitRequestFormParamsSchema',
|
||||
'ElicitRequestParamsSchema',
|
||||
|
|
@ -40,10 +37,6 @@ export const SPEC_SCHEMA_NAMES: ReadonlySet<string> = new Set([
|
|||
'GetPromptRequestParamsSchema',
|
||||
'GetPromptRequestSchema',
|
||||
'GetPromptResultSchema',
|
||||
'GetTaskPayloadRequestSchema',
|
||||
'GetTaskPayloadResultSchema',
|
||||
'GetTaskRequestSchema',
|
||||
'GetTaskResultSchema',
|
||||
'IconSchema',
|
||||
'IconsSchema',
|
||||
'ImageContentSchema',
|
||||
|
|
@ -70,8 +63,6 @@ export const SPEC_SCHEMA_NAMES: ReadonlySet<string> = new Set([
|
|||
'ListResourcesResultSchema',
|
||||
'ListRootsRequestSchema',
|
||||
'ListRootsResultSchema',
|
||||
'ListTasksRequestSchema',
|
||||
'ListTasksResultSchema',
|
||||
'ListToolsRequestSchema',
|
||||
'ListToolsResultSchema',
|
||||
'LoggingLevelSchema',
|
||||
|
|
@ -110,7 +101,6 @@ export const SPEC_SCHEMA_NAMES: ReadonlySet<string> = new Set([
|
|||
'ReadResourceRequestParamsSchema',
|
||||
'ReadResourceRequestSchema',
|
||||
'ReadResourceResultSchema',
|
||||
'RelatedTaskMetadataSchema',
|
||||
'RequestIdSchema',
|
||||
'RequestMetaSchema',
|
||||
'RequestSchema',
|
||||
|
|
@ -140,13 +130,6 @@ export const SPEC_SCHEMA_NAMES: ReadonlySet<string> = new Set([
|
|||
'StringSchemaSchema',
|
||||
'SubscribeRequestParamsSchema',
|
||||
'SubscribeRequestSchema',
|
||||
'TaskAugmentedRequestParamsSchema',
|
||||
'TaskCreationParamsSchema',
|
||||
'TaskMetadataSchema',
|
||||
'TaskSchema',
|
||||
'TaskStatusNotificationParamsSchema',
|
||||
'TaskStatusNotificationSchema',
|
||||
'TaskStatusSchema',
|
||||
'TextContentSchema',
|
||||
'TextResourceContentsSchema',
|
||||
'TitledMultiSelectEnumSchemaSchema',
|
||||
|
|
|
|||
|
|
@ -13,10 +13,7 @@ export const CONTEXT_PROPERTY_MAP: ContextMapping[] = [
|
|||
{ from: '.sessionId', to: '.sessionId' },
|
||||
{ from: '.requestInfo', to: '.http?.req' },
|
||||
{ from: '.closeSSEStream', to: '.http?.closeSSE' },
|
||||
{ from: '.closeStandaloneSSEStream', to: '.http?.closeStandaloneSSE' },
|
||||
{ from: '.taskStore', to: '.task?.store' },
|
||||
{ from: '.taskId', to: '.task?.id' },
|
||||
{ from: '.taskRequestedTtl', to: '.task?.requestedTtl' }
|
||||
{ from: '.closeStandaloneSSEStream', to: '.http?.closeStandaloneSSE' }
|
||||
];
|
||||
|
||||
export const EXTRA_PARAM_NAME = 'extra';
|
||||
|
|
|
|||
|
|
@ -152,12 +152,14 @@ export const IMPORT_MAP: Record<string, ImportMapping> = {
|
|||
},
|
||||
|
||||
'@modelcontextprotocol/sdk/experimental/tasks': {
|
||||
target: '@modelcontextprotocol/server',
|
||||
status: 'moved'
|
||||
target: '',
|
||||
status: 'removed',
|
||||
removalMessage: 'Experimental tasks removed in v2 (SEP-2663 — tasks moved to the Extensions Track). No v2 equivalent.'
|
||||
},
|
||||
'@modelcontextprotocol/sdk/experimental/tasks.js': {
|
||||
target: '@modelcontextprotocol/server',
|
||||
status: 'moved'
|
||||
target: '',
|
||||
status: 'removed',
|
||||
removalMessage: 'Experimental tasks removed in v2 (SEP-2663 — tasks moved to the Extensions Track). No v2 equivalent.'
|
||||
},
|
||||
|
||||
'@modelcontextprotocol/sdk/inMemory.js': {
|
||||
|
|
|
|||
|
|
@ -14,11 +14,7 @@ export const SCHEMA_TO_METHOD: Record<string, string> = {
|
|||
SetLevelRequestSchema: 'logging/setLevel',
|
||||
PingRequestSchema: 'ping',
|
||||
CompleteRequestSchema: 'completion/complete',
|
||||
ListRootsRequestSchema: 'roots/list',
|
||||
ListTasksRequestSchema: 'tasks/list',
|
||||
GetTaskRequestSchema: 'tasks/get',
|
||||
GetTaskPayloadRequestSchema: 'tasks/result',
|
||||
CancelTaskRequestSchema: 'tasks/cancel'
|
||||
ListRootsRequestSchema: 'roots/list'
|
||||
};
|
||||
|
||||
export const NOTIFICATION_SCHEMA_TO_METHOD: Record<string, string> = {
|
||||
|
|
@ -31,6 +27,5 @@ export const NOTIFICATION_SCHEMA_TO_METHOD: Record<string, string> = {
|
|||
CancelledNotificationSchema: 'notifications/cancelled',
|
||||
InitializedNotificationSchema: 'notifications/initialized',
|
||||
RootsListChangedNotificationSchema: 'notifications/roots/list_changed',
|
||||
TaskStatusNotificationSchema: 'notifications/tasks/status',
|
||||
ElicitationCompleteNotificationSchema: 'notifications/elicitation/complete'
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { CONTEXT_PROPERTY_MAP, CTX_PARAM_NAME, EXTRA_PARAM_NAME } from '../mappi
|
|||
|
||||
const HANDLER_METHODS = new Set(['setRequestHandler', 'setNotificationHandler']);
|
||||
|
||||
const REGISTER_METHODS = new Set(['registerTool', 'registerPrompt', 'registerResource', 'registerToolTask', 'tool', 'prompt', 'resource']);
|
||||
const REGISTER_METHODS = new Set(['registerTool', 'registerPrompt', 'registerResource', 'tool', 'prompt', 'resource']);
|
||||
|
||||
/**
|
||||
* Attempt to rename the second parameter of a callback from 'extra' to 'ctx'
|
||||
|
|
@ -219,7 +219,7 @@ export const contextTypesTransform: Transform = {
|
|||
|
||||
if (!callbackArg) continue;
|
||||
|
||||
// Handle ObjectLiteralExpression for registerToolTask-style callbacks
|
||||
// Handle ObjectLiteralExpression callback containers (handler maps)
|
||||
if (Node.isObjectLiteralExpression(callbackArg)) {
|
||||
for (const prop of callbackArg.getProperties()) {
|
||||
let callbackNode: Node | undefined;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { Node, SyntaxKind } from 'ts-morph';
|
|||
|
||||
import type { Diagnostic, Transform, TransformContext, TransformResult } from '../../../types.js';
|
||||
import { info, warning } from '../../../utils/diagnostics.js';
|
||||
import { isOriginalNameImportedFromMcp, resolveLocalImportName } from '../../../utils/importUtils.js';
|
||||
import { isOriginalNameImportedFromMcp } from '../../../utils/importUtils.js';
|
||||
|
||||
export const mcpServerApiTransform: Transform = {
|
||||
name: 'McpServer API migration',
|
||||
|
|
@ -121,8 +121,6 @@ export const mcpServerApiTransform: Transform = {
|
|||
}
|
||||
}
|
||||
|
||||
changesCount += migrateConstructorTaskOptions(sourceFile, diagnostics);
|
||||
|
||||
return { changesCount, diagnostics };
|
||||
}
|
||||
};
|
||||
|
|
@ -394,130 +392,3 @@ function migrateResourceCall(call: CallExpression, _sourceFile: SourceFile): boo
|
|||
|
||||
return true;
|
||||
}
|
||||
|
||||
const TASK_OPTIONS = ['taskStore', 'taskMessageQueue'] as const;
|
||||
|
||||
function migrateConstructorTaskOptions(sourceFile: SourceFile, diagnostics: Diagnostic[]): number {
|
||||
const localName = resolveLocalImportName(sourceFile, 'McpServer');
|
||||
if (!localName) return 0;
|
||||
|
||||
let changes = 0;
|
||||
|
||||
for (const node of sourceFile.getDescendantsOfKind(SyntaxKind.NewExpression)) {
|
||||
if (node.wasForgotten()) continue;
|
||||
const expr = node.getExpression();
|
||||
if (!Node.isIdentifier(expr) || expr.getText() !== localName) continue;
|
||||
|
||||
const args = node.getArguments();
|
||||
if (args.length < 2) continue;
|
||||
|
||||
const optionsArg = args[1]!;
|
||||
if (!Node.isObjectLiteralExpression(optionsArg)) continue;
|
||||
|
||||
// Check if any task options are present at the top level
|
||||
const propsToMove: string[] = [];
|
||||
for (const propName of TASK_OPTIONS) {
|
||||
if (optionsArg.getProperty(propName)) {
|
||||
propsToMove.push(propName);
|
||||
}
|
||||
}
|
||||
if (propsToMove.length === 0) continue;
|
||||
|
||||
// Find the tasks object's position within the options text using AST,
|
||||
// then do all mutations via a single text replacement to avoid node invalidation.
|
||||
const capabilitiesProp = optionsArg.getProperty('capabilities');
|
||||
let tasksObjStart = -1;
|
||||
let tasksObjEnd = -1;
|
||||
const optionsStart = optionsArg.getStart();
|
||||
if (capabilitiesProp && Node.isPropertyAssignment(capabilitiesProp)) {
|
||||
const capInit = capabilitiesProp.getInitializer();
|
||||
if (capInit && Node.isObjectLiteralExpression(capInit)) {
|
||||
const tasksProp = capInit.getProperty('tasks');
|
||||
if (tasksProp && Node.isPropertyAssignment(tasksProp)) {
|
||||
const tasksInit = tasksProp.getInitializer();
|
||||
if (tasksInit && Node.isObjectLiteralExpression(tasksInit)) {
|
||||
tasksObjStart = tasksInit.getStart() - optionsStart;
|
||||
tasksObjEnd = tasksInit.getEnd() - optionsStart;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (tasksObjStart === -1) {
|
||||
for (const propName of propsToMove) {
|
||||
diagnostics.push(
|
||||
warning(
|
||||
sourceFile.getFilePath(),
|
||||
node.getStartLineNumber(),
|
||||
`Move '${propName}' from McpServer options into capabilities.tasks — v2 expects task runtime options inside the tasks capability.`
|
||||
)
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Single text replacement: remove top-level props and insert into tasks object.
|
||||
// Use AST nodes (already located via getProperty) to get brace-balanced text and
|
||||
// exact positions, avoiding regex truncation on values containing commas/braces.
|
||||
// Collect all properties first, then process in reverse position order so each
|
||||
// removal doesn't invalidate the positions of subsequent removals.
|
||||
let optionsText = optionsArg.getText();
|
||||
const argStart = optionsArg.getStart();
|
||||
const propsWithPositions: { text: string; start: number; end: number }[] = [];
|
||||
for (const propName of propsToMove) {
|
||||
const prop = optionsArg.getProperty(propName);
|
||||
if (!prop) continue;
|
||||
propsWithPositions.push({
|
||||
text: prop.getText(),
|
||||
start: prop.getStart() - argStart,
|
||||
end: prop.getEnd() - argStart
|
||||
});
|
||||
}
|
||||
const propTexts = propsWithPositions.map(p => p.text);
|
||||
|
||||
// Remove in reverse position order so earlier positions remain valid
|
||||
const sortedProps = propsWithPositions.toSorted((a, b) => b.start - a.start);
|
||||
for (const { start, end } of sortedProps) {
|
||||
let remStart = start;
|
||||
let remEnd = end;
|
||||
// Consume trailing comma and whitespace
|
||||
const afterProp = optionsText.slice(remEnd);
|
||||
const trailingMatch = afterProp.match(/^\s*,?\s*/);
|
||||
if (trailingMatch) {
|
||||
remEnd += trailingMatch[0].length;
|
||||
}
|
||||
// Consume leading whitespace/newline
|
||||
const beforeProp = optionsText.slice(0, remStart);
|
||||
const leadingMatch = beforeProp.match(/[\n\r]?\s*$/);
|
||||
if (leadingMatch) {
|
||||
remStart -= leadingMatch[0].length;
|
||||
}
|
||||
optionsText = optionsText.slice(0, remStart) + optionsText.slice(remEnd);
|
||||
// Adjust tasks position if removal was before it
|
||||
if (remStart < tasksObjStart) {
|
||||
const shift = remEnd - remStart;
|
||||
tasksObjStart -= shift;
|
||||
tasksObjEnd -= shift;
|
||||
}
|
||||
}
|
||||
|
||||
if (propTexts.length === 0) continue;
|
||||
|
||||
// Insert into the tasks object (just before its closing brace)
|
||||
const tasksText = optionsText.slice(tasksObjStart, tasksObjEnd);
|
||||
const closingBrace = tasksText.lastIndexOf('}');
|
||||
const before = tasksText.slice(0, closingBrace).trimEnd();
|
||||
const sep = before.length > 1 ? ',\n' : '\n';
|
||||
const newTasksText = before + sep + propTexts.join(',\n') + '\n' + tasksText.slice(closingBrace);
|
||||
optionsText = optionsText.slice(0, tasksObjStart) + newTasksText + optionsText.slice(tasksObjEnd);
|
||||
|
||||
// Clean up double/trailing commas
|
||||
optionsText = optionsText.replaceAll(/,(\s*,)/g, ',');
|
||||
optionsText = optionsText.replaceAll(/,(\s*})/g, '$1');
|
||||
|
||||
optionsArg.replaceWithText(optionsText);
|
||||
changes += propTexts.length;
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,18 +85,6 @@ describe('context-types transform', () => {
|
|||
expect(result).toContain('ctx.http?.authInfo');
|
||||
});
|
||||
|
||||
it('rewrites extra.taskStore to ctx.task?.store', () => {
|
||||
const input = [
|
||||
`server.setRequestHandler('tools/call', async (request, extra) => {`,
|
||||
` const store = extra.taskStore;`,
|
||||
` return { content: [] };`,
|
||||
`});`,
|
||||
''
|
||||
].join('\n');
|
||||
const result = applyTransform(input);
|
||||
expect(result).toContain('ctx.task?.store');
|
||||
});
|
||||
|
||||
it('does not touch non-extra parameters', () => {
|
||||
const input = [
|
||||
`server.setRequestHandler('tools/call', async (request, context) => {`,
|
||||
|
|
@ -304,53 +292,6 @@ describe('context-types transform', () => {
|
|||
expect(sourceFile.getFullText()).toContain('extra');
|
||||
});
|
||||
|
||||
it('renames extra to ctx and remaps properties in registerToolTask handler object', () => {
|
||||
const input = [
|
||||
`server.experimental.tasks.registerToolTask('my-task', { schema: {} }, {`,
|
||||
` createTask: async (args, extra) => {`,
|
||||
` const s = extra.signal;`,
|
||||
` const store = extra.taskStore;`,
|
||||
` return { content: [] };`,
|
||||
` },`,
|
||||
` getTask: async (args, extra) => {`,
|
||||
` const auth = extra.authInfo;`,
|
||||
` return { content: [] };`,
|
||||
` },`,
|
||||
` getTaskResult: async (args, extra) => {`,
|
||||
` await extra.sendNotification({ method: 'test', params: {} });`,
|
||||
` return { content: [] };`,
|
||||
` },`,
|
||||
`});`,
|
||||
''
|
||||
].join('\n');
|
||||
const result = applyTransform(input);
|
||||
expect(result).not.toContain('extra');
|
||||
expect(result).toContain('(args, ctx)');
|
||||
expect(result).toContain('ctx.mcpReq.signal');
|
||||
expect(result).toContain('ctx.task?.store');
|
||||
expect(result).toContain('ctx.http?.authInfo');
|
||||
expect(result).toContain('ctx.mcpReq.notify(');
|
||||
});
|
||||
|
||||
it('handles registerToolTask with shorthand method syntax', () => {
|
||||
const input = [
|
||||
`server.registerToolTask('my-task', {}, {`,
|
||||
` async createTask(args, extra) {`,
|
||||
` const s = extra.signal;`,
|
||||
` return { content: [] };`,
|
||||
` },`,
|
||||
` async getTask(args, extra) {`,
|
||||
` return { content: [] };`,
|
||||
` },`,
|
||||
`});`,
|
||||
''
|
||||
].join('\n');
|
||||
const result = applyTransform(input);
|
||||
expect(result).not.toContain('extra');
|
||||
expect(result).toContain('(args, ctx)');
|
||||
expect(result).toContain('ctx.mcpReq.signal');
|
||||
});
|
||||
|
||||
it('does not rename "extra" inside string literals', () => {
|
||||
const input = [
|
||||
`server.setRequestHandler('tools/call', async (request, extra) => {`,
|
||||
|
|
|
|||
|
|
@ -192,69 +192,6 @@ describe('handler-registration transform', () => {
|
|||
expect(result.diagnostics[0]!.message).toContain('CustomNotification');
|
||||
});
|
||||
|
||||
it('replaces ListTasksRequestSchema with method string', () => {
|
||||
const input = [
|
||||
`import { ListTasksRequestSchema } from '@modelcontextprotocol/sdk/types.js';`,
|
||||
`client.setRequestHandler(ListTasksRequestSchema, async (request) => {`,
|
||||
` return { tasks: [] };`,
|
||||
`});`,
|
||||
''
|
||||
].join('\n');
|
||||
const result = applyTransform(input);
|
||||
expect(result).toContain("setRequestHandler('tasks/list'");
|
||||
expect(result).not.toContain('ListTasksRequestSchema');
|
||||
});
|
||||
|
||||
it('replaces GetTaskRequestSchema with method string', () => {
|
||||
const input = [
|
||||
`import { GetTaskRequestSchema } from '@modelcontextprotocol/sdk/types.js';`,
|
||||
`client.setRequestHandler(GetTaskRequestSchema, async (request) => {`,
|
||||
` return { taskId: '1', status: 'completed' };`,
|
||||
`});`,
|
||||
''
|
||||
].join('\n');
|
||||
const result = applyTransform(input);
|
||||
expect(result).toContain("setRequestHandler('tasks/get'");
|
||||
expect(result).not.toContain('GetTaskRequestSchema');
|
||||
});
|
||||
|
||||
it('replaces CancelTaskRequestSchema with method string', () => {
|
||||
const input = [
|
||||
`import { CancelTaskRequestSchema } from '@modelcontextprotocol/sdk/types.js';`,
|
||||
`client.setRequestHandler(CancelTaskRequestSchema, async (request) => {`,
|
||||
` return {};`,
|
||||
`});`,
|
||||
''
|
||||
].join('\n');
|
||||
const result = applyTransform(input);
|
||||
expect(result).toContain("setRequestHandler('tasks/cancel'");
|
||||
expect(result).not.toContain('CancelTaskRequestSchema');
|
||||
});
|
||||
|
||||
it('replaces GetTaskPayloadRequestSchema with method string', () => {
|
||||
const input = [
|
||||
`import { GetTaskPayloadRequestSchema } from '@modelcontextprotocol/sdk/types.js';`,
|
||||
`client.setRequestHandler(GetTaskPayloadRequestSchema, async (request) => {`,
|
||||
` return { content: [] };`,
|
||||
`});`,
|
||||
''
|
||||
].join('\n');
|
||||
const result = applyTransform(input);
|
||||
expect(result).toContain("setRequestHandler('tasks/result'");
|
||||
expect(result).not.toContain('GetTaskPayloadRequestSchema');
|
||||
});
|
||||
|
||||
it('replaces TaskStatusNotificationSchema with method string', () => {
|
||||
const input = [
|
||||
`import { TaskStatusNotificationSchema } from '@modelcontextprotocol/sdk/types.js';`,
|
||||
`client.setNotificationHandler(TaskStatusNotificationSchema, async () => {});`,
|
||||
''
|
||||
].join('\n');
|
||||
const result = applyTransform(input);
|
||||
expect(result).toContain("setNotificationHandler('notifications/tasks/status'");
|
||||
expect(result).not.toContain('TaskStatusNotificationSchema');
|
||||
});
|
||||
|
||||
it('replaces ElicitationCompleteNotificationSchema with method string', () => {
|
||||
const input = [
|
||||
`import { ElicitationCompleteNotificationSchema } from '@modelcontextprotocol/sdk/types.js';`,
|
||||
|
|
|
|||
|
|
@ -307,77 +307,4 @@ describe('mcp-server-api transform', () => {
|
|||
expect(result).toContain('registerTool("ping", {}');
|
||||
expect(result).not.toContain('z.object');
|
||||
});
|
||||
|
||||
it('moves taskStore from top-level options into capabilities.tasks', () => {
|
||||
const input = `const server = new McpServer({ name: "test", version: "1.0" }, { taskStore, capabilities: { tasks: { list: {} } } });\n`;
|
||||
const project = new Project({ useInMemoryFileSystem: true });
|
||||
const sourceFile = project.createSourceFile('test.ts', MCP_IMPORT + input);
|
||||
const result = mcpServerApiTransform.apply(sourceFile, ctx);
|
||||
const text = sourceFile.getFullText();
|
||||
expect(text).not.toMatch(/,\s*taskStore\s*,\s*capabilities/);
|
||||
expect(text).toMatch(/tasks:\s*\{[\s\S]*taskStore/);
|
||||
expect(result.changesCount).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('moves taskMessageQueue from top-level options into capabilities.tasks', () => {
|
||||
const input = `const server = new McpServer({ name: "test", version: "1.0" }, { taskMessageQueue, capabilities: { tasks: { list: {} } } });\n`;
|
||||
const project = new Project({ useInMemoryFileSystem: true });
|
||||
const sourceFile = project.createSourceFile('test.ts', MCP_IMPORT + input);
|
||||
const result = mcpServerApiTransform.apply(sourceFile, ctx);
|
||||
const text = sourceFile.getFullText();
|
||||
expect(text).not.toMatch(/,\s*taskMessageQueue\s*,\s*capabilities/);
|
||||
expect(text).toMatch(/tasks:\s*\{[\s\S]*taskMessageQueue/);
|
||||
expect(result.changesCount).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('moves both taskStore and taskMessageQueue into capabilities.tasks', () => {
|
||||
const input = `const server = new McpServer({ name: "test", version: "1.0" }, { taskStore, taskMessageQueue, capabilities: { tasks: {} } });\n`;
|
||||
const project = new Project({ useInMemoryFileSystem: true });
|
||||
const sourceFile = project.createSourceFile('test.ts', MCP_IMPORT + input);
|
||||
mcpServerApiTransform.apply(sourceFile, ctx);
|
||||
const text = sourceFile.getFullText();
|
||||
expect(text).toMatch(/tasks:\s*\{[\s\S]*taskStore/);
|
||||
expect(text).toMatch(/tasks:\s*\{[\s\S]*taskMessageQueue/);
|
||||
});
|
||||
|
||||
it('moves both task props with complex values without corrupting output', () => {
|
||||
const input = `const server = new McpServer({ name: "test", version: "1.0" }, { taskStore: createStore(a, b), taskMessageQueue: new Queue({ size: 10 }), capabilities: { tasks: {} } });\n`;
|
||||
const project = new Project({ useInMemoryFileSystem: true });
|
||||
const sourceFile = project.createSourceFile('test.ts', MCP_IMPORT + input);
|
||||
mcpServerApiTransform.apply(sourceFile, ctx);
|
||||
const text = sourceFile.getFullText();
|
||||
expect(text).toMatch(/tasks:\s*\{[\s\S]*taskStore:\s*createStore\(a, b\)/);
|
||||
expect(text).toMatch(/tasks:\s*\{[\s\S]*taskMessageQueue:\s*new Queue\(\{\s*size:\s*10\s*\}\)/);
|
||||
expect(text).toContain('capabilities');
|
||||
});
|
||||
|
||||
it('emits warning when capabilities.tasks object is missing', () => {
|
||||
const input = `const server = new McpServer({ name: "test", version: "1.0" }, { taskStore, capabilities: {} });\n`;
|
||||
const project = new Project({ useInMemoryFileSystem: true });
|
||||
const sourceFile = project.createSourceFile('test.ts', MCP_IMPORT + input);
|
||||
const result = mcpServerApiTransform.apply(sourceFile, ctx);
|
||||
const taskWarnings = result.diagnostics.filter(d => d.message.includes('taskStore'));
|
||||
expect(taskWarnings.length).toBe(1);
|
||||
expect(taskWarnings[0]!.message).toContain('capabilities.tasks');
|
||||
});
|
||||
|
||||
it('does not touch constructor without taskStore or taskMessageQueue', () => {
|
||||
const input = `const server = new McpServer({ name: "test", version: "1.0" }, { capabilities: {} });\n`;
|
||||
const project = new Project({ useInMemoryFileSystem: true });
|
||||
const sourceFile = project.createSourceFile('test.ts', MCP_IMPORT + input);
|
||||
const result = mcpServerApiTransform.apply(sourceFile, ctx);
|
||||
const taskWarnings = result.diagnostics.filter(d => d.message.includes('taskStore') || d.message.includes('taskMessageQueue'));
|
||||
expect(taskWarnings.length).toBe(0);
|
||||
});
|
||||
|
||||
it('moves taskStore with complex value expression (nested braces/commas)', () => {
|
||||
const input = `const server = new McpServer({ name: "test", version: "1.0" }, { taskStore: new InMemoryTaskStore({ ttl: 5000 }), capabilities: { tasks: { list: {} } } });\n`;
|
||||
const project = new Project({ useInMemoryFileSystem: true });
|
||||
const sourceFile = project.createSourceFile('test.ts', MCP_IMPORT + input);
|
||||
const result = mcpServerApiTransform.apply(sourceFile, ctx);
|
||||
const text = sourceFile.getFullText();
|
||||
expect(text).toMatch(/tasks:\s*\{[\s\S]*taskStore:\s*new InMemoryTaskStore\(\{\s*ttl:\s*5000\s*\}\)/);
|
||||
expect(text).not.toMatch(/,\s*taskStore.*capabilities/);
|
||||
expect(result.changesCount).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
export * from './tasks/helpers.js';
|
||||
export * from './tasks/interfaces.js';
|
||||
export * from './tasks/stores/inMemory.js';
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
/**
|
||||
* Experimental task capability assertion helpers.
|
||||
* WARNING: These APIs are experimental and may change without notice.
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
|
||||
import { SdkError, SdkErrorCode } from '../../errors/sdkErrors.js';
|
||||
|
||||
/**
|
||||
* Type representing the task requests capability structure.
|
||||
* This is derived from `ClientTasksCapability.requests` and `ServerTasksCapability.requests`.
|
||||
*/
|
||||
interface TaskRequestsCapability {
|
||||
tools?: { call?: object };
|
||||
sampling?: { createMessage?: object };
|
||||
elicitation?: { create?: object };
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that task creation is supported for `tools/call`.
|
||||
* Used to implement the `assertTaskCapability` or `assertTaskHandlerCapability` abstract methods on Protocol.
|
||||
*
|
||||
* @param requests - The task requests capability object
|
||||
* @param method - The method being checked
|
||||
* @param entityName - `'Server'` or `'Client'` for error messages
|
||||
* @throws {@linkcode SdkError} with {@linkcode SdkErrorCode.CapabilityNotSupported} if the capability is not supported
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
export function assertToolsCallTaskCapability(
|
||||
requests: TaskRequestsCapability | undefined,
|
||||
method: string,
|
||||
entityName: 'Server' | 'Client'
|
||||
): void {
|
||||
if (!requests) {
|
||||
throw new SdkError(SdkErrorCode.CapabilityNotSupported, `${entityName} does not support task creation (required for ${method})`);
|
||||
}
|
||||
|
||||
switch (method) {
|
||||
case 'tools/call': {
|
||||
if (!requests.tools?.call) {
|
||||
throw new SdkError(
|
||||
SdkErrorCode.CapabilityNotSupported,
|
||||
`${entityName} does not support task creation for tools/call (required for ${method})`
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
// Method doesn't support tasks, which is fine - no error
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that task creation is supported for `sampling/createMessage` or `elicitation/create`.
|
||||
* Used to implement the `assertTaskCapability` or `assertTaskHandlerCapability` abstract methods on Protocol.
|
||||
*
|
||||
* @param requests - The task requests capability object
|
||||
* @param method - The method being checked
|
||||
* @param entityName - `'Server'` or `'Client'` for error messages
|
||||
* @throws {@linkcode SdkError} with {@linkcode SdkErrorCode.CapabilityNotSupported} if the capability is not supported
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
export function assertClientRequestTaskCapability(
|
||||
requests: TaskRequestsCapability | undefined,
|
||||
method: string,
|
||||
entityName: 'Server' | 'Client'
|
||||
): void {
|
||||
if (!requests) {
|
||||
throw new SdkError(SdkErrorCode.CapabilityNotSupported, `${entityName} does not support task creation (required for ${method})`);
|
||||
}
|
||||
|
||||
switch (method) {
|
||||
case 'sampling/createMessage': {
|
||||
if (!requests.sampling?.createMessage) {
|
||||
throw new SdkError(
|
||||
SdkErrorCode.CapabilityNotSupported,
|
||||
`${entityName} does not support task creation for sampling/createMessage (required for ${method})`
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'elicitation/create': {
|
||||
if (!requests.elicitation?.create) {
|
||||
throw new SdkError(
|
||||
SdkErrorCode.CapabilityNotSupported,
|
||||
`${entityName} does not support task creation for elicitation/create (required for ${method})`
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
// Method doesn't support tasks, which is fine - no error
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,243 +0,0 @@
|
|||
/**
|
||||
* Experimental task interfaces for MCP SDK.
|
||||
* WARNING: These APIs are experimental and may change without notice.
|
||||
*/
|
||||
|
||||
import type { ServerContext } from '../../shared/protocol.js';
|
||||
import type { RequestTaskStore } from '../../shared/taskManager.js';
|
||||
import type {
|
||||
JSONRPCErrorResponse,
|
||||
JSONRPCNotification,
|
||||
JSONRPCRequest,
|
||||
JSONRPCResultResponse,
|
||||
Request,
|
||||
RequestId,
|
||||
Result,
|
||||
Task,
|
||||
ToolExecution
|
||||
} from '../../types/index.js';
|
||||
|
||||
// ============================================================================
|
||||
// Task Handler Types (for registerToolTask)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Server context with guaranteed task store for task creation.
|
||||
* @experimental
|
||||
*/
|
||||
export type CreateTaskServerContext = ServerContext & {
|
||||
task: { store: RequestTaskStore; requestedTtl?: number };
|
||||
};
|
||||
|
||||
/**
|
||||
* Server context with guaranteed task ID and store for task operations.
|
||||
* @experimental
|
||||
*/
|
||||
export type TaskServerContext = ServerContext & {
|
||||
task: { id: string; store: RequestTaskStore; requestedTtl?: number };
|
||||
};
|
||||
|
||||
/**
|
||||
* Task-specific execution configuration.
|
||||
* `taskSupport` cannot be `'forbidden'` for task-based tools.
|
||||
* @experimental
|
||||
*/
|
||||
export type TaskToolExecution<TaskSupport = ToolExecution['taskSupport']> = Omit<ToolExecution, 'taskSupport'> & {
|
||||
taskSupport: TaskSupport extends 'forbidden' | undefined ? never : TaskSupport;
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents a message queued for side-channel delivery via tasks/result.
|
||||
*
|
||||
* This is a serializable data structure that can be stored in external systems.
|
||||
* All fields are JSON-serializable.
|
||||
*/
|
||||
export type QueuedMessage = QueuedRequest | QueuedNotification | QueuedResponse | QueuedError;
|
||||
|
||||
export interface BaseQueuedMessage {
|
||||
/** Type of message */
|
||||
type: string;
|
||||
/** When the message was queued (milliseconds since epoch) */
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export interface QueuedRequest extends BaseQueuedMessage {
|
||||
type: 'request';
|
||||
/** The actual JSONRPC request */
|
||||
message: JSONRPCRequest;
|
||||
}
|
||||
|
||||
export interface QueuedNotification extends BaseQueuedMessage {
|
||||
type: 'notification';
|
||||
/** The actual JSONRPC notification */
|
||||
message: JSONRPCNotification;
|
||||
}
|
||||
|
||||
export interface QueuedResponse extends BaseQueuedMessage {
|
||||
type: 'response';
|
||||
/** The actual JSONRPC response */
|
||||
message: JSONRPCResultResponse;
|
||||
}
|
||||
|
||||
export interface QueuedError extends BaseQueuedMessage {
|
||||
type: 'error';
|
||||
/** The actual JSONRPC error */
|
||||
message: JSONRPCErrorResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for managing per-task FIFO message queues.
|
||||
*
|
||||
* Similar to {@linkcode TaskStore}, this allows pluggable queue implementations
|
||||
* (in-memory, Redis, other distributed queues, etc.).
|
||||
*
|
||||
* Each method accepts taskId and optional sessionId parameters to enable
|
||||
* a single queue instance to manage messages for multiple tasks, with
|
||||
* isolation based on task ID and session ID.
|
||||
*
|
||||
* All methods are async to support external storage implementations.
|
||||
* All data in {@linkcode QueuedMessage} must be JSON-serializable.
|
||||
*
|
||||
* @see {@linkcode InMemoryTaskMessageQueue} for a reference implementation
|
||||
* @experimental
|
||||
*/
|
||||
export interface TaskMessageQueue {
|
||||
/**
|
||||
* Adds a message to the end of the queue for a specific task.
|
||||
* Atomically checks queue size and throws if maxSize would be exceeded.
|
||||
* @param taskId The task identifier
|
||||
* @param message The message to enqueue
|
||||
* @param sessionId Optional session ID for binding the operation to a specific session
|
||||
* @param maxSize Optional maximum queue size - if specified and queue is full, throws an error
|
||||
* @throws Error if maxSize is specified and would be exceeded
|
||||
*/
|
||||
enqueue(taskId: string, message: QueuedMessage, sessionId?: string, maxSize?: number): Promise<void>;
|
||||
|
||||
/**
|
||||
* Removes and returns the first message from the queue for a specific task.
|
||||
* @param taskId The task identifier
|
||||
* @param sessionId Optional session ID for binding the query to a specific session
|
||||
* @returns The first message, or `undefined` if the queue is empty
|
||||
*/
|
||||
dequeue(taskId: string, sessionId?: string): Promise<QueuedMessage | undefined>;
|
||||
|
||||
/**
|
||||
* Removes and returns all messages from the queue for a specific task.
|
||||
* Used when tasks are cancelled or failed to clean up pending messages.
|
||||
* @param taskId The task identifier
|
||||
* @param sessionId Optional session ID for binding the query to a specific session
|
||||
* @returns Array of all messages that were in the queue
|
||||
*/
|
||||
dequeueAll(taskId: string, sessionId?: string): Promise<QueuedMessage[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Task creation options.
|
||||
* @experimental
|
||||
*/
|
||||
export interface CreateTaskOptions {
|
||||
/**
|
||||
* Duration in milliseconds to retain task from creation.
|
||||
* If `null`, the task has unlimited lifetime until manually cleaned up.
|
||||
*/
|
||||
ttl?: number | null;
|
||||
|
||||
/**
|
||||
* Time in milliseconds to wait between task status requests.
|
||||
*/
|
||||
pollInterval?: number;
|
||||
|
||||
/**
|
||||
* Additional context to pass to the task store.
|
||||
*/
|
||||
context?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for storing and retrieving task state and results.
|
||||
*
|
||||
* Similar to {@linkcode Transport}, this allows pluggable task storage implementations
|
||||
* (in-memory, database, distributed cache, etc.).
|
||||
*
|
||||
* @see {@linkcode InMemoryTaskStore} for a reference implementation
|
||||
* @experimental
|
||||
*/
|
||||
export interface TaskStore {
|
||||
/**
|
||||
* Creates a new task with the given creation parameters and original request.
|
||||
* The implementation must generate a unique taskId and createdAt timestamp.
|
||||
*
|
||||
* TTL Management:
|
||||
* - The implementation receives the TTL suggested by the requestor via `taskParams.ttl`
|
||||
* - The implementation MAY override the requested TTL (e.g., to enforce limits)
|
||||
* - The actual TTL used MUST be returned in the {@linkcode Task} object
|
||||
* - `null` TTL indicates unlimited task lifetime (no automatic cleanup)
|
||||
* - Cleanup SHOULD occur automatically after TTL expires, regardless of task status
|
||||
*
|
||||
* @param taskParams - The task creation parameters from the request (ttl, pollInterval)
|
||||
* @param requestId - The JSON-RPC request ID
|
||||
* @param request - The original request that triggered task creation
|
||||
* @param sessionId - Optional session ID for binding the task to a specific session
|
||||
* @returns The created {@linkcode Task} object
|
||||
*/
|
||||
createTask(taskParams: CreateTaskOptions, requestId: RequestId, request: Request, sessionId?: string): Promise<Task>;
|
||||
|
||||
/**
|
||||
* Gets the current status of a task.
|
||||
*
|
||||
* @param taskId - The task identifier
|
||||
* @param sessionId - Optional session ID for binding the query to a specific session
|
||||
* @returns The {@linkcode Task} object, or `null` if it does not exist
|
||||
*/
|
||||
getTask(taskId: string, sessionId?: string): Promise<Task | null>;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param sessionId - Optional session ID for binding the operation to a specific session
|
||||
*/
|
||||
storeTaskResult(taskId: string, status: 'completed' | 'failed', result: Result, sessionId?: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Retrieves the stored result of a task.
|
||||
*
|
||||
* @param taskId - The task identifier
|
||||
* @param sessionId - Optional session ID for binding the query to a specific session
|
||||
* @returns The stored result
|
||||
*/
|
||||
getTaskResult(taskId: string, sessionId?: 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
|
||||
* @param sessionId - Optional session ID for binding the operation to a specific session
|
||||
*/
|
||||
updateTaskStatus(taskId: string, status: Task['status'], statusMessage?: string, sessionId?: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Lists tasks, optionally starting from a pagination cursor.
|
||||
*
|
||||
* @param cursor - Optional cursor for pagination
|
||||
* @param sessionId - Optional session ID for binding the query to a specific session
|
||||
* @returns An object containing the tasks array and an optional nextCursor
|
||||
*/
|
||||
listTasks(cursor?: string, sessionId?: string): Promise<{ tasks: Task[]; nextCursor?: string }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a task status represents a terminal state.
|
||||
* Terminal states are those where the task has finished and will not change.
|
||||
*
|
||||
* @param status - The task status to check
|
||||
* @returns `true` if the status is terminal (`completed`, `failed`, or `cancelled`)
|
||||
* @experimental
|
||||
*/
|
||||
export function isTerminal(status: Task['status']): boolean {
|
||||
return status === 'completed' || status === 'failed' || status === 'cancelled';
|
||||
}
|
||||
|
|
@ -1,313 +0,0 @@
|
|||
/**
|
||||
* In-memory implementations of {@linkcode TaskStore} and {@linkcode TaskMessageQueue}.
|
||||
* @experimental
|
||||
*/
|
||||
|
||||
import type { Request, RequestId, Result, Task } from '../../../types/index.js';
|
||||
import type { CreateTaskOptions, QueuedMessage, TaskMessageQueue, TaskStore } from '../interfaces.js';
|
||||
import { isTerminal } from '../interfaces.js';
|
||||
|
||||
interface StoredTask {
|
||||
task: Task;
|
||||
request: Request;
|
||||
requestId: RequestId;
|
||||
sessionId?: string;
|
||||
result?: Result;
|
||||
}
|
||||
|
||||
/**
|
||||
* In-memory {@linkcode TaskStore} implementation for development and testing.
|
||||
* For production, use a database or distributed cache.
|
||||
* @experimental
|
||||
*/
|
||||
export class InMemoryTaskStore implements TaskStore {
|
||||
private tasks = new Map<string, StoredTask>();
|
||||
private cleanupTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
||||
|
||||
/**
|
||||
* Generates a unique task ID using Web Crypto API.
|
||||
*/
|
||||
private generateTaskId(): string {
|
||||
return crypto.randomUUID().replaceAll('-', '');
|
||||
}
|
||||
|
||||
/** {@inheritDoc TaskStore.createTask} */
|
||||
async createTask(taskParams: CreateTaskOptions, requestId: RequestId, request: Request, sessionId?: string): Promise<Task> {
|
||||
// Generate a unique task ID
|
||||
const taskId = this.generateTaskId();
|
||||
|
||||
// Ensure uniqueness
|
||||
if (this.tasks.has(taskId)) {
|
||||
throw new Error(`Task with ID ${taskId} already exists`);
|
||||
}
|
||||
|
||||
const actualTtl = taskParams.ttl ?? null;
|
||||
|
||||
// Create task with generated ID and timestamps
|
||||
const createdAt = new Date().toISOString();
|
||||
const task: Task = {
|
||||
taskId,
|
||||
status: 'working',
|
||||
ttl: actualTtl,
|
||||
createdAt,
|
||||
lastUpdatedAt: createdAt,
|
||||
pollInterval: taskParams.pollInterval ?? 1000
|
||||
};
|
||||
|
||||
this.tasks.set(taskId, {
|
||||
task,
|
||||
request,
|
||||
requestId,
|
||||
sessionId
|
||||
});
|
||||
|
||||
// Schedule cleanup if ttl is specified
|
||||
// Cleanup occurs regardless of task status
|
||||
if (actualTtl) {
|
||||
const timer = setTimeout(() => {
|
||||
this.tasks.delete(taskId);
|
||||
this.cleanupTimers.delete(taskId);
|
||||
}, actualTtl);
|
||||
|
||||
this.cleanupTimers.set(taskId, timer);
|
||||
}
|
||||
|
||||
return task;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a stored task, enforcing session ownership when a sessionId is provided.
|
||||
* Returns undefined if the task does not exist or belongs to a different session.
|
||||
*/
|
||||
private getStoredTask(taskId: string, sessionId?: string): StoredTask | undefined {
|
||||
const stored = this.tasks.get(taskId);
|
||||
if (!stored) {
|
||||
return undefined;
|
||||
}
|
||||
// Enforce session isolation: if a sessionId is provided and the task
|
||||
// was created with a sessionId, they must match.
|
||||
if (sessionId !== undefined && stored.sessionId !== undefined && stored.sessionId !== sessionId) {
|
||||
return undefined;
|
||||
}
|
||||
return stored;
|
||||
}
|
||||
|
||||
async getTask(taskId: string, sessionId?: string): Promise<Task | null> {
|
||||
const stored = this.getStoredTask(taskId, sessionId);
|
||||
return stored ? { ...stored.task } : null;
|
||||
}
|
||||
|
||||
/** {@inheritDoc TaskStore.storeTaskResult} */
|
||||
async storeTaskResult(taskId: string, status: 'completed' | 'failed', result: Result, sessionId?: string): Promise<void> {
|
||||
const stored = this.getStoredTask(taskId, sessionId);
|
||||
if (!stored) {
|
||||
throw new Error(`Task with ID ${taskId} not found`);
|
||||
}
|
||||
|
||||
// Don't allow storing results for tasks already in terminal state
|
||||
if (isTerminal(stored.task.status)) {
|
||||
throw new Error(
|
||||
`Cannot store result for task ${taskId} in terminal status '${stored.task.status}'. Task results can only be stored once.`
|
||||
);
|
||||
}
|
||||
|
||||
stored.result = result;
|
||||
stored.task.status = status;
|
||||
stored.task.lastUpdatedAt = new Date().toISOString();
|
||||
|
||||
// Reset cleanup timer to start from now (if ttl is set)
|
||||
if (stored.task.ttl) {
|
||||
const existingTimer = this.cleanupTimers.get(taskId);
|
||||
if (existingTimer) {
|
||||
clearTimeout(existingTimer);
|
||||
}
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
this.tasks.delete(taskId);
|
||||
this.cleanupTimers.delete(taskId);
|
||||
}, stored.task.ttl);
|
||||
|
||||
this.cleanupTimers.set(taskId, timer);
|
||||
}
|
||||
}
|
||||
|
||||
/** {@inheritDoc TaskStore.getTaskResult} */
|
||||
async getTaskResult(taskId: string, sessionId?: string): Promise<Result> {
|
||||
const stored = this.getStoredTask(taskId, sessionId);
|
||||
if (!stored) {
|
||||
throw new Error(`Task with ID ${taskId} not found`);
|
||||
}
|
||||
|
||||
if (!stored.result) {
|
||||
throw new Error(`Task ${taskId} has no result stored`);
|
||||
}
|
||||
|
||||
return stored.result;
|
||||
}
|
||||
|
||||
/** {@inheritDoc TaskStore.updateTaskStatus} */
|
||||
async updateTaskStatus(taskId: string, status: Task['status'], statusMessage?: string, sessionId?: string): Promise<void> {
|
||||
const stored = this.getStoredTask(taskId, sessionId);
|
||||
if (!stored) {
|
||||
throw new Error(`Task with ID ${taskId} not found`);
|
||||
}
|
||||
|
||||
// Don't allow transitions from terminal states
|
||||
if (isTerminal(stored.task.status)) {
|
||||
throw new Error(
|
||||
`Cannot update task ${taskId} from terminal status '${stored.task.status}' to '${status}'. Terminal states (completed, failed, cancelled) cannot transition to other states.`
|
||||
);
|
||||
}
|
||||
|
||||
stored.task.status = status;
|
||||
if (statusMessage) {
|
||||
stored.task.statusMessage = statusMessage;
|
||||
}
|
||||
|
||||
stored.task.lastUpdatedAt = new Date().toISOString();
|
||||
|
||||
// If task is in a terminal state and has ttl, start cleanup timer
|
||||
if (isTerminal(status) && stored.task.ttl) {
|
||||
const existingTimer = this.cleanupTimers.get(taskId);
|
||||
if (existingTimer) {
|
||||
clearTimeout(existingTimer);
|
||||
}
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
this.tasks.delete(taskId);
|
||||
this.cleanupTimers.delete(taskId);
|
||||
}, stored.task.ttl);
|
||||
|
||||
this.cleanupTimers.set(taskId, timer);
|
||||
}
|
||||
}
|
||||
|
||||
/** {@inheritDoc TaskStore.listTasks} */
|
||||
async listTasks(cursor?: string, sessionId?: string): Promise<{ tasks: Task[]; nextCursor?: string }> {
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
// Filter tasks by session ownership before pagination
|
||||
const filteredTaskIds = [...this.tasks.entries()]
|
||||
.filter(([, stored]) => {
|
||||
if (sessionId === undefined || stored.sessionId === undefined) {
|
||||
return true;
|
||||
}
|
||||
return stored.sessionId === sessionId;
|
||||
})
|
||||
.map(([taskId]) => taskId);
|
||||
|
||||
let startIndex = 0;
|
||||
if (cursor) {
|
||||
const cursorIndex = filteredTaskIds.indexOf(cursor);
|
||||
if (cursorIndex === -1) {
|
||||
// Invalid cursor - throw error
|
||||
throw new Error(`Invalid cursor: ${cursor}`);
|
||||
} else {
|
||||
startIndex = cursorIndex + 1;
|
||||
}
|
||||
}
|
||||
|
||||
const pageTaskIds = filteredTaskIds.slice(startIndex, startIndex + PAGE_SIZE);
|
||||
const tasks = pageTaskIds.map(taskId => {
|
||||
const stored = this.tasks.get(taskId)!;
|
||||
return { ...stored.task };
|
||||
});
|
||||
|
||||
const nextCursor = startIndex + PAGE_SIZE < filteredTaskIds.length ? pageTaskIds.at(-1) : undefined;
|
||||
|
||||
return { tasks, nextCursor };
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup all timers (useful for testing or graceful shutdown)
|
||||
*/
|
||||
cleanup(): void {
|
||||
for (const timer of this.cleanupTimers.values()) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
this.cleanupTimers.clear();
|
||||
this.tasks.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all tasks (useful for debugging)
|
||||
*/
|
||||
getAllTasks(): Task[] {
|
||||
return [...this.tasks.values()].map(stored => ({ ...stored.task }));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* In-memory {@linkcode TaskMessageQueue} implementation for development and testing.
|
||||
* For production, use Redis or another distributed queue.
|
||||
* @experimental
|
||||
*/
|
||||
export class InMemoryTaskMessageQueue implements TaskMessageQueue {
|
||||
private queues = new Map<string, QueuedMessage[]>();
|
||||
|
||||
/**
|
||||
* Generates a queue key from taskId.
|
||||
* SessionId is intentionally ignored because taskIds are globally unique
|
||||
* and tasks need to be accessible across HTTP requests/sessions.
|
||||
*/
|
||||
private getQueueKey(taskId: string, _sessionId?: string): string {
|
||||
return taskId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets or creates a queue for the given task and session.
|
||||
*/
|
||||
private getQueue(taskId: string, sessionId?: string): QueuedMessage[] {
|
||||
const key = this.getQueueKey(taskId, sessionId);
|
||||
let queue = this.queues.get(key);
|
||||
if (!queue) {
|
||||
queue = [];
|
||||
this.queues.set(key, queue);
|
||||
}
|
||||
return queue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a message to the end of the queue for a specific task.
|
||||
* Atomically checks queue size and throws if maxSize would be exceeded.
|
||||
* @param taskId The task identifier
|
||||
* @param message The message to enqueue
|
||||
* @param sessionId Optional session ID for binding the operation to a specific session
|
||||
* @param maxSize Optional maximum queue size - if specified and queue is full, throws an error
|
||||
* @throws Error if maxSize is specified and would be exceeded
|
||||
*/
|
||||
async enqueue(taskId: string, message: QueuedMessage, sessionId?: string, maxSize?: number): Promise<void> {
|
||||
const queue = this.getQueue(taskId, sessionId);
|
||||
|
||||
// Atomically check size and enqueue
|
||||
if (maxSize !== undefined && queue.length >= maxSize) {
|
||||
throw new Error(`Task message queue overflow: queue size (${queue.length}) exceeds maximum (${maxSize})`);
|
||||
}
|
||||
|
||||
queue.push(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes and returns the first message from the queue for a specific task.
|
||||
* @param taskId The task identifier
|
||||
* @param sessionId Optional session ID for binding the query to a specific session
|
||||
* @returns The first message, or `undefined` if the queue is empty
|
||||
*/
|
||||
async dequeue(taskId: string, sessionId?: string): Promise<QueuedMessage | undefined> {
|
||||
const queue = this.getQueue(taskId, sessionId);
|
||||
return queue.shift();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes and returns all messages from the queue for a specific task.
|
||||
* @param taskId The task identifier
|
||||
* @param sessionId Optional session ID for binding the query to a specific session
|
||||
* @returns Array of all messages that were in the queue
|
||||
*/
|
||||
async dequeueAll(taskId: string, sessionId?: string): Promise<QueuedMessage[]> {
|
||||
const key = this.getQueueKey(taskId, sessionId);
|
||||
const queue = this.queues.get(key) ?? [];
|
||||
this.queues.delete(key);
|
||||
return queue;
|
||||
}
|
||||
}
|
||||
|
|
@ -52,20 +52,6 @@ export type {
|
|||
} from '../../shared/protocol.js';
|
||||
export { DEFAULT_REQUEST_TIMEOUT_MSEC } from '../../shared/protocol.js';
|
||||
|
||||
// Task manager types (NOT TaskManager class itself — internal)
|
||||
export type { RequestTaskStore, TaskContext, TaskManagerOptions, TaskRequestOptions } from '../../shared/taskManager.js';
|
||||
|
||||
// Response message types
|
||||
export type {
|
||||
BaseResponseMessage,
|
||||
ErrorMessage,
|
||||
ResponseMessage,
|
||||
ResultMessage,
|
||||
TaskCreatedMessage,
|
||||
TaskStatusMessage
|
||||
} from '../../shared/responseMessage.js';
|
||||
export { takeResult, toArrayAsync } from '../../shared/responseMessage.js';
|
||||
|
||||
// stdio message framing utilities (for custom transport authors)
|
||||
export { deserializeMessage, ReadBuffer, serializeMessage } from '../../shared/stdio.js';
|
||||
|
||||
|
|
@ -93,7 +79,6 @@ export {
|
|||
LATEST_PROTOCOL_VERSION,
|
||||
METHOD_NOT_FOUND,
|
||||
PARSE_ERROR,
|
||||
RELATED_TASK_META_KEY,
|
||||
SUPPORTED_PROTOCOL_VERSIONS
|
||||
} from '../../types/constants.js';
|
||||
|
||||
|
|
@ -115,29 +100,9 @@ export {
|
|||
isJSONRPCRequest,
|
||||
isJSONRPCResponse,
|
||||
isJSONRPCResultResponse,
|
||||
isTaskAugmentedRequestParams,
|
||||
parseJSONRPCMessage
|
||||
} from '../../types/guards.js';
|
||||
|
||||
// Experimental task types and classes
|
||||
export { assertClientRequestTaskCapability, assertToolsCallTaskCapability } from '../../experimental/tasks/helpers.js';
|
||||
export type {
|
||||
BaseQueuedMessage,
|
||||
CreateTaskOptions,
|
||||
CreateTaskServerContext,
|
||||
QueuedError,
|
||||
QueuedMessage,
|
||||
QueuedNotification,
|
||||
QueuedRequest,
|
||||
QueuedResponse,
|
||||
TaskMessageQueue,
|
||||
TaskServerContext,
|
||||
TaskStore,
|
||||
TaskToolExecution
|
||||
} from '../../experimental/tasks/interfaces.js';
|
||||
export { isTerminal } from '../../experimental/tasks/interfaces.js';
|
||||
export { InMemoryTaskMessageQueue, InMemoryTaskStore } from '../../experimental/tasks/stores/inMemory.js';
|
||||
|
||||
// Validator types and classes
|
||||
export type { SpecTypeName, SpecTypes } from '../../types/specTypeSchema.js';
|
||||
export { isSpecType, specTypeSchemas } from '../../types/specTypeSchema.js';
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue