Skip to content

Commit a64cee7

Browse files
feat: examples for interactive cases
1 parent b9538a2 commit a64cee7

File tree

3 files changed

+1109
-0
lines changed

3 files changed

+1109
-0
lines changed
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
/**
2+
* Simple interactive task client demonstrating elicitation and sampling responses.
3+
*
4+
* This client connects to simpleTaskInteractive.ts server and demonstrates:
5+
* - Handling elicitation requests (y/n confirmation)
6+
* - Handling sampling requests (returns a hardcoded haiku)
7+
* - Using task-based tool execution with streaming
8+
*/
9+
10+
import { Client } from '../../client/index.js';
11+
import { StreamableHTTPClientTransport } from '../../client/streamableHttp.js';
12+
import { createInterface } from 'node:readline';
13+
import {
14+
CallToolResultSchema,
15+
TextContent,
16+
ElicitRequestSchema,
17+
CreateMessageRequestSchema,
18+
CreateMessageRequest,
19+
CreateMessageResult,
20+
ErrorCode,
21+
McpError
22+
} from '../../types.js';
23+
24+
// Create readline interface for user input
25+
const readline = createInterface({
26+
input: process.stdin,
27+
output: process.stdout
28+
});
29+
30+
function question(prompt: string): Promise<string> {
31+
return new Promise(resolve => {
32+
readline.question(prompt, answer => {
33+
resolve(answer.trim());
34+
});
35+
});
36+
}
37+
38+
function getTextContent(result: { content: Array<{ type: string; text?: string }> }): string {
39+
const textContent = result.content.find((c): c is TextContent => c.type === 'text');
40+
return textContent?.text ?? '(no text)';
41+
}
42+
43+
async function elicitationCallback(params: {
44+
mode?: string;
45+
message: string;
46+
requestedSchema?: object;
47+
}): Promise<{ action: string; content?: Record<string, unknown> }> {
48+
console.log(`\n[Elicitation] Server asks: ${params.message}`);
49+
50+
// Simple terminal prompt for y/n
51+
const response = await question('Your response (y/n): ');
52+
const confirmed = ['y', 'yes', 'true', '1'].includes(response.toLowerCase());
53+
54+
console.log(`[Elicitation] Responding with: confirm=${confirmed}`);
55+
return { action: 'accept', content: { confirm: confirmed } };
56+
}
57+
58+
async function samplingCallback(params: CreateMessageRequest['params']): Promise<CreateMessageResult> {
59+
// Get the prompt from the first message
60+
let prompt = 'unknown';
61+
if (params.messages && params.messages.length > 0) {
62+
const firstMessage = params.messages[0];
63+
const content = firstMessage.content;
64+
if (typeof content === 'object' && !Array.isArray(content) && content.type === 'text' && 'text' in content) {
65+
prompt = content.text;
66+
} else if (Array.isArray(content)) {
67+
const textPart = content.find(c => c.type === 'text' && 'text' in c);
68+
if (textPart && 'text' in textPart) {
69+
prompt = textPart.text;
70+
}
71+
}
72+
}
73+
74+
console.log(`\n[Sampling] Server requests LLM completion for: ${prompt}`);
75+
76+
// Return a hardcoded haiku (in real use, call your LLM here)
77+
const haiku = `Cherry blossoms fall
78+
Softly on the quiet pond
79+
Spring whispers goodbye`;
80+
81+
console.log('[Sampling] Responding with haiku');
82+
return {
83+
model: 'mock-haiku-model',
84+
role: 'assistant',
85+
content: { type: 'text', text: haiku }
86+
};
87+
}
88+
89+
async function run(url: string): Promise<void> {
90+
console.log('Simple Task Interactive Client');
91+
console.log('==============================');
92+
console.log(`Connecting to ${url}...`);
93+
94+
// Create client with elicitation and sampling capabilities
95+
const client = new Client(
96+
{ name: 'simple-task-interactive-client', version: '1.0.0' },
97+
{
98+
capabilities: {
99+
elicitation: { form: {} },
100+
sampling: {}
101+
}
102+
}
103+
);
104+
105+
// Set up elicitation request handler
106+
client.setRequestHandler(ElicitRequestSchema, async request => {
107+
if (request.params.mode && request.params.mode !== 'form') {
108+
throw new McpError(ErrorCode.InvalidParams, `Unsupported elicitation mode: ${request.params.mode}`);
109+
}
110+
return elicitationCallback(request.params);
111+
});
112+
113+
// Set up sampling request handler
114+
client.setRequestHandler(CreateMessageRequestSchema, async request => {
115+
return samplingCallback(request.params) as unknown as ReturnType<typeof samplingCallback>;
116+
});
117+
118+
// Connect to server
119+
const transport = new StreamableHTTPClientTransport(new URL(url));
120+
await client.connect(transport);
121+
console.log('Connected!\n');
122+
123+
// List tools
124+
const toolsResult = await client.listTools();
125+
console.log(`Available tools: ${toolsResult.tools.map(t => t.name).join(', ')}`);
126+
127+
// Demo 1: Elicitation (confirm_delete)
128+
console.log('\n--- Demo 1: Elicitation ---');
129+
console.log('Calling confirm_delete tool...');
130+
131+
const confirmStream = client.experimental.tasks.callToolStream(
132+
{ name: 'confirm_delete', arguments: { filename: 'important.txt' } },
133+
CallToolResultSchema,
134+
{ task: { ttl: 60000 } }
135+
);
136+
137+
for await (const message of confirmStream) {
138+
switch (message.type) {
139+
case 'taskCreated':
140+
console.log(`Task created: ${message.task.taskId}`);
141+
break;
142+
case 'taskStatus':
143+
console.log(`Task status: ${message.task.status}`);
144+
break;
145+
case 'result':
146+
console.log(`Result: ${getTextContent(message.result)}`);
147+
break;
148+
case 'error':
149+
console.error(`Error: ${message.error}`);
150+
break;
151+
}
152+
}
153+
154+
// Demo 2: Sampling (write_haiku)
155+
console.log('\n--- Demo 2: Sampling ---');
156+
console.log('Calling write_haiku tool...');
157+
158+
const haikuStream = client.experimental.tasks.callToolStream(
159+
{ name: 'write_haiku', arguments: { topic: 'autumn leaves' } },
160+
CallToolResultSchema,
161+
{
162+
task: { ttl: 60000 }
163+
}
164+
);
165+
166+
for await (const message of haikuStream) {
167+
switch (message.type) {
168+
case 'taskCreated':
169+
console.log(`Task created: ${message.task.taskId}`);
170+
break;
171+
case 'taskStatus':
172+
console.log(`Task status: ${message.task.status}`);
173+
break;
174+
case 'result':
175+
console.log(`Result:\n${getTextContent(message.result)}`);
176+
break;
177+
case 'error':
178+
console.error(`Error: ${message.error}`);
179+
break;
180+
}
181+
}
182+
183+
// Cleanup
184+
console.log('\nDemo complete. Closing connection...');
185+
await transport.close();
186+
readline.close();
187+
}
188+
189+
// Parse command line arguments
190+
const args = process.argv.slice(2);
191+
let url = 'http://localhost:8000/mcp';
192+
193+
for (let i = 0; i < args.length; i++) {
194+
if (args[i] === '--url' && args[i + 1]) {
195+
url = args[i + 1];
196+
i++;
197+
}
198+
}
199+
200+
// Run the client
201+
run(url).catch(error => {
202+
console.error('Error running client:', error);
203+
process.exit(1);
204+
});
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
# Simple Task Interactive Example
2+
3+
This example demonstrates the MCP Tasks message queue pattern with interactive server-to-client requests (elicitation and sampling).
4+
5+
## Overview
6+
7+
The example consists of two components:
8+
9+
1. **Server** (`simpleTaskInteractive.ts`) - Exposes two task-based tools that require client interaction:
10+
- `confirm_delete` - Uses elicitation to ask the user for confirmation before "deleting" a file
11+
- `write_haiku` - Uses sampling to request an LLM to generate a haiku on a topic
12+
13+
2. **Client** (`simpleTaskInteractiveClient.ts`) - Connects to the server and handles:
14+
- Elicitation requests with simple y/n terminal prompts
15+
- Sampling requests with a mock haiku generator
16+
17+
## Key Concepts
18+
19+
### Task-Based Execution
20+
21+
Both tools use `execution.taskSupport: 'required'`, meaning they follow the "call-now, fetch-later" pattern:
22+
23+
1. Client calls tool with `task: { ttl: 60000 }` parameter
24+
2. Server creates a task and returns `CreateTaskResult` immediately
25+
3. Client polls via `tasks/result` to get the final result
26+
4. Server sends elicitation/sampling requests through the task message queue
27+
5. Client handles requests and returns responses
28+
6. Server completes the task with the final result
29+
30+
### Message Queue Pattern
31+
32+
When a tool needs to interact with the client (elicitation or sampling), it:
33+
34+
1. Updates task status to `input_required`
35+
2. Enqueues the request in the task message queue
36+
3. Waits for the response via a Resolver
37+
4. Updates task status back to `working`
38+
5. Continues processing
39+
40+
The `TaskResultHandler` dequeues messages when the client calls `tasks/result` and routes responses back to waiting Resolvers.
41+
42+
## Running the Example
43+
44+
### Start the Server
45+
46+
```bash
47+
# From the SDK root directory
48+
npx tsx src/examples/server/simpleTaskInteractive.ts
49+
50+
# Or with a custom port
51+
PORT=9000 npx tsx src/examples/server/simpleTaskInteractive.ts
52+
```
53+
54+
The server will start on http://localhost:8000/mcp (or your custom port).
55+
56+
### Run the Client
57+
58+
```bash
59+
# From the SDK root directory
60+
npx tsx src/examples/client/simpleTaskInteractiveClient.ts
61+
62+
# Or connect to a different server
63+
npx tsx src/examples/client/simpleTaskInteractiveClient.ts --url http://localhost:9000/mcp
64+
```
65+
66+
## Expected Output
67+
68+
### Server Output
69+
70+
```
71+
Starting server on http://localhost:8000/mcp
72+
73+
Available tools:
74+
- confirm_delete: Demonstrates elicitation (asks user y/n)
75+
- write_haiku: Demonstrates sampling (requests LLM completion)
76+
77+
[Server] confirm_delete called, task created: task-abc123
78+
[Server] confirm_delete: asking about 'important.txt'
79+
[Server] Sending elicitation request to client...
80+
[Server] tasks/result called for task task-abc123
81+
[Server] Delivering queued request message for task task-abc123
82+
[Server] Received elicitation response: action=accept, content={"confirm":true}
83+
[Server] Completing task with result: Deleted 'important.txt'
84+
85+
[Server] write_haiku called, task created: task-def456
86+
[Server] write_haiku: topic 'autumn leaves'
87+
[Server] Sending sampling request to client...
88+
[Server] tasks/result called for task task-def456
89+
[Server] Delivering queued request message for task task-def456
90+
[Server] Received sampling response: Cherry blossoms fall...
91+
[Server] Completing task with haiku
92+
```
93+
94+
### Client Output
95+
96+
```
97+
Simple Task Interactive Client
98+
==============================
99+
Connecting to http://localhost:8000/mcp...
100+
Connected!
101+
102+
Available tools: confirm_delete, write_haiku
103+
104+
--- Demo 1: Elicitation ---
105+
Calling confirm_delete tool...
106+
Task created: task-abc123
107+
Task status: working
108+
109+
[Elicitation] Server asks: Are you sure you want to delete 'important.txt'?
110+
Your response (y/n): y
111+
[Elicitation] Responding with: confirm=true
112+
Task status: input_required
113+
Task status: completed
114+
Result: Deleted 'important.txt'
115+
116+
--- Demo 2: Sampling ---
117+
Calling write_haiku tool...
118+
Task created: task-def456
119+
Task status: working
120+
121+
[Sampling] Server requests LLM completion for: Write a haiku about autumn leaves
122+
[Sampling] Responding with haiku
123+
Task status: input_required
124+
Task status: completed
125+
Result:
126+
Haiku:
127+
Cherry blossoms fall
128+
Softly on the quiet pond
129+
Spring whispers goodbye
130+
131+
Demo complete. Closing connection...
132+
```
133+
134+
## Implementation Details
135+
136+
### Server Components
137+
138+
- **Resolver**: Promise-like class for passing results between async operations
139+
- **TaskMessageQueueWithResolvers**: Extended message queue that tracks pending requests with their Resolvers
140+
- **TaskStoreWithNotifications**: Extended task store with notification support for status changes
141+
- **TaskResultHandler**: Handles `tasks/result` requests by dequeuing messages and routing responses
142+
- **TaskSession**: Wraps the server to enqueue requests during task execution
143+
144+
### Client Capabilities
145+
146+
The client declares these capabilities during initialization:
147+
148+
```typescript
149+
capabilities: {
150+
elicitation: { form: {} },
151+
sampling: {}
152+
}
153+
```
154+
155+
This tells the server that the client can handle both form-based elicitation and sampling requests.
156+
157+
## Related Files
158+
159+
- `src/shared/task.ts` - Core task interfaces (TaskStore, TaskMessageQueue)
160+
- `src/examples/shared/inMemoryTaskStore.ts` - In-memory implementations
161+
- `src/types.ts` - Task-related types (Task, CreateTaskResult, GetTaskRequestSchema, etc.)

0 commit comments

Comments
 (0)