v2 refactor!: remove Zod schema parameter from public-facing APIs (#1606)
This commit is contained in:
parent
1a78b0138f
commit
f88c376c52
8 changed files with 1483 additions and 1708 deletions
|
|
@ -7,7 +7,6 @@ import type {
|
|||
ClientNotification,
|
||||
ClientRequest,
|
||||
ClientResult,
|
||||
CompatibilityCallToolResultSchema,
|
||||
CompleteRequest,
|
||||
GetPromptRequest,
|
||||
Implementation,
|
||||
|
|
@ -476,7 +475,7 @@ export class Client extends Protocol<ClientContext> {
|
|||
return;
|
||||
}
|
||||
try {
|
||||
const result = await this.request(
|
||||
const result = await this._requestWithSchema(
|
||||
{
|
||||
method: 'initialize',
|
||||
params: {
|
||||
|
|
@ -709,22 +708,22 @@ export class Client extends Protocol<ClientContext> {
|
|||
|
||||
/** Sends a ping to the server to check connectivity. */
|
||||
async ping(options?: RequestOptions) {
|
||||
return this.request({ method: 'ping' }, EmptyResultSchema, options);
|
||||
return this._requestWithSchema({ method: 'ping' }, EmptyResultSchema, options);
|
||||
}
|
||||
|
||||
/** Requests argument autocompletion suggestions from the server for a prompt or resource. */
|
||||
async complete(params: CompleteRequest['params'], options?: RequestOptions) {
|
||||
return this.request({ method: 'completion/complete', params }, CompleteResultSchema, options);
|
||||
return this._requestWithSchema({ method: 'completion/complete', params }, CompleteResultSchema, options);
|
||||
}
|
||||
|
||||
/** Sets the minimum severity level for log messages sent by the server. */
|
||||
async setLoggingLevel(level: LoggingLevel, options?: RequestOptions) {
|
||||
return this.request({ method: 'logging/setLevel', params: { level } }, EmptyResultSchema, options);
|
||||
return this._requestWithSchema({ method: 'logging/setLevel', params: { level } }, EmptyResultSchema, options);
|
||||
}
|
||||
|
||||
/** Retrieves a prompt by name from the server, passing the given arguments for template substitution. */
|
||||
async getPrompt(params: GetPromptRequest['params'], options?: RequestOptions) {
|
||||
return this.request({ method: 'prompts/get', params }, GetPromptResultSchema, options);
|
||||
return this._requestWithSchema({ method: 'prompts/get', params }, GetPromptResultSchema, options);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -754,7 +753,7 @@ export class Client extends Protocol<ClientContext> {
|
|||
console.debug('Client.listPrompts() called but server does not advertise prompts capability - returning empty list');
|
||||
return { prompts: [] };
|
||||
}
|
||||
return this.request({ method: 'prompts/list', params }, ListPromptsResultSchema, options);
|
||||
return this._requestWithSchema({ method: 'prompts/list', params }, ListPromptsResultSchema, options);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -784,7 +783,7 @@ export class Client extends Protocol<ClientContext> {
|
|||
console.debug('Client.listResources() called but server does not advertise resources capability - returning empty list');
|
||||
return { resources: [] };
|
||||
}
|
||||
return this.request({ method: 'resources/list', params }, ListResourcesResultSchema, options);
|
||||
return this._requestWithSchema({ method: 'resources/list', params }, ListResourcesResultSchema, options);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -801,22 +800,22 @@ export class Client extends Protocol<ClientContext> {
|
|||
);
|
||||
return { resourceTemplates: [] };
|
||||
}
|
||||
return this.request({ method: 'resources/templates/list', params }, ListResourceTemplatesResultSchema, options);
|
||||
return this._requestWithSchema({ method: 'resources/templates/list', params }, ListResourceTemplatesResultSchema, options);
|
||||
}
|
||||
|
||||
/** Reads the contents of a resource by URI. */
|
||||
async readResource(params: ReadResourceRequest['params'], options?: RequestOptions) {
|
||||
return this.request({ method: 'resources/read', params }, ReadResourceResultSchema, options);
|
||||
return this._requestWithSchema({ method: 'resources/read', params }, ReadResourceResultSchema, options);
|
||||
}
|
||||
|
||||
/** Subscribes to change notifications for a resource. The server must support resource subscriptions. */
|
||||
async subscribeResource(params: SubscribeRequest['params'], options?: RequestOptions) {
|
||||
return this.request({ method: 'resources/subscribe', params }, EmptyResultSchema, options);
|
||||
return this._requestWithSchema({ method: 'resources/subscribe', params }, EmptyResultSchema, options);
|
||||
}
|
||||
|
||||
/** Unsubscribes from change notifications for a resource. */
|
||||
async unsubscribeResource(params: UnsubscribeRequest['params'], options?: RequestOptions) {
|
||||
return this.request({ method: 'resources/unsubscribe', params }, EmptyResultSchema, options);
|
||||
return this._requestWithSchema({ method: 'resources/unsubscribe', params }, EmptyResultSchema, options);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -858,11 +857,7 @@ export class Client extends Protocol<ClientContext> {
|
|||
* }
|
||||
* ```
|
||||
*/
|
||||
async callTool(
|
||||
params: CallToolRequest['params'],
|
||||
resultSchema: typeof CallToolResultSchema | typeof CompatibilityCallToolResultSchema = CallToolResultSchema,
|
||||
options?: RequestOptions
|
||||
) {
|
||||
async callTool(params: CallToolRequest['params'], options?: RequestOptions) {
|
||||
// Guard: required-task tools need experimental API
|
||||
if (this.isToolTaskRequired(params.name)) {
|
||||
throw new ProtocolError(
|
||||
|
|
@ -871,7 +866,7 @@ export class Client extends Protocol<ClientContext> {
|
|||
);
|
||||
}
|
||||
|
||||
const result = await this.request({ method: 'tools/call', params }, resultSchema, options);
|
||||
const result = await this._requestWithSchema({ method: 'tools/call', params }, CallToolResultSchema, options);
|
||||
|
||||
// Check if the tool has an outputSchema
|
||||
const validator = this.getToolOutputValidator(params.name);
|
||||
|
|
@ -988,7 +983,7 @@ export class Client extends Protocol<ClientContext> {
|
|||
console.debug('Client.listTools() called but server does not advertise tools capability - returning empty list');
|
||||
return { tools: [] };
|
||||
}
|
||||
const result = await this.request({ method: 'tools/list', params }, ListToolsResultSchema, options);
|
||||
const result = await this._requestWithSchema({ method: 'tools/list', params }, ListToolsResultSchema, options);
|
||||
|
||||
// Cache the tools and their output schemas for future validation
|
||||
this.cacheToolMetadata(result.tools);
|
||||
|
|
|
|||
|
|
@ -7,8 +7,7 @@
|
|||
* @module
|
||||
*/
|
||||
|
||||
import type { Request, RequestOptions } from '@modelcontextprotocol/core';
|
||||
import { CallToolResultSchema } from '@modelcontextprotocol/core';
|
||||
import type { RequestOptions } from '@modelcontextprotocol/core';
|
||||
|
||||
import type { Client } from '../../client/client.js';
|
||||
|
||||
|
|
@ -44,9 +43,9 @@ async function ExperimentalClientTasks_callToolStream(client: Client) {
|
|||
/**
|
||||
* Example: Using requestStream to consume task lifecycle events for any request type.
|
||||
*/
|
||||
async function ExperimentalClientTasks_requestStream(client: Client, request: Request, options: RequestOptions) {
|
||||
async function ExperimentalClientTasks_requestStream(client: Client, options: RequestOptions) {
|
||||
//#region ExperimentalClientTasks_requestStream
|
||||
const stream = client.experimental.tasks.requestStream(request, CallToolResultSchema, options);
|
||||
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': {
|
||||
|
|
|
|||
|
|
@ -8,15 +8,18 @@
|
|||
import type {
|
||||
AnyObjectSchema,
|
||||
CallToolRequest,
|
||||
CallToolResult,
|
||||
CancelTaskResult,
|
||||
CreateTaskResult,
|
||||
GetTaskResult,
|
||||
ListTasksResult,
|
||||
Request,
|
||||
RequestMethod,
|
||||
RequestOptions,
|
||||
ResponseMessage,
|
||||
ResultTypeMap,
|
||||
SchemaOutput
|
||||
} from '@modelcontextprotocol/core';
|
||||
import { CallToolResultSchema, ProtocolError, ProtocolErrorCode } from '@modelcontextprotocol/core';
|
||||
import { ProtocolError, ProtocolErrorCode } from '@modelcontextprotocol/core';
|
||||
|
||||
import type { Client } from '../../client/client.js';
|
||||
|
||||
|
|
@ -25,11 +28,10 @@ import type { Client } from '../../client/client.js';
|
|||
* @internal
|
||||
*/
|
||||
interface ClientInternal {
|
||||
requestStream<T extends AnyObjectSchema>(
|
||||
request: Request,
|
||||
resultSchema: T,
|
||||
requestStream<M extends RequestMethod>(
|
||||
request: { method: M; params?: Record<string, unknown> },
|
||||
options?: RequestOptions
|
||||
): AsyncGenerator<ResponseMessage<SchemaOutput<T>>, void, void>;
|
||||
): AsyncGenerator<ResponseMessage<ResultTypeMap[M]>, void, void>;
|
||||
isToolTask(toolName: string): boolean;
|
||||
getToolOutputValidator(toolName: string): ((data: unknown) => { valid: boolean; errorMessage?: string }) | undefined;
|
||||
}
|
||||
|
|
@ -90,7 +92,7 @@ export class ExperimentalClientTasks {
|
|||
async *callToolStream(
|
||||
params: CallToolRequest['params'],
|
||||
options?: RequestOptions
|
||||
): AsyncGenerator<ResponseMessage<SchemaOutput<typeof CallToolResultSchema>>, void, void> {
|
||||
): AsyncGenerator<ResponseMessage<CallToolResult | CreateTaskResult>, void, void> {
|
||||
// Access Client's internal methods
|
||||
const clientInternal = this._client as unknown as ClientInternal;
|
||||
|
||||
|
|
@ -102,7 +104,7 @@ export class ExperimentalClientTasks {
|
|||
task: options?.task ?? (clientInternal.isToolTask(params.name) ? {} : undefined)
|
||||
};
|
||||
|
||||
const stream = clientInternal.requestStream({ method: 'tools/call', params }, CallToolResultSchema, optionsWithTask);
|
||||
const stream = clientInternal.requestStream({ method: 'tools/call', params }, optionsWithTask);
|
||||
|
||||
// Get the validator for this tool (if it has an output schema)
|
||||
const validator = clientInternal.getToolOutputValidator(params.name);
|
||||
|
|
@ -110,8 +112,9 @@ export class ExperimentalClientTasks {
|
|||
// 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
|
||||
if (message.type === 'result' && validator) {
|
||||
const result = message.result;
|
||||
// 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) {
|
||||
|
|
@ -245,7 +248,7 @@ export class ExperimentalClientTasks {
|
|||
*
|
||||
* @example
|
||||
* ```ts source="./client.examples.ts#ExperimentalClientTasks_requestStream"
|
||||
* const stream = client.experimental.tasks.requestStream(request, CallToolResultSchema, options);
|
||||
* 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': {
|
||||
|
|
@ -269,25 +272,16 @@ export class ExperimentalClientTasks {
|
|||
* ```
|
||||
*
|
||||
* @param request - The request to send
|
||||
* @param resultSchema - Zod schema for validating the result
|
||||
* @param options - Optional request options (timeout, signal, task creation params, etc.)
|
||||
* @returns AsyncGenerator that yields {@linkcode ResponseMessage} objects
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
requestStream<T extends AnyObjectSchema>(
|
||||
request: Request,
|
||||
resultSchema: T,
|
||||
requestStream<M extends RequestMethod>(
|
||||
request: { method: M; params?: Record<string, unknown> },
|
||||
options?: RequestOptions
|
||||
): AsyncGenerator<ResponseMessage<SchemaOutput<T>>, void, void> {
|
||||
): AsyncGenerator<ResponseMessage<ResultTypeMap[M]>, void, void> {
|
||||
// Delegate to the client's underlying Protocol method
|
||||
type ClientWithRequestStream = {
|
||||
requestStream<U extends AnyObjectSchema>(
|
||||
request: Request,
|
||||
resultSchema: U,
|
||||
options?: RequestOptions
|
||||
): AsyncGenerator<ResponseMessage<SchemaOutput<U>>, void, void>;
|
||||
};
|
||||
return (this._client as unknown as ClientWithRequestStream).requestStream(request, resultSchema, options);
|
||||
return (this._client as unknown as ClientInternal).requestStream(request, options);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import type {
|
|||
RequestMethod,
|
||||
RequestTypeMap,
|
||||
Result,
|
||||
ResultTypeMap,
|
||||
ServerCapabilities,
|
||||
Task,
|
||||
TaskCreationParams,
|
||||
|
|
@ -44,6 +45,7 @@ import {
|
|||
CreateTaskResultSchema,
|
||||
getNotificationSchema,
|
||||
getRequestSchema,
|
||||
getResultSchema,
|
||||
GetTaskResultSchema,
|
||||
isJSONRPCErrorResponse,
|
||||
isJSONRPCNotification,
|
||||
|
|
@ -297,7 +299,10 @@ export type BaseContext = {
|
|||
*
|
||||
* This is used by certain transports to correctly associate related messages.
|
||||
*/
|
||||
send: <U extends AnySchema>(request: Request, resultSchema: U, options?: TaskRequestOptions) => Promise<SchemaOutput<U>>;
|
||||
send: <M extends RequestMethod>(
|
||||
request: { method: M; params?: Record<string, unknown> },
|
||||
options?: TaskRequestOptions
|
||||
) => Promise<ResultTypeMap[M]>;
|
||||
|
||||
/**
|
||||
* Sends a notification that relates to the current request being handled.
|
||||
|
|
@ -808,7 +813,7 @@ export abstract class Protocol<ContextT extends BaseContext> {
|
|||
method: request.method,
|
||||
_meta: request.params?._meta,
|
||||
signal: abortController.signal,
|
||||
send: async (r, resultSchema, options?) => {
|
||||
send: async (r, options?) => {
|
||||
const requestOptions: RequestOptions = { ...options, relatedRequestId: request.id };
|
||||
if (relatedTaskId && !requestOptions.relatedTask) {
|
||||
requestOptions.relatedTask = { taskId: relatedTaskId };
|
||||
|
|
@ -817,7 +822,7 @@ export abstract class Protocol<ContextT extends BaseContext> {
|
|||
if (effectiveTaskId && taskStore) {
|
||||
await taskStore.updateTaskStatus(effectiveTaskId, 'input_required');
|
||||
}
|
||||
return await this.request(r, resultSchema, requestOptions);
|
||||
return await this.request(r, requestOptions);
|
||||
},
|
||||
notify: async notification => {
|
||||
const notificationOptions: NotificationOptions = { relatedRequestId: request.id };
|
||||
|
|
@ -1030,12 +1035,32 @@ export abstract class Protocol<ContextT extends BaseContext> {
|
|||
protected abstract assertTaskHandlerCapability(method: string): void;
|
||||
|
||||
/**
|
||||
* Sends a request and returns an AsyncGenerator that yields response messages.
|
||||
* Sends a request and returns an AsyncGenerator that yields response messages,
|
||||
* resolving the result schema automatically from the method name.
|
||||
* The generator is guaranteed to end with either a `'result'` or `'error'` message.
|
||||
*
|
||||
* @experimental Use `client.experimental.tasks.requestStream()` to access this method.
|
||||
*/
|
||||
protected async *requestStream<T extends AnyObjectSchema>(
|
||||
protected async *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;
|
||||
yield* this._requestStreamWithSchema(request as Request, resultSchema, options) as AsyncGenerator<
|
||||
ResponseMessage<ResultTypeMap[M]>,
|
||||
void,
|
||||
void
|
||||
>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request and returns an AsyncGenerator that yields response messages,
|
||||
* using the provided schema for validation.
|
||||
*
|
||||
* This is the internal implementation used by SDK methods that need to specify
|
||||
* a particular result schema.
|
||||
*/
|
||||
protected async *_requestStreamWithSchema<T extends AnyObjectSchema>(
|
||||
request: Request,
|
||||
resultSchema: T,
|
||||
options?: RequestOptions
|
||||
|
|
@ -1045,7 +1070,7 @@ export abstract class Protocol<ContextT extends BaseContext> {
|
|||
// For non-task requests, just yield the result
|
||||
if (!task) {
|
||||
try {
|
||||
const result = await this.request(request, resultSchema, options);
|
||||
const result = await this._requestWithSchema(request, resultSchema, options);
|
||||
yield { type: 'result', result };
|
||||
} catch (error) {
|
||||
yield {
|
||||
|
|
@ -1061,7 +1086,7 @@ export abstract class Protocol<ContextT extends BaseContext> {
|
|||
let taskId: string | undefined;
|
||||
try {
|
||||
// Send the request and get the CreateTaskResult
|
||||
const createResult = await this.request(request, CreateTaskResultSchema, options);
|
||||
const createResult = await this._requestWithSchema(request, CreateTaskResultSchema, options);
|
||||
|
||||
// Extract taskId from the result
|
||||
if (createResult.task) {
|
||||
|
|
@ -1132,11 +1157,30 @@ export abstract class Protocol<ContextT extends BaseContext> {
|
|||
}
|
||||
|
||||
/**
|
||||
* Sends a request and waits for a response.
|
||||
* Sends a request and waits for a response, resolving the result schema
|
||||
* automatically from the method name.
|
||||
*
|
||||
* Do not use this method to emit notifications! Use {@linkcode Protocol.notification | notification()} instead.
|
||||
*/
|
||||
request<T extends AnySchema>(request: Request, resultSchema: T, options?: RequestOptions): Promise<SchemaOutput<T>> {
|
||||
request<M extends RequestMethod>(
|
||||
request: { method: M; params?: Record<string, unknown> },
|
||||
options?: RequestOptions
|
||||
): Promise<ResultTypeMap[M]> {
|
||||
const resultSchema = getResultSchema(request.method);
|
||||
return this._requestWithSchema(request as Request, resultSchema, options) as Promise<ResultTypeMap[M]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request and waits for a response, using the provided schema for validation.
|
||||
*
|
||||
* This is the internal implementation used by SDK methods that need to specify
|
||||
* a particular result schema (e.g., for compatibility or task-specific schemas).
|
||||
*/
|
||||
protected _requestWithSchema<T extends AnySchema>(
|
||||
request: Request,
|
||||
resultSchema: T,
|
||||
options?: RequestOptions
|
||||
): Promise<SchemaOutput<T>> {
|
||||
const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
|
||||
|
||||
// Send the request
|
||||
|
|
@ -1299,7 +1343,7 @@ export abstract class Protocol<ContextT extends BaseContext> {
|
|||
* @experimental Use `client.experimental.tasks.getTask()` to access this method.
|
||||
*/
|
||||
protected async getTask(params: GetTaskRequest['params'], options?: RequestOptions): Promise<GetTaskResult> {
|
||||
return this.request({ method: 'tasks/get', params }, GetTaskResultSchema, options);
|
||||
return this._requestWithSchema({ method: 'tasks/get', params }, GetTaskResultSchema, options);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1312,7 +1356,7 @@ export abstract class Protocol<ContextT extends BaseContext> {
|
|||
resultSchema: T,
|
||||
options?: RequestOptions
|
||||
): Promise<SchemaOutput<T>> {
|
||||
return this.request({ method: 'tasks/result', params }, resultSchema, options);
|
||||
return this._requestWithSchema({ method: 'tasks/result', params }, resultSchema, options);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1321,7 +1365,7 @@ export abstract class Protocol<ContextT extends BaseContext> {
|
|||
* @experimental Use `client.experimental.tasks.listTasks()` to access this method.
|
||||
*/
|
||||
protected async listTasks(params?: { cursor?: string }, options?: RequestOptions): Promise<SchemaOutput<typeof ListTasksResultSchema>> {
|
||||
return this.request({ method: 'tasks/list', params }, ListTasksResultSchema, options);
|
||||
return this._requestWithSchema({ method: 'tasks/list', params }, ListTasksResultSchema, options);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1330,7 +1374,7 @@ export abstract class Protocol<ContextT extends BaseContext> {
|
|||
* @experimental Use `client.experimental.tasks.cancelTask()` to access this method.
|
||||
*/
|
||||
protected async cancelTask(params: { taskId: string }, options?: RequestOptions): Promise<SchemaOutput<typeof CancelTaskResultSchema>> {
|
||||
return this.request({ method: 'tasks/cancel', params }, CancelTaskResultSchema, options);
|
||||
return this._requestWithSchema({ method: 'tasks/cancel', params }, CancelTaskResultSchema, options);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -2638,7 +2638,39 @@ export type ResultTypeMap = {
|
|||
'tasks/cancel': CancelTaskResult;
|
||||
};
|
||||
|
||||
/* Runtime schema lookup */
|
||||
/* Runtime schema lookup — result schemas by method */
|
||||
const resultSchemas: Record<string, z.core.$ZodType> = {
|
||||
ping: EmptyResultSchema,
|
||||
initialize: InitializeResultSchema,
|
||||
'completion/complete': CompleteResultSchema,
|
||||
'logging/setLevel': EmptyResultSchema,
|
||||
'prompts/get': GetPromptResultSchema,
|
||||
'prompts/list': ListPromptsResultSchema,
|
||||
'resources/list': ListResourcesResultSchema,
|
||||
'resources/templates/list': ListResourceTemplatesResultSchema,
|
||||
'resources/read': ReadResourceResultSchema,
|
||||
'resources/subscribe': EmptyResultSchema,
|
||||
'resources/unsubscribe': EmptyResultSchema,
|
||||
'tools/call': z.union([CallToolResultSchema, CreateTaskResultSchema]),
|
||||
'tools/list': ListToolsResultSchema,
|
||||
'sampling/createMessage': z.union([CreateMessageResultWithToolsSchema, CreateTaskResultSchema]),
|
||||
'elicitation/create': z.union([ElicitResultSchema, CreateTaskResultSchema]),
|
||||
'roots/list': ListRootsResultSchema,
|
||||
'tasks/get': GetTaskResultSchema,
|
||||
'tasks/result': ResultSchema,
|
||||
'tasks/list': ListTasksResultSchema,
|
||||
'tasks/cancel': CancelTaskResultSchema
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the Zod schema for validating results of a given request method.
|
||||
* @see getRequestSchema for explanation of the internal type assertion.
|
||||
*/
|
||||
export function getResultSchema<M extends RequestMethod>(method: M): z.ZodType<ResultTypeMap[M]> {
|
||||
return resultSchemas[method] as unknown as z.ZodType<ResultTypeMap[M]>;
|
||||
}
|
||||
|
||||
/* Runtime schema lookup — request schemas by method */
|
||||
type RequestSchemaType = (typeof ClientRequestSchema.options)[number] | (typeof ServerRequestSchema.options)[number];
|
||||
type NotificationSchemaType = (typeof ClientNotificationSchema.options)[number] | (typeof ServerNotificationSchema.options)[number];
|
||||
|
||||
|
|
|
|||
|
|
@ -41,10 +41,15 @@ interface TestProtocol {
|
|||
_taskProgressTokens: Map<string, number>;
|
||||
_clearTaskQueue: (taskId: string, sessionId?: string) => Promise<void>;
|
||||
requestTaskStore: (request: Request, authInfo: unknown) => TaskStore;
|
||||
// Protected task methods (exposed for testing)
|
||||
// Protected methods (exposed for testing)
|
||||
_requestWithSchema: <T extends ZodType>(request: Request, resultSchema: T, options?: unknown) => Promise<z.output<T>>;
|
||||
listTasks: (params?: { cursor?: string }) => Promise<{ tasks: Task[]; nextCursor?: string }>;
|
||||
cancelTask: (params: { taskId: string }) => Promise<Result>;
|
||||
requestStream: <T extends Result>(request: Request, schema: ZodType<T>, options?: unknown) => AsyncGenerator<ResponseMessage<T>>;
|
||||
_requestStreamWithSchema: <T extends Result>(
|
||||
request: Request,
|
||||
schema: ZodType<T>,
|
||||
options?: unknown
|
||||
) => AsyncGenerator<ResponseMessage<T>>;
|
||||
}
|
||||
|
||||
// Mock Transport class
|
||||
|
|
@ -149,6 +154,15 @@ function assertQueuedRequest(o?: QueuedMessage): asserts o is QueuedRequest {
|
|||
expect(o?.type).toBe('request');
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to call the protected _requestWithSchema method from tests that
|
||||
* use custom method names not present in RequestMethod.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function testRequest(proto: Protocol<BaseContext>, request: Request, resultSchema: ZodType, options?: any) {
|
||||
return (proto as unknown as TestProtocol)._requestWithSchema(request, resultSchema, options);
|
||||
}
|
||||
|
||||
describe('protocol tests', () => {
|
||||
let protocol: Protocol<BaseContext>;
|
||||
let transport: MockTransport;
|
||||
|
|
@ -176,7 +190,7 @@ describe('protocol tests', () => {
|
|||
const mockSchema: ZodType<{ result: string }> = z.object({
|
||||
result: z.string()
|
||||
});
|
||||
await protocol.request(request, mockSchema, {
|
||||
await testRequest(protocol, request, mockSchema, {
|
||||
timeout: 0
|
||||
});
|
||||
} catch (error) {
|
||||
|
|
@ -230,13 +244,11 @@ describe('protocol tests', () => {
|
|||
const onProgressMock = vi.fn();
|
||||
|
||||
// Start request but don't await - we're testing the sent message
|
||||
void protocol
|
||||
.request(request, mockSchema, {
|
||||
onprogress: onProgressMock
|
||||
})
|
||||
.catch(() => {
|
||||
// May not complete, ignore error
|
||||
});
|
||||
void testRequest(protocol, request, mockSchema, {
|
||||
onprogress: onProgressMock
|
||||
}).catch(() => {
|
||||
// May not complete, ignore error
|
||||
});
|
||||
|
||||
expect(sendSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
|
|
@ -270,13 +282,11 @@ describe('protocol tests', () => {
|
|||
const onProgressMock = vi.fn();
|
||||
|
||||
// Start request but don't await - we're testing the sent message
|
||||
void protocol
|
||||
.request(request, mockSchema, {
|
||||
onprogress: onProgressMock
|
||||
})
|
||||
.catch(() => {
|
||||
// May not complete, ignore error
|
||||
});
|
||||
void testRequest(protocol, request, mockSchema, {
|
||||
onprogress: onProgressMock
|
||||
}).catch(() => {
|
||||
// May not complete, ignore error
|
||||
});
|
||||
|
||||
expect(sendSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
|
|
@ -310,7 +320,7 @@ describe('protocol tests', () => {
|
|||
});
|
||||
|
||||
// Start request but don't await - we're testing the sent message
|
||||
void protocol.request(request, mockSchema).catch(() => {
|
||||
void testRequest(protocol, request, mockSchema).catch(() => {
|
||||
// May not complete, ignore error
|
||||
});
|
||||
|
||||
|
|
@ -341,13 +351,11 @@ describe('protocol tests', () => {
|
|||
const onProgressMock = vi.fn();
|
||||
|
||||
// Start request but don't await - we're testing the sent message
|
||||
void protocol
|
||||
.request(request, mockSchema, {
|
||||
onprogress: onProgressMock
|
||||
})
|
||||
.catch(() => {
|
||||
// May not complete, ignore error
|
||||
});
|
||||
void testRequest(protocol, request, mockSchema, {
|
||||
onprogress: onProgressMock
|
||||
}).catch(() => {
|
||||
// May not complete, ignore error
|
||||
});
|
||||
|
||||
expect(sendSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
|
|
@ -380,7 +388,7 @@ describe('protocol tests', () => {
|
|||
result: z.string()
|
||||
});
|
||||
const onProgressMock = vi.fn();
|
||||
const requestPromise = protocol.request(request, mockSchema, {
|
||||
const requestPromise = testRequest(protocol, request, mockSchema, {
|
||||
timeout: 1000,
|
||||
resetTimeoutOnProgress: false,
|
||||
onprogress: onProgressMock
|
||||
|
|
@ -418,7 +426,7 @@ describe('protocol tests', () => {
|
|||
result: z.string()
|
||||
});
|
||||
const onProgressMock = vi.fn();
|
||||
const requestPromise = protocol.request(request, mockSchema, {
|
||||
const requestPromise = testRequest(protocol, request, mockSchema, {
|
||||
timeout: 1000,
|
||||
resetTimeoutOnProgress: true,
|
||||
onprogress: onProgressMock
|
||||
|
|
@ -459,7 +467,7 @@ describe('protocol tests', () => {
|
|||
result: z.string()
|
||||
});
|
||||
const onProgressMock = vi.fn();
|
||||
const requestPromise = protocol.request(request, mockSchema, {
|
||||
const requestPromise = testRequest(protocol, request, mockSchema, {
|
||||
timeout: 1000,
|
||||
maxTotalTimeout: 150,
|
||||
resetTimeoutOnProgress: true,
|
||||
|
|
@ -506,7 +514,7 @@ describe('protocol tests', () => {
|
|||
const mockSchema: ZodType<{ result: string }> = z.object({
|
||||
result: z.string()
|
||||
});
|
||||
const requestPromise = protocol.request(request, mockSchema, {
|
||||
const requestPromise = testRequest(protocol, request, mockSchema, {
|
||||
timeout: 100,
|
||||
resetTimeoutOnProgress: true
|
||||
});
|
||||
|
|
@ -521,7 +529,7 @@ describe('protocol tests', () => {
|
|||
result: z.string()
|
||||
});
|
||||
const onProgressMock = vi.fn();
|
||||
const requestPromise = protocol.request(request, mockSchema, {
|
||||
const requestPromise = testRequest(protocol, request, mockSchema, {
|
||||
timeout: 1000,
|
||||
resetTimeoutOnProgress: true,
|
||||
onprogress: onProgressMock
|
||||
|
|
@ -566,7 +574,7 @@ describe('protocol tests', () => {
|
|||
});
|
||||
const onProgressMock = vi.fn();
|
||||
|
||||
const requestPromise = protocol.request(request, mockSchema, {
|
||||
const requestPromise = testRequest(protocol, request, mockSchema, {
|
||||
timeout: 1000,
|
||||
onprogress: onProgressMock
|
||||
});
|
||||
|
|
@ -1060,16 +1068,14 @@ describe('Task-based execution', () => {
|
|||
content: z.array(z.object({ type: z.literal('text'), text: z.string() }))
|
||||
});
|
||||
|
||||
void protocol
|
||||
.request(request, resultSchema, {
|
||||
task: {
|
||||
ttl: 30000,
|
||||
pollInterval: 1000
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// May not complete, ignore error
|
||||
});
|
||||
void testRequest(protocol, request, resultSchema, {
|
||||
task: {
|
||||
ttl: 30000,
|
||||
pollInterval: 1000
|
||||
}
|
||||
}).catch(() => {
|
||||
// May not complete, ignore error
|
||||
});
|
||||
|
||||
expect(sendSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
|
|
@ -1103,15 +1109,13 @@ describe('Task-based execution', () => {
|
|||
content: z.array(z.object({ type: z.literal('text'), text: z.string() }))
|
||||
});
|
||||
|
||||
void protocol
|
||||
.request(request, resultSchema, {
|
||||
task: {
|
||||
ttl: 60000
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// May not complete, ignore error
|
||||
});
|
||||
void testRequest(protocol, request, resultSchema, {
|
||||
task: {
|
||||
ttl: 60000
|
||||
}
|
||||
}).catch(() => {
|
||||
// May not complete, ignore error
|
||||
});
|
||||
|
||||
expect(sendSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
|
|
@ -1141,7 +1145,7 @@ describe('Task-based execution', () => {
|
|||
content: z.array(z.object({ type: z.literal('text'), text: z.string() }))
|
||||
});
|
||||
|
||||
const resultPromise = protocol.request(request, resultSchema, {
|
||||
const resultPromise = testRequest(protocol, request, resultSchema, {
|
||||
task: {
|
||||
ttl: 30000
|
||||
}
|
||||
|
|
@ -1164,15 +1168,13 @@ describe('Task-based execution', () => {
|
|||
const resultSchema = z.object({});
|
||||
|
||||
// Start the request (don't await completion, just let it send)
|
||||
void protocol
|
||||
.request(request, resultSchema, {
|
||||
relatedTask: {
|
||||
taskId: 'parent-task-123'
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// May not complete, ignore error
|
||||
});
|
||||
void testRequest(protocol, request, resultSchema, {
|
||||
relatedTask: {
|
||||
taskId: 'parent-task-123'
|
||||
}
|
||||
}).catch(() => {
|
||||
// May not complete, ignore error
|
||||
});
|
||||
|
||||
// Wait a bit for the request to be queued
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
|
|
@ -1230,20 +1232,18 @@ describe('Task-based execution', () => {
|
|||
});
|
||||
|
||||
// Start the request (don't await completion, just let it send)
|
||||
void protocol
|
||||
.request(request, resultSchema, {
|
||||
task: {
|
||||
ttl: 60000,
|
||||
pollInterval: 1000
|
||||
},
|
||||
relatedTask: {
|
||||
taskId: 'parent-task'
|
||||
},
|
||||
onprogress: vi.fn()
|
||||
})
|
||||
.catch(() => {
|
||||
// May not complete, ignore error
|
||||
});
|
||||
void testRequest(protocol, request, resultSchema, {
|
||||
task: {
|
||||
ttl: 60000,
|
||||
pollInterval: 1000
|
||||
},
|
||||
relatedTask: {
|
||||
taskId: 'parent-task'
|
||||
},
|
||||
onprogress: vi.fn()
|
||||
}).catch(() => {
|
||||
// May not complete, ignore error
|
||||
});
|
||||
|
||||
// Wait a bit for the request to be queued
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
|
|
@ -2482,14 +2482,12 @@ describe('Progress notification support for tasks', () => {
|
|||
});
|
||||
|
||||
// Start a task-augmented request with progress callback
|
||||
void protocol
|
||||
.request(request, resultSchema, {
|
||||
task: { ttl: 60000 },
|
||||
onprogress: progressCallback
|
||||
})
|
||||
.catch(() => {
|
||||
// May not complete, ignore error
|
||||
});
|
||||
void testRequest(protocol, request, resultSchema, {
|
||||
task: { ttl: 60000 },
|
||||
onprogress: progressCallback
|
||||
}).catch(() => {
|
||||
// May not complete, ignore error
|
||||
});
|
||||
|
||||
// Wait a bit for the request to be sent
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
|
|
@ -2596,14 +2594,12 @@ describe('Progress notification support for tasks', () => {
|
|||
});
|
||||
|
||||
// Start a task-augmented request with progress callback
|
||||
void protocol
|
||||
.request(request, resultSchema, {
|
||||
task: { ttl: 60000 },
|
||||
onprogress: progressCallback
|
||||
})
|
||||
.catch(() => {
|
||||
// May not complete, ignore error
|
||||
});
|
||||
void testRequest(protocol, request, resultSchema, {
|
||||
task: { ttl: 60000 },
|
||||
onprogress: progressCallback
|
||||
}).catch(() => {
|
||||
// May not complete, ignore error
|
||||
});
|
||||
|
||||
// Wait a bit for the request to be sent
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
|
|
@ -2716,7 +2712,7 @@ describe('Progress notification support for tasks', () => {
|
|||
})
|
||||
});
|
||||
|
||||
void protocol.request(request, resultSchema, {
|
||||
void testRequest(protocol, request, resultSchema, {
|
||||
task: { ttl: 60000 },
|
||||
onprogress: progressCallback
|
||||
});
|
||||
|
|
@ -2817,7 +2813,7 @@ describe('Progress notification support for tasks', () => {
|
|||
})
|
||||
});
|
||||
|
||||
void protocol.request(request, resultSchema, {
|
||||
void testRequest(protocol, request, resultSchema, {
|
||||
task: { ttl: 60000 },
|
||||
onprogress: progressCallback
|
||||
});
|
||||
|
|
@ -2915,7 +2911,7 @@ describe('Progress notification support for tasks', () => {
|
|||
})
|
||||
});
|
||||
|
||||
void protocol.request(request, resultSchema, {
|
||||
void testRequest(protocol, request, resultSchema, {
|
||||
task: { ttl: 60000 },
|
||||
onprogress: progressCallback
|
||||
});
|
||||
|
|
@ -2986,7 +2982,7 @@ describe('Progress notification support for tasks', () => {
|
|||
|
||||
const onProgressMock = vi.fn();
|
||||
|
||||
void protocol.request(request, resultSchema, {
|
||||
void testRequest(protocol, request, resultSchema, {
|
||||
task: {
|
||||
ttl: 60000
|
||||
},
|
||||
|
|
@ -3011,7 +3007,7 @@ describe('Progress notification support for tasks', () => {
|
|||
|
||||
const onProgressMock = vi.fn();
|
||||
|
||||
void protocol.request(request, resultSchema, {
|
||||
void testRequest(protocol, request, resultSchema, {
|
||||
task: {
|
||||
ttl: 30000
|
||||
},
|
||||
|
|
@ -3061,7 +3057,7 @@ describe('Progress notification support for tasks', () => {
|
|||
|
||||
const onProgressMock = vi.fn();
|
||||
|
||||
void protocol.request(request, resultSchema, {
|
||||
void testRequest(protocol, request, resultSchema, {
|
||||
task: {
|
||||
ttl: 30000
|
||||
},
|
||||
|
|
@ -3375,7 +3371,8 @@ describe('Message interception for task-related requests', () => {
|
|||
const task = await taskStore.createTask({ ttl: 60000 }, 'test-request-1', { method: 'tools/call', params: {} });
|
||||
|
||||
// Send a request with related task metadata (don't await - we're testing queuing)
|
||||
const requestPromise = server.request(
|
||||
const requestPromise = testRequest(
|
||||
server,
|
||||
{
|
||||
method: 'ping',
|
||||
params: {}
|
||||
|
|
@ -3427,7 +3424,8 @@ describe('Message interception for task-related requests', () => {
|
|||
await server.connect(transport);
|
||||
|
||||
// Send a request without related task metadata
|
||||
const requestPromise = server.request(
|
||||
const requestPromise = testRequest(
|
||||
server,
|
||||
{
|
||||
method: 'ping',
|
||||
params: {}
|
||||
|
|
@ -3472,7 +3470,8 @@ describe('Message interception for task-related requests', () => {
|
|||
const task = await taskStore.createTask({ ttl: 60000 }, 'test-request-1', { method: 'tools/call', params: {} });
|
||||
|
||||
// Send a request with related task metadata
|
||||
const requestPromise = server.request(
|
||||
const requestPromise = testRequest(
|
||||
server,
|
||||
{
|
||||
method: 'ping',
|
||||
params: {}
|
||||
|
|
@ -3528,7 +3527,8 @@ describe('Message interception for task-related requests', () => {
|
|||
const task = await taskStore.createTask({ ttl: 60000 }, 'test-request-1', { method: 'tools/call', params: {} });
|
||||
|
||||
// Send a request with related task metadata
|
||||
const requestPromise = server.request(
|
||||
const requestPromise = testRequest(
|
||||
server,
|
||||
{
|
||||
method: 'ping',
|
||||
params: {}
|
||||
|
|
@ -3601,7 +3601,8 @@ describe('Message interception for task-related requests', () => {
|
|||
const task = await taskStore.createTask({ ttl: 60000 }, 'test-request-1', { method: 'tools/call', params: {} });
|
||||
|
||||
// Send a request with related task metadata
|
||||
void server.request(
|
||||
void testRequest(
|
||||
server,
|
||||
{
|
||||
method: 'ping',
|
||||
params: {}
|
||||
|
|
@ -3677,26 +3678,26 @@ describe('Message interception for task-related requests', () => {
|
|||
// Fill the queue to max capacity (100 messages)
|
||||
const promises: Promise<unknown>[] = [];
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const promise = server
|
||||
.request(
|
||||
{
|
||||
method: 'ping',
|
||||
params: {}
|
||||
},
|
||||
z.object({}),
|
||||
{
|
||||
relatedTask: { taskId: task.taskId }
|
||||
}
|
||||
)
|
||||
.catch(() => {
|
||||
// Requests will remain pending until task completes or fails
|
||||
});
|
||||
const promise = testRequest(
|
||||
server,
|
||||
{
|
||||
method: 'ping',
|
||||
params: {}
|
||||
},
|
||||
z.object({}),
|
||||
{
|
||||
relatedTask: { taskId: task.taskId }
|
||||
}
|
||||
).catch(() => {
|
||||
// Requests will remain pending until task completes or fails
|
||||
});
|
||||
promises.push(promise);
|
||||
}
|
||||
|
||||
// Try to add one more request - should throw an error
|
||||
await expect(
|
||||
server.request(
|
||||
testRequest(
|
||||
server,
|
||||
{
|
||||
method: 'ping',
|
||||
params: {}
|
||||
|
|
@ -3766,7 +3767,8 @@ describe('Message Interception', () => {
|
|||
const mockSchema = z.object({ result: z.string() });
|
||||
|
||||
// Send a request with relatedTask metadata
|
||||
const requestPromise = protocol.request(
|
||||
const requestPromise = testRequest(
|
||||
protocol,
|
||||
{
|
||||
method: 'test/request',
|
||||
params: { data: 'test' }
|
||||
|
|
@ -3969,7 +3971,8 @@ describe('Message Interception', () => {
|
|||
const sendSpy = vi.spyOn(transport, 'send');
|
||||
|
||||
// Send a request without relatedTask metadata
|
||||
const requestPromise = protocol.request(
|
||||
const requestPromise = testRequest(
|
||||
protocol,
|
||||
{
|
||||
method: 'test/request',
|
||||
params: { data: 'test' }
|
||||
|
|
@ -4030,7 +4033,8 @@ describe('Message Interception', () => {
|
|||
const mockSchema = z.object({ result: z.string() });
|
||||
|
||||
// Send a request with relatedTask metadata
|
||||
const requestPromise = protocol.request(
|
||||
const requestPromise = testRequest(
|
||||
protocol,
|
||||
{
|
||||
method: 'test/request',
|
||||
params: { data: 'test' }
|
||||
|
|
@ -4174,7 +4178,8 @@ describe('Message Interception', () => {
|
|||
const relatedTask = { taskId: 'task-meta-456' };
|
||||
const mockSchema = z.object({ result: z.string() });
|
||||
|
||||
const requestPromise = protocol.request(
|
||||
const requestPromise = testRequest(
|
||||
protocol,
|
||||
{
|
||||
method: 'test/request',
|
||||
params: { data: 'test' }
|
||||
|
|
@ -4366,11 +4371,14 @@ describe('Queue lifecycle management', () => {
|
|||
const taskId = task.taskId;
|
||||
|
||||
// Queue a request (catch rejection to avoid unhandled promise rejection)
|
||||
const requestPromise = protocol
|
||||
.request({ method: 'test/request', params: { data: 'test' } }, z.object({ result: z.string() }), {
|
||||
const requestPromise = testRequest(
|
||||
protocol,
|
||||
{ method: 'test/request', params: { data: 'test' } },
|
||||
z.object({ result: z.string() }),
|
||||
{
|
||||
relatedTask: { taskId }
|
||||
})
|
||||
.catch(err => err);
|
||||
}
|
||||
).catch(err => err);
|
||||
|
||||
// Verify request is queued
|
||||
const queue = (protocol as unknown as TestProtocol)._taskMessageQueue;
|
||||
|
|
@ -4391,7 +4399,7 @@ describe('Queue lifecycle management', () => {
|
|||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
|
||||
// Verify the request promise is rejected
|
||||
const result = await requestPromise;
|
||||
const result = (await requestPromise) as Error;
|
||||
expect(result).toBeInstanceOf(ProtocolError);
|
||||
expect(result.message).toContain('Task cancelled or completed');
|
||||
|
||||
|
|
@ -4439,11 +4447,14 @@ describe('Queue lifecycle management', () => {
|
|||
const taskId = task.taskId;
|
||||
|
||||
// Queue a request (catch the rejection to avoid unhandled promise rejection)
|
||||
const requestPromise = protocol
|
||||
.request({ method: 'test/request', params: { data: 'test' } }, z.object({ result: z.string() }), {
|
||||
const requestPromise = testRequest(
|
||||
protocol,
|
||||
{ method: 'test/request', params: { data: 'test' } },
|
||||
z.object({ result: z.string() }),
|
||||
{
|
||||
relatedTask: { taskId }
|
||||
})
|
||||
.catch(err => err);
|
||||
}
|
||||
).catch(err => err);
|
||||
|
||||
// Verify request is queued
|
||||
const queue = (protocol as unknown as TestProtocol)._taskMessageQueue;
|
||||
|
|
@ -4453,7 +4464,7 @@ describe('Queue lifecycle management', () => {
|
|||
(protocol as unknown as TestProtocol)._clearTaskQueue(taskId);
|
||||
|
||||
// Verify the request promise is rejected
|
||||
const result = await requestPromise;
|
||||
const result = (await requestPromise) as Error;
|
||||
expect(result).toBeInstanceOf(ProtocolError);
|
||||
expect(result.message).toContain('Task cancelled or completed');
|
||||
|
||||
|
|
@ -4472,23 +4483,32 @@ describe('Queue lifecycle management', () => {
|
|||
const taskId = task.taskId;
|
||||
|
||||
// Queue multiple requests (catch rejections to avoid unhandled promise rejections)
|
||||
const request1Promise = protocol
|
||||
.request({ method: 'test/request1', params: { data: 'test1' } }, z.object({ result: z.string() }), {
|
||||
const request1Promise = testRequest(
|
||||
protocol,
|
||||
{ method: 'test/request1', params: { data: 'test1' } },
|
||||
z.object({ result: z.string() }),
|
||||
{
|
||||
relatedTask: { taskId }
|
||||
})
|
||||
.catch(err => err);
|
||||
}
|
||||
).catch(err => err);
|
||||
|
||||
const request2Promise = protocol
|
||||
.request({ method: 'test/request2', params: { data: 'test2' } }, z.object({ result: z.string() }), {
|
||||
const request2Promise = testRequest(
|
||||
protocol,
|
||||
{ method: 'test/request2', params: { data: 'test2' } },
|
||||
z.object({ result: z.string() }),
|
||||
{
|
||||
relatedTask: { taskId }
|
||||
})
|
||||
.catch(err => err);
|
||||
}
|
||||
).catch(err => err);
|
||||
|
||||
const request3Promise = protocol
|
||||
.request({ method: 'test/request3', params: { data: 'test3' } }, z.object({ result: z.string() }), {
|
||||
const request3Promise = testRequest(
|
||||
protocol,
|
||||
{ method: 'test/request3', params: { data: 'test3' } },
|
||||
z.object({ result: z.string() }),
|
||||
{
|
||||
relatedTask: { taskId }
|
||||
})
|
||||
.catch(err => err);
|
||||
}
|
||||
).catch(err => err);
|
||||
|
||||
// Verify requests are queued
|
||||
const queue = (protocol as unknown as TestProtocol)._taskMessageQueue;
|
||||
|
|
@ -4498,9 +4518,9 @@ describe('Queue lifecycle management', () => {
|
|||
(protocol as unknown as TestProtocol)._clearTaskQueue(taskId);
|
||||
|
||||
// Verify all request promises are rejected
|
||||
const result1 = await request1Promise;
|
||||
const result2 = await request2Promise;
|
||||
const result3 = await request3Promise;
|
||||
const result1 = (await request1Promise) as Error;
|
||||
const result2 = (await request2Promise) as Error;
|
||||
const result3 = (await request3Promise) as Error;
|
||||
|
||||
expect(result1).toBeInstanceOf(ProtocolError);
|
||||
expect(result1.message).toContain('Task cancelled or completed');
|
||||
|
|
@ -4522,11 +4542,14 @@ describe('Queue lifecycle management', () => {
|
|||
const taskId = task.taskId;
|
||||
|
||||
// Queue a request (catch rejection to avoid unhandled promise rejection)
|
||||
const requestPromise = protocol
|
||||
.request({ method: 'test/request', params: { data: 'test' } }, z.object({ result: z.string() }), {
|
||||
const requestPromise = testRequest(
|
||||
protocol,
|
||||
{ method: 'test/request', params: { data: 'test' } },
|
||||
z.object({ result: z.string() }),
|
||||
{
|
||||
relatedTask: { taskId }
|
||||
})
|
||||
.catch(err => err);
|
||||
}
|
||||
).catch(err => err);
|
||||
|
||||
// Get the request ID that was sent
|
||||
const requestResolvers = (protocol as unknown as TestProtocol)._requestResolvers;
|
||||
|
|
@ -4541,7 +4564,7 @@ describe('Queue lifecycle management', () => {
|
|||
(protocol as unknown as TestProtocol)._clearTaskQueue(taskId);
|
||||
|
||||
// Verify request promise is rejected
|
||||
const result = await requestPromise;
|
||||
const result = (await requestPromise) as Error;
|
||||
expect(result).toBeInstanceOf(ProtocolError);
|
||||
expect(result.message).toContain('Task cancelled or completed');
|
||||
|
||||
|
|
@ -4575,7 +4598,7 @@ describe('requestStream() method', () => {
|
|||
// Start the request stream
|
||||
const streamPromise = (async () => {
|
||||
const messages = [];
|
||||
const stream = (protocol as unknown as TestProtocol).requestStream(
|
||||
const stream = (protocol as unknown as TestProtocol)._requestStreamWithSchema(
|
||||
{ method: 'tools/call', params: { name: 'test', arguments: {} } },
|
||||
CallToolResultSchema
|
||||
);
|
||||
|
|
@ -4621,7 +4644,7 @@ describe('requestStream() method', () => {
|
|||
// Start the request stream
|
||||
const streamPromise = (async () => {
|
||||
const messages = [];
|
||||
const stream = (protocol as unknown as TestProtocol).requestStream(
|
||||
const stream = (protocol as unknown as TestProtocol)._requestStreamWithSchema(
|
||||
{ method: 'tools/call', params: { name: 'test', arguments: {} } },
|
||||
CallToolResultSchema
|
||||
);
|
||||
|
|
@ -4674,7 +4697,7 @@ describe('requestStream() method', () => {
|
|||
|
||||
// Start the request stream with already-aborted signal
|
||||
const messages = [];
|
||||
const stream = (protocol as unknown as TestProtocol).requestStream(
|
||||
const stream = (protocol as unknown as TestProtocol)._requestStreamWithSchema(
|
||||
{ method: 'tools/call', params: { name: 'test', arguments: {} } },
|
||||
CallToolResultSchema,
|
||||
{
|
||||
|
|
@ -4709,7 +4732,7 @@ describe('requestStream() method', () => {
|
|||
await protocol.connect(transport);
|
||||
|
||||
const messagesPromise = toArrayAsync(
|
||||
(protocol as unknown as TestProtocol).requestStream(
|
||||
(protocol as unknown as TestProtocol)._requestStreamWithSchema(
|
||||
{ method: 'tools/call', params: { name: 'test', arguments: {} } },
|
||||
CallToolResultSchema
|
||||
)
|
||||
|
|
@ -4754,7 +4777,7 @@ describe('requestStream() method', () => {
|
|||
await protocol.connect(transport);
|
||||
|
||||
const messagesPromise = toArrayAsync(
|
||||
(protocol as unknown as TestProtocol).requestStream(
|
||||
(protocol as unknown as TestProtocol)._requestStreamWithSchema(
|
||||
{ method: 'tools/call', params: { name: 'test', arguments: {} } },
|
||||
CallToolResultSchema,
|
||||
{
|
||||
|
|
@ -4800,7 +4823,7 @@ describe('requestStream() method', () => {
|
|||
|
||||
// Collect messages
|
||||
const messages = await toArrayAsync(
|
||||
(protocol as unknown as TestProtocol).requestStream(
|
||||
(protocol as unknown as TestProtocol)._requestStreamWithSchema(
|
||||
{ method: 'tools/call', params: { name: 'test', arguments: {} } },
|
||||
CallToolResultSchema,
|
||||
{
|
||||
|
|
@ -4832,7 +4855,7 @@ describe('requestStream() method', () => {
|
|||
await protocol.connect(transport);
|
||||
|
||||
const messagesPromise = toArrayAsync(
|
||||
(protocol as unknown as TestProtocol).requestStream(
|
||||
(protocol as unknown as TestProtocol)._requestStreamWithSchema(
|
||||
{ method: 'tools/call', params: { name: 'test', arguments: {} } },
|
||||
CallToolResultSchema
|
||||
)
|
||||
|
|
@ -4887,7 +4910,7 @@ describe('requestStream() method', () => {
|
|||
await protocol.connect(transport);
|
||||
|
||||
const messagesPromise = toArrayAsync(
|
||||
(protocol as unknown as TestProtocol).requestStream(
|
||||
(protocol as unknown as TestProtocol)._requestStreamWithSchema(
|
||||
{ method: 'tools/call', params: { name: 'test', arguments: {} } },
|
||||
CallToolResultSchema
|
||||
)
|
||||
|
|
@ -4953,7 +4976,7 @@ describe('requestStream() method', () => {
|
|||
transport.send = vi.fn().mockRejectedValue(new Error('Network error'));
|
||||
|
||||
const messages = await toArrayAsync(
|
||||
(protocol as unknown as TestProtocol).requestStream(
|
||||
(protocol as unknown as TestProtocol)._requestStreamWithSchema(
|
||||
{ method: 'tools/call', params: { name: 'test', arguments: {} } },
|
||||
CallToolResultSchema
|
||||
)
|
||||
|
|
@ -4981,7 +5004,7 @@ describe('requestStream() method', () => {
|
|||
await protocol.connect(transport);
|
||||
|
||||
const messagesPromise = toArrayAsync(
|
||||
(protocol as unknown as TestProtocol).requestStream(
|
||||
(protocol as unknown as TestProtocol)._requestStreamWithSchema(
|
||||
{ method: 'tools/call', params: { name: 'test', arguments: {} } },
|
||||
CallToolResultSchema
|
||||
)
|
||||
|
|
|
|||
|
|
@ -15,13 +15,12 @@ import type {
|
|||
ElicitResult,
|
||||
GetTaskResult,
|
||||
ListTasksResult,
|
||||
Request,
|
||||
RequestMethod,
|
||||
RequestOptions,
|
||||
ResponseMessage,
|
||||
Result,
|
||||
ResultTypeMap,
|
||||
SchemaOutput
|
||||
} from '@modelcontextprotocol/core';
|
||||
import { CreateMessageResultSchema, ElicitResultSchema } from '@modelcontextprotocol/core';
|
||||
|
||||
import type { Server } from '../../server/server.js';
|
||||
|
||||
|
|
@ -30,7 +29,7 @@ import type { Server } from '../../server/server.js';
|
|||
*
|
||||
* Access via `server.experimental.tasks`:
|
||||
* ```typescript
|
||||
* const stream = server.experimental.tasks.requestStream(request, schema, options);
|
||||
* const stream = server.experimental.tasks.requestStream(request, options);
|
||||
* ```
|
||||
*
|
||||
* For high-level server usage with task-based tools, use {@linkcode index.McpServer | McpServer}.experimental.tasks instead.
|
||||
|
|
@ -47,27 +46,24 @@ export class ExperimentalServerTasks {
|
|||
* This method provides streaming access to request processing, allowing you to
|
||||
* observe intermediate task status updates for task-augmented requests.
|
||||
*
|
||||
* @param request - The request to send
|
||||
* @param resultSchema - Zod schema for validating the result
|
||||
* @param request - The request to send (method name determines the result schema)
|
||||
* @param options - Optional request options (timeout, signal, task creation params, etc.)
|
||||
* @returns AsyncGenerator that yields {@linkcode ResponseMessage} objects
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
requestStream<T extends AnySchema>(
|
||||
request: Request,
|
||||
resultSchema: T,
|
||||
requestStream<M extends RequestMethod>(
|
||||
request: { method: M; params?: Record<string, unknown> },
|
||||
options?: RequestOptions
|
||||
): AsyncGenerator<ResponseMessage<SchemaOutput<T> & Result>, void, void> {
|
||||
): AsyncGenerator<ResponseMessage<ResultTypeMap[M]>, void, void> {
|
||||
// Delegate to the server's underlying Protocol method
|
||||
type ServerWithRequestStream = {
|
||||
requestStream<U extends AnySchema>(
|
||||
request: Request,
|
||||
resultSchema: U,
|
||||
requestStream<N extends RequestMethod>(
|
||||
request: { method: N; params?: Record<string, unknown> },
|
||||
options?: RequestOptions
|
||||
): AsyncGenerator<ResponseMessage<SchemaOutput<U> & Result>, void, void>;
|
||||
): AsyncGenerator<ResponseMessage<ResultTypeMap[N]>, void, void>;
|
||||
};
|
||||
return (this._server as unknown as ServerWithRequestStream).requestStream(request, resultSchema, options);
|
||||
return (this._server as unknown as ServerWithRequestStream).requestStream(request, options);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -163,9 +159,8 @@ export class ExperimentalServerTasks {
|
|||
method: 'sampling/createMessage',
|
||||
params
|
||||
},
|
||||
CreateMessageResultSchema,
|
||||
options
|
||||
);
|
||||
) as AsyncGenerator<ResponseMessage<CreateMessageResult>, void, void>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -241,9 +236,8 @@ export class ExperimentalServerTasks {
|
|||
method: 'elicitation/create',
|
||||
params: normalizedParams
|
||||
},
|
||||
ElicitResultSchema,
|
||||
options
|
||||
);
|
||||
) as AsyncGenerator<ResponseMessage<ElicitResult>, void, void>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -460,7 +460,7 @@ export class Server extends Protocol<ServerContext> {
|
|||
}
|
||||
|
||||
async ping() {
|
||||
return this.request({ method: 'ping' }, EmptyResultSchema);
|
||||
return this._requestWithSchema({ method: 'ping' }, EmptyResultSchema);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -540,9 +540,9 @@ export class Server extends Protocol<ServerContext> {
|
|||
|
||||
// Use different schemas based on whether tools are provided
|
||||
if (params.tools) {
|
||||
return this.request({ method: 'sampling/createMessage', params }, CreateMessageResultWithToolsSchema, options);
|
||||
return this._requestWithSchema({ method: 'sampling/createMessage', params }, CreateMessageResultWithToolsSchema, options);
|
||||
}
|
||||
return this.request({ method: 'sampling/createMessage', params }, CreateMessageResultSchema, options);
|
||||
return this._requestWithSchema({ method: 'sampling/createMessage', params }, CreateMessageResultSchema, options);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -562,7 +562,7 @@ export class Server extends Protocol<ServerContext> {
|
|||
}
|
||||
|
||||
const urlParams = params as ElicitRequestURLParams;
|
||||
return this.request({ method: 'elicitation/create', params: urlParams }, ElicitResultSchema, options);
|
||||
return this._requestWithSchema({ method: 'elicitation/create', params: urlParams }, ElicitResultSchema, options);
|
||||
}
|
||||
case 'form': {
|
||||
if (!this._clientCapabilities?.elicitation?.form) {
|
||||
|
|
@ -572,7 +572,11 @@ export class Server extends Protocol<ServerContext> {
|
|||
const formParams: ElicitRequestFormParams =
|
||||
params.mode === 'form' ? (params as ElicitRequestFormParams) : { ...(params as ElicitRequestFormParams), mode: 'form' };
|
||||
|
||||
const result = await this.request({ method: 'elicitation/create', params: formParams }, ElicitResultSchema, options);
|
||||
const result = await this._requestWithSchema(
|
||||
{ method: 'elicitation/create', params: formParams },
|
||||
ElicitResultSchema,
|
||||
options
|
||||
);
|
||||
|
||||
if (result.action === 'accept' && result.content && formParams.requestedSchema) {
|
||||
try {
|
||||
|
|
@ -629,7 +633,7 @@ export class Server extends Protocol<ServerContext> {
|
|||
}
|
||||
|
||||
async listRoots(params?: ListRootsRequest['params'], options?: RequestOptions) {
|
||||
return this.request({ method: 'roots/list', params }, ListRootsResultSchema, options);
|
||||
return this._requestWithSchema({ method: 'roots/list', params }, ListRootsResultSchema, options);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in a new issue