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
30 changed files with 1483 additions and 1708 deletions

View file

@ -16,7 +16,6 @@ The examples below use these imports. Adjust based on which features and transpo
import type { Prompt, Resource, Tool } from '@modelcontextprotocol/client';
import {
applyMiddlewares,
CallToolResultSchema,
Client,
ClientCredentialsProvider,
createMiddleware,
@ -198,13 +197,16 @@ if (result.structuredContent) {
Pass `onprogress` to receive incremental progress notifications from long-running tools. Use `resetTimeoutOnProgress` to keep the request alive while the server is actively reporting, and `maxTotalTimeout` as an absolute cap:
```ts source="../examples/client/src/clientGuide.examples.ts#callTool_progress"
const result = await client.callTool({ name: 'long-operation', arguments: {} }, undefined, {
onprogress: ({ progress, total }) => {
console.log(`Progress: ${progress}/${total ?? '?'}`);
},
resetTimeoutOnProgress: true,
maxTotalTimeout: 600_000
});
const result = await client.callTool(
{ name: 'long-operation', arguments: {} },
{
onprogress: ({ progress, total }: { progress: number; total?: number }) => {
console.log(`Progress: ${progress}/${total ?? '?'}`);
},
resetTimeoutOnProgress: true,
maxTotalTimeout: 600_000
}
);
console.log(result.content);
```
@ -484,7 +486,6 @@ All requests have a 60-second default timeout. Pass a custom `timeout` in the op
try {
const result = await client.callTool(
{ name: 'slow-task', arguments: {} },
undefined,
{ timeout: 120_000 } // 2 minutes instead of the default 60 seconds
);
console.log(result.content);
@ -523,7 +524,6 @@ const result = await client.request(
method: 'tools/call',
params: { name: 'long-running-task', arguments: {} }
},
CallToolResultSchema,
{
resumptionToken: lastToken,
onresumptiontoken: (token: string) => {

View file

@ -203,7 +203,7 @@ import { OAuthError, OAuthErrorCode } from '@modelcontextprotocol/core';
if (error instanceof OAuthError && error.code === OAuthErrorCode.InvalidClient) { ... }
```
**Unchanged APIs** (only import paths changed): `Client` constructor and methods, `McpServer` constructor, `server.connect()`, `server.close()`, all client transports (`StreamableHTTPClientTransport`, `SSEClientTransport`, `StdioClientTransport`), `StdioServerTransport`, all Zod schemas, all callback return types.
**Unchanged APIs** (only import paths changed): `Client` constructor and most methods, `McpServer` constructor, `server.connect()`, `server.close()`, all client transports (`StreamableHTTPClientTransport`, `SSEClientTransport`, `StdioClientTransport`), `StdioServerTransport`, all Zod schemas, all callback return types. Note: `callTool()` and `request()` signatures changed (schema parameter removed, see section 11).
## 6. McpServer API Changes
@ -396,11 +396,39 @@ Request/notification params remain fully typed. Remove unused schema imports aft
| `ctx.mcpReq.elicitInput(params, options?)` | Elicit user input (form or URL) | `server.elicitInput(...)` from within handler |
| `ctx.mcpReq.requestSampling(params, options?)` | Request LLM sampling from client | `server.createMessage(...)` from within handler |
## 11. Client Behavioral Changes
## 11. Schema parameter removed from `request()`, `send()`, and `callTool()`
`Protocol.request()`, `BaseContext.mcpReq.send()`, and `Client.callTool()` no longer take a Zod result schema argument. The SDK resolves the schema internally from the method name.
```typescript
// v1: schema required
import { CallToolResultSchema, ElicitResultSchema } from '@modelcontextprotocol/sdk/types.js';
const result = await client.request({ method: 'tools/call', params: { ... } }, CallToolResultSchema);
const elicit = await ctx.mcpReq.send({ method: 'elicitation/create', params: { ... } }, ElicitResultSchema);
const tool = await client.callTool({ name: 'my-tool', arguments: {} }, CompatibilityCallToolResultSchema);
// v2: no schema argument
const result = await client.request({ method: 'tools/call', params: { ... } });
const elicit = await ctx.mcpReq.send({ method: 'elicitation/create', params: { ... } });
const tool = await client.callTool({ name: 'my-tool', arguments: {} });
```
| v1 call | v2 call |
|---------|---------|
| `client.request(req, ResultSchema)` | `client.request(req)` |
| `client.request(req, ResultSchema, options)` | `client.request(req, options)` |
| `ctx.mcpReq.send(req, ResultSchema)` | `ctx.mcpReq.send(req)` |
| `ctx.mcpReq.send(req, ResultSchema, options)` | `ctx.mcpReq.send(req, options)` |
| `client.callTool(params, CompatibilityCallToolResultSchema)` | `client.callTool(params)` |
| `client.callTool(params, schema, options)` | `client.callTool(params, options)` |
Remove unused schema imports: `CallToolResultSchema`, `CompatibilityCallToolResultSchema`, `ElicitResultSchema`, `CreateMessageResultSchema`, etc., when they were only used in `request()`/`send()`/`callTool()` calls.
## 12. Client Behavioral Changes
`Client.listPrompts()`, `listResources()`, `listResourceTemplates()`, `listTools()` now return empty results when the server lacks the corresponding capability (instead of sending the request). Set `enforceStrictCapabilities: true` in `ClientOptions` to throw an error instead.
## 12. Runtime-Specific JSON Schema Validators (Enhancement)
## 13. Runtime-Specific JSON Schema Validators (Enhancement)
The SDK now auto-selects the appropriate JSON Schema validator based on runtime:
- Node.js → `AjvJsonSchemaValidator` (no change from v1)
@ -420,7 +448,7 @@ new McpServer({ name: 'server', version: '1.0.0' }, {});
Access validators via `_shims` export: `import { DefaultJsonSchemaValidator } from '@modelcontextprotocol/server/_shims';`
## 13. Migration Steps (apply in this order)
## 14. Migration Steps (apply in this order)
1. Update `package.json`: `npm uninstall @modelcontextprotocol/sdk`, install the appropriate v2 packages
2. Replace all imports from `@modelcontextprotocol/sdk/...` using the import mapping tables (sections 3-4), including `StreamableHTTPServerTransport` → `NodeStreamableHTTPServerTransport`

View file

@ -337,6 +337,73 @@ Common method string replacements:
| `ResourceListChangedNotificationSchema` | `'notifications/resources/list_changed'` |
| `PromptListChangedNotificationSchema` | `'notifications/prompts/list_changed'` |
### `Protocol.request()`, `ctx.mcpReq.send()`, and `Client.callTool()` no longer take a schema parameter
The public `Protocol.request()`, `BaseContext.mcpReq.send()`, and `Client.callTool()` methods no longer accept a Zod result schema argument. The SDK now resolves the correct result schema internally based on the method name. This means you no longer need to import result schemas like `CallToolResultSchema` or `ElicitResultSchema` when making requests.
**`client.request()` — Before (v1):**
```typescript
import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';
const result = await client.request(
{ method: 'tools/call', params: { name: 'my-tool', arguments: {} } },
CallToolResultSchema
);
```
**After (v2):**
```typescript
const result = await client.request(
{ method: 'tools/call', params: { name: 'my-tool', arguments: {} } }
);
```
**`ctx.mcpReq.send()` — Before (v1):**
```typescript
import { CreateMessageResultSchema } from '@modelcontextprotocol/sdk/types.js';
server.setRequestHandler('tools/call', async (request, ctx) => {
const samplingResult = await ctx.mcpReq.send(
{ method: 'sampling/createMessage', params: { messages: [...], maxTokens: 100 } },
CreateMessageResultSchema
);
return { content: [{ type: 'text', text: 'done' }] };
});
```
**After (v2):**
```typescript
server.setRequestHandler('tools/call', async (request, ctx) => {
const samplingResult = await ctx.mcpReq.send(
{ method: 'sampling/createMessage', params: { messages: [...], maxTokens: 100 } }
);
return { content: [{ type: 'text', text: 'done' }] };
});
```
**`client.callTool()` — Before (v1):**
```typescript
import { CompatibilityCallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';
const result = await client.callTool(
{ name: 'my-tool', arguments: {} },
CompatibilityCallToolResultSchema
);
```
**After (v2):**
```typescript
const result = await client.callTool({ name: 'my-tool', arguments: {} });
```
The return type is now inferred from the method name via `ResultTypeMap`. For example, `client.request({ method: 'tools/call', ... })` returns `Promise<CallToolResult | CreateTaskResult>`.
### Client list methods return empty results for missing capabilities
`Client.listPrompts()`, `listResources()`, `listResourceTemplates()`, and `listTools()` now return empty results when the server didn't advertise the corresponding capability, instead of sending the request. This respects the MCP spec's capability negotiation.
@ -708,7 +775,7 @@ import { AjvJsonSchemaValidator, CfWorkerJsonSchemaValidator } from '@modelconte
The following APIs are unchanged between v1 and v2 (only the import paths changed):
- `Client` constructor and all client methods (`connect`, `callTool`, `listTools`, `listPrompts`, `listResources`, `readResource`, etc.)
- `Client` constructor and most client methods (`connect`, `listTools`, `listPrompts`, `listResources`, `readResource`, etc.) — note: `callTool()` signature changed (schema parameter removed)
- `McpServer` constructor, `server.connect(transport)`, `server.close()`
- `Server` (low-level) constructor and all methods
- `StreamableHTTPClientTransport`, `SSEClientTransport`, `StdioClientTransport` constructors and options

View file

@ -11,7 +11,6 @@
import type { Prompt, Resource, Tool } from '@modelcontextprotocol/client';
import {
applyMiddlewares,
CallToolResultSchema,
Client,
ClientCredentialsProvider,
createMiddleware,
@ -181,13 +180,16 @@ async function callTool_structuredOutput(client: Client) {
/** Example: Track progress of a long-running tool call. */
async function callTool_progress(client: Client) {
//#region callTool_progress
const result = await client.callTool({ name: 'long-operation', arguments: {} }, undefined, {
onprogress: ({ progress, total }) => {
console.log(`Progress: ${progress}/${total ?? '?'}`);
},
resetTimeoutOnProgress: true,
maxTotalTimeout: 600_000
});
const result = await client.callTool(
{ name: 'long-operation', arguments: {} },
{
onprogress: ({ progress, total }: { progress: number; total?: number }) => {
console.log(`Progress: ${progress}/${total ?? '?'}`);
},
resetTimeoutOnProgress: true,
maxTotalTimeout: 600_000
}
);
console.log(result.content);
//#endregion callTool_progress
}
@ -450,7 +452,6 @@ async function errorHandling_timeout(client: Client) {
try {
const result = await client.callTool(
{ name: 'slow-task', arguments: {} },
undefined,
{ timeout: 120_000 } // 2 minutes instead of the default 60 seconds
);
console.log(result.content);
@ -492,7 +493,6 @@ async function resumptionToken_basic(client: Client) {
method: 'tools/call',
params: { name: 'long-running-task', arguments: {} }
},
CallToolResultSchema,
{
resumptionToken: lastToken,
onresumptiontoken: (token: string) => {

View file

@ -10,6 +10,7 @@ import { createInterface } from 'node:readline';
import type {
CallToolRequest,
CallToolResult,
ElicitRequest,
ElicitRequestURLParams,
ElicitResult,
@ -18,10 +19,8 @@ import type {
ResourceLink
} from '@modelcontextprotocol/client';
import {
CallToolResultSchema,
Client,
getDisplayName,
ListToolsResultSchema,
ProtocolError,
ProtocolErrorCode,
StreamableHTTPClientTransport,
@ -666,7 +665,7 @@ async function listTools(): Promise<void> {
method: 'tools/list',
params: {}
};
const toolsResult = await client.request(toolsRequest, ListToolsResultSchema);
const toolsResult = await client.request(toolsRequest);
console.log('Available tools:');
if (toolsResult.tools.length === 0) {
@ -697,7 +696,7 @@ async function callTool(name: string, args: Record<string, unknown>): Promise<vo
};
console.log(`Calling tool '${name}' with args:`, args);
const result = await client.request(request, CallToolResultSchema);
const result = (await client.request(request)) as CallToolResult;
console.log('Tool result:');
const resourceLinks: ResourceLink[] = [];

View file

@ -1,5 +1,5 @@
import type { CallToolRequest, CallToolResult } from '@modelcontextprotocol/client';
import { CallToolResultSchema, Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
/**
* Multiple Clients MCP Example
@ -60,7 +60,7 @@ async function createAndRunClient(config: ClientConfig): Promise<{ id: string; r
}
};
const result = await client.request(toolRequest, CallToolResultSchema);
const result = (await client.request(toolRequest)) as CallToolResult;
console.log(`[${config.id}] Tool call completed`);
// Keep the connection open for a bit to receive notifications

View file

@ -1,5 +1,5 @@
import type { CallToolResult, ListToolsRequest } from '@modelcontextprotocol/client';
import { CallToolResultSchema, Client, ListToolsResultSchema, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
import type { CallToolRequest, CallToolResult, ListToolsRequest } from '@modelcontextprotocol/client';
import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
/**
* Parallel Tool Calls MCP Client
@ -86,7 +86,7 @@ async function listTools(client: Client): Promise<void> {
method: 'tools/list',
params: {}
};
const toolsResult = await client.request(toolsRequest, ListToolsResultSchema);
const toolsResult = await client.request(toolsRequest);
console.log('Available tools:');
if (toolsResult.tools.length === 0) {
@ -108,7 +108,7 @@ async function listTools(client: Client): Promise<void> {
async function startParallelNotificationTools(client: Client): Promise<Record<string, CallToolResult>> {
try {
// Define multiple tool calls with different configurations
const toolCalls = [
const toolCalls: Array<{ caller: string; request: CallToolRequest }> = [
{
caller: 'fast-notifier',
request: {
@ -159,7 +159,7 @@ async function startParallelNotificationTools(client: Client): Promise<Record<st
const toolPromises = toolCalls.map(({ caller, request }) => {
console.log(`Starting tool call for ${caller}...`);
return client
.request(request, CallToolResultSchema)
.request(request)
.then(result => ({ caller, result }))
.catch(error => {
console.error(`Error in tool call for ${caller}:`, error);
@ -173,7 +173,7 @@ async function startParallelNotificationTools(client: Client): Promise<Record<st
// Organize results by caller
const resultsByTool: Record<string, CallToolResult> = {};
for (const { caller, result } of results) {
resultsByTool[caller] = result;
resultsByTool[caller] = result as CallToolResult;
}
return resultsByTool;

View file

@ -4,14 +4,8 @@ import { createServer } from 'node:http';
import { createInterface } from 'node:readline';
import { URL } from 'node:url';
import type { CallToolRequest, ListToolsRequest, OAuthClientMetadata } from '@modelcontextprotocol/client';
import {
CallToolResultSchema,
Client,
ListToolsResultSchema,
StreamableHTTPClientTransport,
UnauthorizedError
} from '@modelcontextprotocol/client';
import type { CallToolRequest, CallToolResult, ListToolsRequest, OAuthClientMetadata } from '@modelcontextprotocol/client';
import { Client, StreamableHTTPClientTransport, UnauthorizedError } from '@modelcontextprotocol/client';
import open from 'open';
import { InMemoryOAuthClientProvider } from './simpleOAuthClientProvider.js';
@ -262,7 +256,7 @@ class InteractiveOAuthClient {
params: {}
};
const result = await this.client.request(request, ListToolsResultSchema);
const result = await this.client.request(request);
if (result.tools && result.tools.length > 0) {
console.log('\n📋 Available tools:');
@ -320,7 +314,7 @@ class InteractiveOAuthClient {
}
};
const result = await this.client.request(request, CallToolResultSchema);
const result = (await this.client.request(request)) as CallToolResult;
console.log(`\n🔧 Tool '${toolName}' result:`);
if (result.content) {
@ -404,7 +398,8 @@ class InteractiveOAuthClient {
case 'result': {
console.log('✓ Completed!');
for (const content of message.result.content) {
const toolResult = message.result as CallToolResult;
for (const content of toolResult.content) {
if (content.type === 'text') {
console.log(content.text);
} else {

View file

@ -2,6 +2,7 @@ import { createInterface } from 'node:readline';
import type {
CallToolRequest,
CallToolResult,
GetPromptRequest,
ListPromptsRequest,
ListResourcesRequest,
@ -10,17 +11,11 @@ import type {
ResourceLink
} from '@modelcontextprotocol/client';
import {
CallToolResultSchema,
Client,
getDisplayName,
GetPromptResultSchema,
InMemoryTaskStore,
ListPromptsResultSchema,
ListResourcesResultSchema,
ListToolsResultSchema,
ProtocolError,
ProtocolErrorCode,
ReadResourceResultSchema,
RELATED_TASK_META_KEY,
StreamableHTTPClientTransport
} from '@modelcontextprotocol/client';
@ -541,13 +536,10 @@ async function connect(url?: string): Promise<void> {
console.log('Client disconnected, cannot fetch resources');
return;
}
const resourcesResult = await client.request(
{
method: 'resources/list',
params: {}
},
ListResourcesResultSchema
);
const resourcesResult = await client.request({
method: 'resources/list',
params: {}
});
console.log('Available resources count:', resourcesResult.resources.length);
} catch {
console.log('Failed to list resources after change notification');
@ -632,7 +624,7 @@ async function listTools(): Promise<void> {
method: 'tools/list',
params: {}
};
const toolsResult = await client.request(toolsRequest, ListToolsResultSchema);
const toolsResult = await client.request(toolsRequest);
console.log('Available tools:');
if (toolsResult.tools.length === 0) {
@ -663,7 +655,7 @@ async function callTool(name: string, args: Record<string, unknown>): Promise<vo
};
console.log(`Calling tool '${name}' with args:`, args);
const result = await client.request(request, CallToolResultSchema);
const result = (await client.request(request)) as CallToolResult;
console.log('Tool result:');
const resourceLinks: ResourceLink[] = [];
@ -767,10 +759,10 @@ async function runNotificationsToolWithResumability(interval: number, count: num
console.log(`Updated resumption token: ${event}`);
};
const result = await client.request(request, CallToolResultSchema, {
const result = (await client.request(request, {
resumptionToken: notificationsToolLastEventId,
onresumptiontoken: onLastEventIdUpdate
});
})) as CallToolResult;
console.log('Tool result:');
for (const item of result.content) {
@ -796,7 +788,7 @@ async function listPrompts(): Promise<void> {
method: 'prompts/list',
params: {}
};
const promptsResult = await client.request(promptsRequest, ListPromptsResultSchema);
const promptsResult = await client.request(promptsRequest);
console.log('Available prompts:');
if (promptsResult.prompts.length === 0) {
console.log(' No prompts available');
@ -825,7 +817,7 @@ async function getPrompt(name: string, args: Record<string, unknown>): Promise<v
}
};
const promptResult = await client.request(promptRequest, GetPromptResultSchema);
const promptResult = await client.request(promptRequest);
console.log('Prompt template:');
for (const [index, msg] of promptResult.messages.entries()) {
console.log(` [${index + 1}] ${msg.role}: ${msg.content.type === 'text' ? msg.content.text : JSON.stringify(msg.content)}`);
@ -846,7 +838,7 @@ async function listResources(): Promise<void> {
method: 'resources/list',
params: {}
};
const resourcesResult = await client.request(resourcesRequest, ListResourcesResultSchema);
const resourcesResult = await client.request(resourcesRequest);
console.log('Available resources:');
if (resourcesResult.resources.length === 0) {
@ -874,7 +866,7 @@ async function readResource(uri: string): Promise<void> {
};
console.log(`Reading resource: ${uri}`);
const result = await client.request(request, ReadResourceResultSchema);
const result = await client.request(request);
console.log('Resource contents:');
for (const content of result.contents) {
@ -948,7 +940,8 @@ async function callToolTask(name: string, args: Record<string, unknown>): Promis
case 'result': {
console.log('Task completed!');
console.log('Tool result:');
for (const item of message.result.content) {
const toolResult = message.result as CallToolResult;
for (const item of toolResult.content) {
if (item.type === 'text') {
console.log(` ${item.text}`);
}

View file

@ -9,7 +9,7 @@
import { createInterface } from 'node:readline';
import type { CreateMessageRequest, CreateMessageResult, TextContent } from '@modelcontextprotocol/client';
import type { CallToolResult, CreateMessageRequest, CreateMessageResult, TextContent } from '@modelcontextprotocol/client';
import { Client, ProtocolError, ProtocolErrorCode, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
// Create readline interface for user input
@ -135,7 +135,8 @@ async function run(url: string): Promise<void> {
break;
}
case 'result': {
console.log(`Result: ${getTextContent(message.result)}`);
const toolResult = message.result as CallToolResult;
console.log(`Result: ${getTextContent(toolResult)}`);
break;
}
case 'error': {
@ -165,7 +166,8 @@ async function run(url: string): Promise<void> {
break;
}
case 'result': {
console.log(`Result:\n${getTextContent(message.result)}`);
const toolResult = message.result as CallToolResult;
console.log(`Result:\n${getTextContent(toolResult)}`);
break;
}
case 'error': {

View file

@ -12,7 +12,7 @@
* Run with: pnpm tsx src/ssePollingClient.ts
* Requires: ssePollingExample.ts server running on port 3001
*/
import { CallToolResultSchema, Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
const SERVER_URL = 'http://localhost:3001/mcp';
@ -78,7 +78,6 @@ async function main(): Promise<void> {
arguments: {}
}
},
CallToolResultSchema,
{
// Track resumption tokens for debugging
onresumptiontoken: token => {

View file

@ -1,11 +1,5 @@
import type { CallToolRequest, ListToolsRequest } from '@modelcontextprotocol/client';
import {
CallToolResultSchema,
Client,
ListToolsResultSchema,
SSEClientTransport,
StreamableHTTPClientTransport
} from '@modelcontextprotocol/client';
import type { CallToolRequest, CallToolResult, ListToolsRequest } from '@modelcontextprotocol/client';
import { Client, SSEClientTransport, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
/**
* Simplified Backwards Compatible MCP Client
@ -135,7 +129,7 @@ async function listTools(client: Client): Promise<void> {
method: 'tools/list',
params: {}
};
const toolsResult = await client.request(toolsRequest, ListToolsResultSchema);
const toolsResult = await client.request(toolsRequest);
console.log('Available tools:');
if (toolsResult.tools.length === 0) {
@ -168,7 +162,7 @@ async function startNotificationTool(client: Client): Promise<void> {
};
console.log('Calling notification tool...');
const result = await client.request(request, CallToolResultSchema);
const result = (await client.request(request)) as CallToolResult;
console.log('Tool result:');
for (const item of result.content) {

View file

@ -16,13 +16,7 @@ import type {
ReadResourceResult,
ResourceLink
} from '@modelcontextprotocol/server';
import {
ElicitResultSchema,
InMemoryTaskMessageQueue,
InMemoryTaskStore,
isInitializeRequest,
McpServer
} from '@modelcontextprotocol/server';
import { InMemoryTaskMessageQueue, InMemoryTaskStore, isInitializeRequest, McpServer } from '@modelcontextprotocol/server';
import cors from 'cors';
import type { Request, Response } from 'express';
import * as z from 'zod/v4';
@ -222,17 +216,14 @@ const getServer = () => {
try {
// Use sendRequest through the ctx parameter to elicit input
const result = await ctx.mcpReq.send(
{
method: 'elicitation/create',
params: {
mode: 'form',
message,
requestedSchema
}
},
ElicitResultSchema
);
const result = await ctx.mcpReq.send({
method: 'elicitation/create',
params: {
mode: 'form',
message,
requestedSchema
}
});
if (result.action === 'accept') {
return {

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

View file

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

View file

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

View file

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

View file

@ -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>;
}
/**

Some files were not shown because too many files have changed in this diff Show more