Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
204 changes: 204 additions & 0 deletions src/examples/client/simpleTaskInteractiveClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
/**
* 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 { Client } from '../../client/index.js';
import { StreamableHTTPClientTransport } from '../../client/streamableHttp.js';
import { createInterface } from 'node:readline';
import {
CallToolResultSchema,
TextContent,
ElicitRequestSchema,
CreateMessageRequestSchema,
CreateMessageRequest,
CreateMessageResult,
ErrorCode,
McpError
} from '../../types.js';

// 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: string; content?: Record<string, unknown> }> {
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(ElicitRequestSchema, async request => {
if (request.params.mode && request.params.mode !== 'form') {
throw new McpError(ErrorCode.InvalidParams, `Unsupported elicitation mode: ${request.params.mode}`);
}
return elicitationCallback(request.params);
});

// Set up sampling request handler
client.setRequestHandler(CreateMessageRequestSchema, 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' } },
CallToolResultSchema,
{ task: { ttl: 60000 } }
);

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':
console.log(`Result: ${getTextContent(message.result)}`);
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' } },
CallToolResultSchema,
{
task: { ttl: 60000 }
}
);

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':
console.log(`Result:\n${getTextContent(message.result)}`);
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
run(url).catch(error => {
console.error('Error running client:', error);
process.exit(1);
});
161 changes: 161 additions & 0 deletions src/examples/server/README-simpleTaskInteractive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
# Simple Task Interactive Example

This example demonstrates the MCP Tasks message queue pattern with interactive server-to-client requests (elicitation and sampling).

## Overview

The example consists of two components:

1. **Server** (`simpleTaskInteractive.ts`) - Exposes two task-based tools that require client interaction:
- `confirm_delete` - Uses elicitation to ask the user for confirmation before "deleting" a file
- `write_haiku` - Uses sampling to request an LLM to generate a haiku on a topic

2. **Client** (`simpleTaskInteractiveClient.ts`) - Connects to the server and handles:
- Elicitation requests with simple y/n terminal prompts
- Sampling requests with a mock haiku generator

## Key Concepts

### Task-Based Execution

Both tools use `execution.taskSupport: 'required'`, meaning they follow the "call-now, fetch-later" pattern:

1. Client calls tool with `task: { ttl: 60000 }` parameter
2. Server creates a task and returns `CreateTaskResult` immediately
3. Client polls via `tasks/result` to get the final result
4. Server sends elicitation/sampling requests through the task message queue
5. Client handles requests and returns responses
6. Server completes the task with the final result

### Message Queue Pattern

When a tool needs to interact with the client (elicitation or sampling), it:

1. Updates task status to `input_required`
2. Enqueues the request in the task message queue
3. Waits for the response via a Resolver
4. Updates task status back to `working`
5. Continues processing

The `TaskResultHandler` dequeues messages when the client calls `tasks/result` and routes responses back to waiting Resolvers.

## Running the Example

### Start the Server

```bash
# From the SDK root directory
npx tsx src/examples/server/simpleTaskInteractive.ts

# Or with a custom port
PORT=9000 npx tsx src/examples/server/simpleTaskInteractive.ts
```

The server will start on http://localhost:8000/mcp (or your custom port).

### Run the Client

```bash
# From the SDK root directory
npx tsx src/examples/client/simpleTaskInteractiveClient.ts

# Or connect to a different server
npx tsx src/examples/client/simpleTaskInteractiveClient.ts --url http://localhost:9000/mcp
```

## Expected Output

### Server Output

```
Starting server on http://localhost:8000/mcp

Available tools:
- confirm_delete: Demonstrates elicitation (asks user y/n)
- write_haiku: Demonstrates sampling (requests LLM completion)

[Server] confirm_delete called, task created: task-abc123
[Server] confirm_delete: asking about 'important.txt'
[Server] Sending elicitation request to client...
[Server] tasks/result called for task task-abc123
[Server] Delivering queued request message for task task-abc123
[Server] Received elicitation response: action=accept, content={"confirm":true}
[Server] Completing task with result: Deleted 'important.txt'

[Server] write_haiku called, task created: task-def456
[Server] write_haiku: topic 'autumn leaves'
[Server] Sending sampling request to client...
[Server] tasks/result called for task task-def456
[Server] Delivering queued request message for task task-def456
[Server] Received sampling response: Cherry blossoms fall...
[Server] Completing task with haiku
```

### Client Output

```
Simple Task Interactive Client
==============================
Connecting to http://localhost:8000/mcp...
Connected!

Available tools: confirm_delete, write_haiku

--- Demo 1: Elicitation ---
Calling confirm_delete tool...
Task created: task-abc123
Task status: working

[Elicitation] Server asks: Are you sure you want to delete 'important.txt'?
Your response (y/n): y
[Elicitation] Responding with: confirm=true
Task status: input_required
Task status: completed
Result: Deleted 'important.txt'

--- Demo 2: Sampling ---
Calling write_haiku tool...
Task created: task-def456
Task status: working

[Sampling] Server requests LLM completion for: Write a haiku about autumn leaves
[Sampling] Responding with haiku
Task status: input_required
Task status: completed
Result:
Haiku:
Cherry blossoms fall
Softly on the quiet pond
Spring whispers goodbye

Demo complete. Closing connection...
```

## Implementation Details

### Server Components

- **Resolver**: Promise-like class for passing results between async operations
- **TaskMessageQueueWithResolvers**: Extended message queue that tracks pending requests with their Resolvers
- **TaskStoreWithNotifications**: Extended task store with notification support for status changes
- **TaskResultHandler**: Handles `tasks/result` requests by dequeuing messages and routing responses
- **TaskSession**: Wraps the server to enqueue requests during task execution

### Client Capabilities

The client declares these capabilities during initialization:

```typescript
capabilities: {
elicitation: { form: {} },
sampling: {}
}
```

This tells the server that the client can handle both form-based elicitation and sampling requests.

## Related Files

- `src/shared/task.ts` - Core task interfaces (TaskStore, TaskMessageQueue)
- `src/examples/shared/inMemoryTaskStore.ts` - In-memory implementations
- `src/types.ts` - Task-related types (Task, CreateTaskResult, GetTaskRequestSchema, etc.)
Loading
Loading