[SEP-2663] refactor!: remove 2025-11 experimental tasks (#2128)

This commit is contained in:
Felix Weinberger 2026-06-02 13:04:18 +01:00 • committed by GitHub
commit c8d7401b46
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1,7 +1,6 @@
import { createInterface } from 'node:readline';
import type {
CallToolResult,
GetPromptRequest,
ListPromptsRequest,
ListResourcesRequest,
@ -9,15 +8,7 @@ import type {
ReadResourceRequest,
ResourceLink
} from '@modelcontextprotocol/client';
import {
Client,
getDisplayName,
InMemoryTaskStore,
ProtocolError,
ProtocolErrorCode,
RELATED_TASK_META_KEY,
StreamableHTTPClientTransport
} from '@modelcontextprotocol/client';
import { Client, getDisplayName, ProtocolError, ProtocolErrorCode, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
import { Ajv } from 'ajv';
// Create readline interface for user input
@ -56,11 +47,9 @@ function printHelp(): void {
console.log(' reconnect - Reconnect to the server');
console.log(' list-tools - List available tools');
console.log(' call-tool <name> [args] - Call a tool with optional JSON arguments');
console.log(' call-tool-task <name> [args] - Call a tool with task-based execution (example: call-tool-task delay {"duration":3000})');
console.log(' greet [name] - Call the greet tool');
console.log(' multi-greet [name] - Call the multi-greet tool with notifications');
console.log(' collect-info [type] - Test form elicitation with collect-user-info tool (contact/preferences/feedback)');
console.log(' collect-info-task [type] - Test bidirectional task support (server+client tasks) with elicitation');
console.log(' start-notifications [interval] [count] - Start periodic notifications');
console.log(' run-notifications-tool-with-resumability [interval] [count] - Run notification tool with resumability');
console.log(' list-prompts - List available prompts');
@ -135,12 +124,6 @@ function commandLoop(): void {
await callCollectInfoTool(args[1] || 'contact');
break;
}
case 'collect-info-task': {
await callCollectInfoWithTask(args[1] || 'contact');
break;
}
case 'start-notifications': {
const interval = args[1] ? Number.parseInt(args[1], 10) : 2000;
const count = args[2] ? Number.parseInt(args[2], 10) : 10;
@ -154,25 +137,6 @@ function commandLoop(): void {
await runNotificationsToolWithResumability(interval, count);
break;
}
case 'call-tool-task': {
if (args.length < 2) {
console.log('Usage: call-tool-task <name> [args]');
} else {
const toolName = args[1]!;
let toolArgs = {};
if (args.length > 2) {
try {
toolArgs = JSON.parse(args.slice(2).join(' '));
} catch {
console.log('Invalid JSON arguments. Using empty args.');
}
}
await callToolTask(toolName, toolArgs);
}
break;
}
case 'list-prompts': {
await listPrompts();
break;
@ -250,10 +214,7 @@ async function connect(url?: string): Promise<void> {
console.log(`Connecting to ${serverUrl}...`);
try {
// Create task store for client-side task support
const clientTaskStore = new InMemoryTaskStore();
// Create a new client with form elicitation capability and task support
// Create a new client with form elicitation capability
client = new Client(
{
name: 'example-client',
@ -263,14 +224,6 @@ async function connect(url?: string): Promise<void> {
capabilities: {
elicitation: {
form: {}
},
tasks: {
taskStore: clientTaskStore,
requests: {
elicitation: {
create: {}
}
}
}
}
}
@ -279,33 +232,16 @@ async function connect(url?: string): Promise<void> {
console.error('\u001B[31mClient error:', error, '\u001B[0m');
};
// Set up elicitation request handler with proper validation and task support
client.setRequestHandler('elicitation/create', async (request, extra) => {
// Set up elicitation request handler with proper validation
client.setRequestHandler('elicitation/create', async request => {
if (request.params.mode !== 'form') {
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Unsupported elicitation mode: ${request.params.mode}`);
}
console.log('\n🔔 Elicitation (form) Request Received:');
console.log(`Message: ${request.params.message}`);
console.log(`Related Task: ${request.params._meta?.[RELATED_TASK_META_KEY]?.taskId}`);
console.log(`Task Creation Requested: ${request.params.task ? 'yes' : 'no'}`);
console.log('Requested Schema:');
console.log(JSON.stringify(request.params.requestedSchema, null, 2));
// Helper to return result, optionally creating a task if requested
const returnResult = async (result: {
action: 'accept' | 'decline' | 'cancel';
content?: Record<string, string | number | boolean | string[]>;
}) => {
if (request.params.task && extra.task?.store) {
// Create a task and store the result
const task = await extra.task.store.createTask({ ttl: extra.task.requestedTtl });
await extra.task.store.storeTaskResult(task.taskId, 'completed', result);
console.log(`📋 Created client-side task: ${task.taskId}`);
return { task };
}
return result;
};
const schema = request.params.requestedSchema;
const properties = schema.properties;
const required = schema.required || [];
@ -439,7 +375,7 @@ async function connect(url?: string): Promise<void> {
}
if (inputCancelled) {
return returnResult({ action: 'cancel' });
return { action: 'cancel' };
}
// If we didn't complete all fields due to an error, try again
@ -452,7 +388,7 @@ async function connect(url?: string): Promise<void> {
continue;
} else {
console.log('Maximum attempts reached. Declining request.');
return returnResult({ action: 'decline' });
return { action: 'decline' };
}
}
@ -471,7 +407,7 @@ async function connect(url?: string): Promise<void> {
continue;
} else {
console.log('Maximum attempts reached. Declining request.');
return returnResult({ action: 'decline' });
return { action: 'decline' };
}
}
@ -488,14 +424,14 @@ async function connect(url?: string): Promise<void> {
switch (confirmAnswer) {
case 'yes':
case 'y': {
return returnResult({
return {
action: 'accept',
content
});
};
}
case 'cancel':
case 'c': {
return returnResult({ action: 'cancel' });
return { action: 'cancel' };
}
case 'no':
case 'n': {
@ -503,7 +439,7 @@ async function connect(url?: string): Promise<void> {
console.log('Please re-enter the information...');
continue;
} else {
return returnResult({ action: 'decline' });
return { action: 'decline' };
}
break;
@ -513,7 +449,7 @@ async function connect(url?: string): Promise<void> {
}
console.log('Maximum attempts reached. Declining request.');
return returnResult({ action: 'decline' });
return { action: 'decline' };
});
transport = new StreamableHTTPClientTransport(new URL(serverUrl), {
@ -716,12 +652,6 @@ async function callCollectInfoTool(infoType: string): Promise<void> {
await callTool('collect-user-info', { infoType });
}
async function callCollectInfoWithTask(infoType: string): Promise<void> {
console.log(`\n🔄 Testing bidirectional task support with collect-user-info-task tool (${infoType})...`);
console.log('This will create a task on the server, which will elicit input and create a task on the client.\n');
await callToolTask('collect-user-info-task', { infoType });
}
async function startNotifications(interval: number, count: number): Promise<void> {
console.log(`Starting notification stream: interval=${interval}ms, count=${count || 'unlimited'}`);
await callTool('start-notification-stream', { interval, count });
@ -880,70 +810,6 @@ async function readResource(uri: string): Promise<void> {
}
}
async function callToolTask(name: string, args: Record<string, unknown>): Promise<void> {
if (!client) {
console.log('Not connected to server.');
return;
}
console.log(`Calling tool '${name}' with task-based execution...`);
console.log('Arguments:', args);
// Use task-based execution - call now, fetch later
// Using the experimental tasks API - WARNING: may change without notice
console.log('This will return immediately while processing continues in the background...');
try {
// Call the tool with task metadata using streaming API
const stream = client.experimental.tasks.callToolStream(
{
name,
arguments: args
},
{
task: {
ttl: 60_000 // Keep results for 60 seconds
}
}
);
console.log('Waiting for task completion...');
let lastStatus = '';
for await (const message of stream) {
switch (message.type) {
case 'taskCreated': {
console.log('Task created successfully with ID:', message.task.taskId);
break;
}
case 'taskStatus': {
if (lastStatus !== message.task.status) {
console.log(` ${message.task.status}${message.task.statusMessage ? ` - ${message.task.statusMessage}` : ''}`);
}
lastStatus = message.task.status;
break;
}
case 'result': {
console.log('Task completed!');
console.log('Tool result:');
const toolResult = message.result as CallToolResult;
for (const item of toolResult.content) {
if (item.type === 'text') {
console.log(` ${item.text}`);
}
}
break;
}
case 'error': {
throw message.error;
}
}
}
} catch (error) {
console.log(`Error with task-based execution: ${error}`);
}
}
async function cleanup(): Promise<void> {
if (client && transport) {
try {