v2 refactor!: remove Zod schema parameter from public-facing APIs (#1606)

This commit is contained in:
Konstantin Konstantinov 2026-03-04 20:34:47 +02:00 • committed by GitHub
commit f88c376c52
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 1483 additions and 1708 deletions

View file

@ -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);

View file

@ -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': {

View file

@ -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);
}
}