[SEP-2663] refactor!: remove 2025-11 experimental tasks (#2128)
This commit is contained in:
parent
600ba75f34
commit
c8d7401b46
6 changed files with 423 additions and 18280 deletions
|
|
@ -24,18 +24,17 @@ Most clients expect a server to be running. Start one from [`../server/README.md
|
|||
|
||||
## Example index
|
||||
|
||||
| Scenario | Description | File |
|
||||
| --------------------------------------------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
|
||||
| Interactive Streamable HTTP client | CLI client that exercises tools/resources/prompts, notifications, elicitation, and tasks. | [`src/simpleStreamableHttp.ts`](src/simpleStreamableHttp.ts) |
|
||||
| Backwards-compatible client (Streamable HTTP → SSE) | Tries Streamable HTTP first, falls back to legacy SSE on 4xx responses. | [`src/streamableHttpWithSseFallbackClient.ts`](src/streamableHttpWithSseFallbackClient.ts) |
|
||||
| SSE polling client (legacy) | Polls a legacy HTTP+SSE server and demonstrates notification handling. | [`src/ssePollingClient.ts`](src/ssePollingClient.ts) |
|
||||
| Parallel tool calls | Runs multiple tool calls in parallel. | [`src/parallelToolCallsClient.ts`](src/parallelToolCallsClient.ts) |
|
||||
| Multiple clients in parallel | Connects multiple clients concurrently to the same server. | [`src/multipleClientsParallel.ts`](src/multipleClientsParallel.ts) |
|
||||
| OAuth client (interactive) | OAuth-enabled client (dynamic registration, auth flow). | [`src/simpleOAuthClient.ts`](src/simpleOAuthClient.ts) |
|
||||
| OAuth provider helper | Demonstrates reusable OAuth providers. | [`src/simpleOAuthClientProvider.ts`](src/simpleOAuthClientProvider.ts) |
|
||||
| Client credentials (M2M) | Machine-to-machine OAuth client credentials example. | [`src/simpleClientCredentials.ts`](src/simpleClientCredentials.ts) |
|
||||
| URL elicitation client | Drives URL-mode elicitation flows (sensitive input in a browser). | [`src/elicitationUrlExample.ts`](src/elicitationUrlExample.ts) |
|
||||
| Task interactive client | Demonstrates task-based execution + interactive server→client requests. | [`src/simpleTaskInteractiveClient.ts`](src/simpleTaskInteractiveClient.ts) |
|
||||
| Scenario | Description | File |
|
||||
| --------------------------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
|
||||
| Interactive Streamable HTTP client | CLI client that exercises tools/resources/prompts, notifications, and elicitation. | [`src/simpleStreamableHttp.ts`](src/simpleStreamableHttp.ts) |
|
||||
| Backwards-compatible client (Streamable HTTP → SSE) | Tries Streamable HTTP first, falls back to legacy SSE on 4xx responses. | [`src/streamableHttpWithSseFallbackClient.ts`](src/streamableHttpWithSseFallbackClient.ts) |
|
||||
| SSE polling client (legacy) | Polls a legacy HTTP+SSE server and demonstrates notification handling. | [`src/ssePollingClient.ts`](src/ssePollingClient.ts) |
|
||||
| Parallel tool calls | Runs multiple tool calls in parallel. | [`src/parallelToolCallsClient.ts`](src/parallelToolCallsClient.ts) |
|
||||
| Multiple clients in parallel | Connects multiple clients concurrently to the same server. | [`src/multipleClientsParallel.ts`](src/multipleClientsParallel.ts) |
|
||||
| OAuth client (interactive) | OAuth-enabled client (dynamic registration, auth flow). | [`src/simpleOAuthClient.ts`](src/simpleOAuthClient.ts) |
|
||||
| OAuth provider helper | Demonstrates reusable OAuth providers. | [`src/simpleOAuthClientProvider.ts`](src/simpleOAuthClientProvider.ts) |
|
||||
| Client credentials (M2M) | Machine-to-machine OAuth client credentials example. | [`src/simpleClientCredentials.ts`](src/simpleClientCredentials.ts) |
|
||||
| URL elicitation client | Drives URL-mode elicitation flows (sensitive input in a browser). | [`src/elicitationUrlExample.ts`](src/elicitationUrlExample.ts) |
|
||||
|
||||
## URL elicitation example (server + client)
|
||||
|
||||
|
|
|
|||
|
|
@ -490,7 +490,7 @@ async function errorHandling_timeout(client: Client) {
|
|||
//#region errorHandling_timeout
|
||||
try {
|
||||
const result = await client.callTool(
|
||||
{ name: 'slow-task', arguments: {} },
|
||||
{ name: 'slow-operation', arguments: {} },
|
||||
{ timeout: 120_000 } // 2 minutes instead of the default 60 seconds
|
||||
);
|
||||
console.log(result.content);
|
||||
|
|
@ -530,7 +530,7 @@ async function resumptionToken_basic(client: Client) {
|
|||
const result = await client.request(
|
||||
{
|
||||
method: 'tools/call',
|
||||
params: { name: 'long-running-task', arguments: {} }
|
||||
params: { name: 'long-running-operation', arguments: {} }
|
||||
},
|
||||
{
|
||||
resumptionToken: lastToken,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { createServer } from 'node:http';
|
|||
import { createInterface } from 'node:readline';
|
||||
import { URL } from 'node:url';
|
||||
|
||||
import type { CallToolResult, ListToolsRequest, OAuthClientMetadata } from '@modelcontextprotocol/client';
|
||||
import type { ListToolsRequest, OAuthClientMetadata } from '@modelcontextprotocol/client';
|
||||
import { Client, StreamableHTTPClientTransport, UnauthorizedError } from '@modelcontextprotocol/client';
|
||||
import open from 'open';
|
||||
|
||||
|
|
@ -209,7 +209,7 @@ class InteractiveOAuthClient {
|
|||
console.log('Commands:');
|
||||
console.log(' list - List available tools');
|
||||
console.log(' call <tool_name> [args] - Call a tool');
|
||||
console.log(' stream <tool_name> [args] - Call a tool with streaming (shows task status)');
|
||||
console.log(' stream <tool_name> [args] - (disabled; returns when the SEP-2663 tasks extension lands)');
|
||||
console.log(' quit - Exit the client');
|
||||
console.log();
|
||||
|
||||
|
|
@ -232,7 +232,7 @@ class InteractiveOAuthClient {
|
|||
} else if (command.startsWith('stream ')) {
|
||||
await this.handleStreamTool(command);
|
||||
} else {
|
||||
console.log("❌ Unknown command. Try 'list', 'call <tool_name>', 'stream <tool_name>', or 'quit'");
|
||||
console.log("❌ Unknown command. Try 'list', 'call <tool_name>', or 'quit'");
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message === 'SIGINT') {
|
||||
|
|
@ -358,62 +358,11 @@ class InteractiveOAuthClient {
|
|||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Using the experimental tasks API - WARNING: may change without notice
|
||||
console.log(`\n🔧 Streaming tool '${toolName}'...`);
|
||||
|
||||
const stream = this.client.experimental.tasks.callToolStream(
|
||||
{
|
||||
name: toolName,
|
||||
arguments: toolArgs
|
||||
},
|
||||
{
|
||||
task: {
|
||||
taskId: `task-${Date.now()}`,
|
||||
ttl: 60_000
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Iterate through all messages yielded by the generator
|
||||
for await (const message of stream) {
|
||||
switch (message.type) {
|
||||
case 'taskCreated': {
|
||||
console.log(`✓ Task created: ${message.task.taskId}`);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'taskStatus': {
|
||||
console.log(`⟳ Status: ${message.task.status}`);
|
||||
if (message.task.statusMessage) {
|
||||
console.log(` ${message.task.statusMessage}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'result': {
|
||||
console.log('✓ Completed!');
|
||||
const toolResult = message.result as CallToolResult;
|
||||
for (const content of toolResult.content) {
|
||||
if (content.type === 'text') {
|
||||
console.log(content.text);
|
||||
} else {
|
||||
console.log(content);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'error': {
|
||||
console.log('✗ Error:');
|
||||
console.log(` ${message.error.message}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to stream tool '${toolName}':`, error);
|
||||
}
|
||||
// The streaming-tool demo (callToolStream) was removed with the 2025-11
|
||||
// experimental tasks (SEP-2663); it returns when the tasks extension lands.
|
||||
void toolName;
|
||||
void toolArgs;
|
||||
console.log('Streaming tool demo removed with the 2025-11 experimental tasks (SEP-2663); returns when the tasks extension lands.');
|
||||
}
|
||||
|
||||
close(): void {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -1,204 +0,0 @@
|
|||
/**
|
||||
* Simple interactive task client demonstrating elicitation and sampling responses.
|
||||
*
|
||||
* This client connects to simpleTaskInteractive.ts server and demonstrates:
|
||||
* - Handling elicitation requests (y/n confirmation)
|
||||
* - Handling sampling requests (returns a hardcoded haiku)
|
||||
* - Using task-based tool execution with streaming
|
||||
*/
|
||||
|
||||
import { createInterface } from 'node:readline';
|
||||
|
||||
import type { CallToolResult, CreateMessageRequest, CreateMessageResult, TextContent } from '@modelcontextprotocol/client';
|
||||
import { Client, ProtocolError, ProtocolErrorCode, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
|
||||
|
||||
// Create readline interface for user input
|
||||
const readline = createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
function question(prompt: string): Promise<string> {
|
||||
return new Promise(resolve => {
|
||||
readline.question(prompt, answer => {
|
||||
resolve(answer.trim());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getTextContent(result: { content: Array<{ type: string; text?: string }> }): string {
|
||||
const textContent = result.content.find((c): c is TextContent => c.type === 'text');
|
||||
return textContent?.text ?? '(no text)';
|
||||
}
|
||||
|
||||
async function elicitationCallback(params: {
|
||||
mode?: string;
|
||||
message: string;
|
||||
requestedSchema?: object;
|
||||
}): Promise<{ action: 'accept' | 'cancel' | 'decline'; content?: Record<string, string | number | boolean | string[]> }> {
|
||||
console.log(`\n[Elicitation] Server asks: ${params.message}`);
|
||||
|
||||
// Simple terminal prompt for y/n
|
||||
const response = await question('Your response (y/n): ');
|
||||
const confirmed = ['y', 'yes', 'true', '1'].includes(response.toLowerCase());
|
||||
|
||||
console.log(`[Elicitation] Responding with: confirm=${confirmed}`);
|
||||
return { action: 'accept', content: { confirm: confirmed } };
|
||||
}
|
||||
|
||||
async function samplingCallback(params: CreateMessageRequest['params']): Promise<CreateMessageResult> {
|
||||
// Get the prompt from the first message
|
||||
let prompt = 'unknown';
|
||||
if (params.messages && params.messages.length > 0) {
|
||||
const firstMessage = params.messages[0]!;
|
||||
const content = firstMessage.content;
|
||||
if (typeof content === 'object' && !Array.isArray(content) && content.type === 'text' && 'text' in content) {
|
||||
prompt = content.text;
|
||||
} else if (Array.isArray(content)) {
|
||||
const textPart = content.find(c => c.type === 'text' && 'text' in c);
|
||||
if (textPart && 'text' in textPart) {
|
||||
prompt = textPart.text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n[Sampling] Server requests LLM completion for: ${prompt}`);
|
||||
|
||||
// Return a hardcoded haiku (in real use, call your LLM here)
|
||||
const haiku = `Cherry blossoms fall
|
||||
Softly on the quiet pond
|
||||
Spring whispers goodbye`;
|
||||
|
||||
console.log('[Sampling] Responding with haiku');
|
||||
return {
|
||||
model: 'mock-haiku-model',
|
||||
role: 'assistant',
|
||||
content: { type: 'text', text: haiku }
|
||||
};
|
||||
}
|
||||
|
||||
async function run(url: string): Promise<void> {
|
||||
console.log('Simple Task Interactive Client');
|
||||
console.log('==============================');
|
||||
console.log(`Connecting to ${url}...`);
|
||||
|
||||
// Create client with elicitation and sampling capabilities
|
||||
const client = new Client(
|
||||
{ name: 'simple-task-interactive-client', version: '1.0.0' },
|
||||
{
|
||||
capabilities: {
|
||||
elicitation: { form: {} },
|
||||
sampling: {}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Set up elicitation request handler
|
||||
client.setRequestHandler('elicitation/create', async request => {
|
||||
if (request.params.mode && request.params.mode !== 'form') {
|
||||
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Unsupported elicitation mode: ${request.params.mode}`);
|
||||
}
|
||||
return elicitationCallback(request.params);
|
||||
});
|
||||
|
||||
// Set up sampling request handler
|
||||
client.setRequestHandler('sampling/createMessage', async request => {
|
||||
return samplingCallback(request.params) as unknown as ReturnType<typeof samplingCallback>;
|
||||
});
|
||||
|
||||
// Connect to server
|
||||
const transport = new StreamableHTTPClientTransport(new URL(url));
|
||||
await client.connect(transport);
|
||||
console.log('Connected!\n');
|
||||
|
||||
// List tools
|
||||
const toolsResult = await client.listTools();
|
||||
console.log(`Available tools: ${toolsResult.tools.map(t => t.name).join(', ')}`);
|
||||
|
||||
// Demo 1: Elicitation (confirm_delete)
|
||||
console.log('\n--- Demo 1: Elicitation ---');
|
||||
console.log('Calling confirm_delete tool...');
|
||||
|
||||
const confirmStream = client.experimental.tasks.callToolStream(
|
||||
{ name: 'confirm_delete', arguments: { filename: 'important.txt' } },
|
||||
{ task: { ttl: 60_000 } }
|
||||
);
|
||||
|
||||
for await (const message of confirmStream) {
|
||||
switch (message.type) {
|
||||
case 'taskCreated': {
|
||||
console.log(`Task created: ${message.task.taskId}`);
|
||||
break;
|
||||
}
|
||||
case 'taskStatus': {
|
||||
console.log(`Task status: ${message.task.status}`);
|
||||
break;
|
||||
}
|
||||
case 'result': {
|
||||
const toolResult = message.result as CallToolResult;
|
||||
console.log(`Result: ${getTextContent(toolResult)}`);
|
||||
break;
|
||||
}
|
||||
case 'error': {
|
||||
console.error(`Error: ${message.error}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Demo 2: Sampling (write_haiku)
|
||||
console.log('\n--- Demo 2: Sampling ---');
|
||||
console.log('Calling write_haiku tool...');
|
||||
|
||||
const haikuStream = client.experimental.tasks.callToolStream(
|
||||
{ name: 'write_haiku', arguments: { topic: 'autumn leaves' } },
|
||||
{ task: { ttl: 60_000 } }
|
||||
);
|
||||
|
||||
for await (const message of haikuStream) {
|
||||
switch (message.type) {
|
||||
case 'taskCreated': {
|
||||
console.log(`Task created: ${message.task.taskId}`);
|
||||
break;
|
||||
}
|
||||
case 'taskStatus': {
|
||||
console.log(`Task status: ${message.task.status}`);
|
||||
break;
|
||||
}
|
||||
case 'result': {
|
||||
const toolResult = message.result as CallToolResult;
|
||||
console.log(`Result:\n${getTextContent(toolResult)}`);
|
||||
break;
|
||||
}
|
||||
case 'error': {
|
||||
console.error(`Error: ${message.error}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
console.log('\nDemo complete. Closing connection...');
|
||||
await transport.close();
|
||||
readline.close();
|
||||
}
|
||||
|
||||
// Parse command line arguments
|
||||
const args = process.argv.slice(2);
|
||||
let url = 'http://localhost:8000/mcp';
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i] === '--url' && args[i + 1]) {
|
||||
url = args[i + 1]!;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
// Run the client
|
||||
try {
|
||||
await run(url);
|
||||
} catch (error) {
|
||||
console.error('Error running client:', error);
|
||||
// eslint-disable-next-line unicorn/no-process-exit
|
||||
process.exit(1);
|
||||
}
|
||||
|
|
@ -65,16 +65,16 @@ async function main(): Promise<void> {
|
|||
console.log('[Client] Connected successfully');
|
||||
console.log('');
|
||||
|
||||
// Call the long-task tool
|
||||
console.log('[Client] Calling long-task tool...');
|
||||
console.log('[Client] Server will disconnect mid-task to demonstrate polling');
|
||||
// Call the long-operation tool
|
||||
console.log('[Client] Calling long-operation tool...');
|
||||
console.log('[Client] Server will disconnect mid-operation to demonstrate polling');
|
||||
console.log('');
|
||||
|
||||
const result = await client.request(
|
||||
{
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: 'long-task',
|
||||
name: 'long-operation',
|
||||
arguments: {}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue