Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
4ed61b6
feat: add streaming methods for elicitation and createMessage
LucaButBoring Dec 1, 2025
d41bcf8
chore: add elicitRequestStream example for validation
LucaButBoring Dec 2, 2025
26ebf24
Merge branch 'main' of https://github.com/modelcontextprotocol/typesc…
LucaButBoring Dec 2, 2025
2a2709a
Merge branch 'main' into feat/elicitation-sampling-streaming
LucaButBoring Dec 2, 2025
ca06490
Merge branch 'main' of https://github.com/modelcontextprotocol/typesc…
LucaButBoring Dec 3, 2025
771f101
Merge branch 'main' of https://github.com/modelcontextprotocol/typesc…
LucaButBoring Dec 8, 2025
01f864b
Merge branch 'main' of https://github.com/modelcontextprotocol/typesc…
LucaButBoring Dec 22, 2025
9943196
chore: update elicitation/sampling streaming for v2
LucaButBoring Dec 22, 2025
f123dae
Merge branch 'main' of https://github.com/modelcontextprotocol/typesc…
LucaButBoring Jan 30, 2026
7c3e35d
chore: fix lint errors
LucaButBoring Jan 30, 2026
1fbb82c
Merge branch 'main' of https://github.com/modelcontextprotocol/typesc…
LucaButBoring Feb 8, 2026
d8d7686
fix: migrate handlers to string-based API after merge
LucaButBoring Feb 8, 2026
e928f94
fix(examples): update task context API in server example
LucaButBoring Feb 8, 2026
72f986e
fix: lint import sorting and type imports
LucaButBoring Feb 8, 2026
56a28db
chore: remove unneeded typecasts in elicitation/sampling streaming
LucaButBoring Feb 8, 2026
79904ec
chore: rerun ci
LucaButBoring Feb 8, 2026
c467a6e
Merge branch 'main' into feat/elicitation-sampling-streaming
KKonstantinov Feb 11, 2026
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
64 changes: 52 additions & 12 deletions examples/client/src/simpleStreamableHttp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
Client,
getDisplayName,
GetPromptResultSchema,
InMemoryTaskStore,
ListPromptsResultSchema,
ListResourcesResultSchema,
ListToolsResultSchema,
Expand Down Expand Up @@ -65,6 +66,7 @@ function printHelp(): void {
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');
Expand Down Expand Up @@ -140,6 +142,11 @@ function commandLoop(): void {
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;
Expand Down Expand Up @@ -249,7 +256,10 @@ async function connect(url?: string): Promise<void> {
console.log(`Connecting to ${serverUrl}...`);

try {
// Create a new client with form elicitation capability
// Create task store for client-side task support
const clientTaskStore = new InMemoryTaskStore();

// Create a new client with form elicitation capability and task support
client = new Client(
{
name: 'example-client',
Expand All @@ -259,25 +269,49 @@ async function connect(url?: string): Promise<void> {
capabilities: {
elicitation: {
form: {}
},
tasks: {
requests: {
elicitation: {
create: {}
}
}
}
}
},
taskStore: clientTaskStore
}
);
client.onerror = error => {
console.error('\u001B[31mClient error:', error, '\u001B[0m');
};

// Set up elicitation request handler with proper validation
client.setRequestHandler('elicitation/create', async request => {
// Set up elicitation request handler with proper validation and task support
client.setRequestHandler('elicitation/create', async (request, extra) => {
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 || [];
Expand Down Expand Up @@ -411,7 +445,7 @@ async function connect(url?: string): Promise<void> {
}

if (inputCancelled) {
return { action: 'cancel' };
return returnResult({ action: 'cancel' });
}

// If we didn't complete all fields due to an error, try again
Expand All @@ -424,7 +458,7 @@ async function connect(url?: string): Promise<void> {
continue;
} else {
console.log('Maximum attempts reached. Declining request.');
return { action: 'decline' };
return returnResult({ action: 'decline' });
}
}

Expand All @@ -443,7 +477,7 @@ async function connect(url?: string): Promise<void> {
continue;
} else {
console.log('Maximum attempts reached. Declining request.');
return { action: 'decline' };
return returnResult({ action: 'decline' });
}
}

Expand All @@ -460,22 +494,22 @@ async function connect(url?: string): Promise<void> {
switch (confirmAnswer) {
case 'yes':
case 'y': {
return {
return returnResult({
action: 'accept',
content
};
});
}
case 'cancel':
case 'c': {
return { action: 'cancel' };
return returnResult({ action: 'cancel' });
}
case 'no':
case 'n': {
if (attempts < maxAttempts) {
console.log('Please re-enter the information...');
continue;
} else {
return { action: 'decline' };
return returnResult({ action: 'decline' });
}

break;
Expand All @@ -485,7 +519,7 @@ async function connect(url?: string): Promise<void> {
}

console.log('Maximum attempts reached. Declining request.');
return { action: 'decline' };
return returnResult({ action: 'decline' });
});

transport = new StreamableHTTPClientTransport(new URL(serverUrl), {
Expand Down Expand Up @@ -699,6 +733,12 @@ 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 });
Expand Down
109 changes: 109 additions & 0 deletions examples/server/src/simpleStreamableHttp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { createMcpExpressApp } from '@modelcontextprotocol/express';
import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
import type {
CallToolResult,
ElicitResult,
GetPromptResult,
PrimitiveSchemaDefinition,
ReadResourceResult,
Expand Down Expand Up @@ -494,6 +495,114 @@ const getServer = () => {
}
);

// Register a tool that demonstrates bidirectional task support:
// Server creates a task, then elicits input from client using elicitInputStream
// Using the experimental tasks API - WARNING: may change without notice
server.experimental.tasks.registerToolTask(
'collect-user-info-task',
{
title: 'Collect Info with Task',
description: 'Collects user info via elicitation with task support using elicitInputStream',
inputSchema: z.object({
infoType: z.enum(['contact', 'preferences']).describe('Type of information to collect').default('contact')
})
},
{
async createTask({ infoType }, ctx) {
// Create the server-side task
const task = await ctx.task.store.createTask({
ttl: ctx.task.requestedTtl
});

// Perform async work that makes a nested elicitation request using elicitInputStream
(async () => {
try {
const message = infoType === 'contact' ? 'Please provide your contact information' : 'Please set your preferences';

// Define schemas with proper typing for PrimitiveSchemaDefinition
const contactSchema: {
type: 'object';
properties: Record<string, PrimitiveSchemaDefinition>;
required: string[];
} = {
type: 'object',
properties: {
name: { type: 'string', title: 'Full Name', description: 'Your full name' },
email: { type: 'string', title: 'Email', description: 'Your email address' }
},
required: ['name', 'email']
};

const preferencesSchema: {
type: 'object';
properties: Record<string, PrimitiveSchemaDefinition>;
required: string[];
} = {
type: 'object',
properties: {
theme: { type: 'string', title: 'Theme', enum: ['light', 'dark', 'auto'] },
notifications: { type: 'boolean', title: 'Enable Notifications', default: true }
},
required: ['theme']
};

const requestedSchema = infoType === 'contact' ? contactSchema : preferencesSchema;

// Use elicitInputStream to elicit input from client
// This demonstrates the streaming elicitation API
// Access via server.server to get the underlying Server instance
const stream = server.server.experimental.tasks.elicitInputStream({
mode: 'form',
message,
requestedSchema
});

let elicitResult: ElicitResult | undefined;
for await (const msg of stream) {
if (msg.type === 'result') {
elicitResult = msg.result as ElicitResult;
} else if (msg.type === 'error') {
throw msg.error;
}
}

if (!elicitResult) {
throw new Error('No result received from elicitation');
}

let resultText: string;
if (elicitResult.action === 'accept') {
resultText = `Collected ${infoType} info: ${JSON.stringify(elicitResult.content, null, 2)}`;
} else if (elicitResult.action === 'decline') {
resultText = `User declined to provide ${infoType} information`;
} else {
resultText = 'User cancelled the request';
}

await taskStore.storeTaskResult(task.taskId, 'completed', {
content: [{ type: 'text', text: resultText }]
});
} catch (error) {
console.error('Error in collect-user-info-task:', error);
await taskStore.storeTaskResult(task.taskId, 'failed', {
content: [{ type: 'text', text: `Error: ${error}` }],
isError: true
});
}
})();

return { task };
},
async getTask(_args, ctx) {
return await ctx.task.store.getTask(ctx.task.id);
},
async getTaskResult(_args, ctx) {
const result = await ctx.task.store.getTaskResult(ctx.task.id);
return result as CallToolResult;
}
}
);

return server;
};

Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2629,8 +2629,8 @@ export type ResultTypeMap = {
'resources/unsubscribe': EmptyResult;
'tools/call': CallToolResult | CreateTaskResult;
'tools/list': ListToolsResult;
'sampling/createMessage': CreateMessageResult | CreateMessageResultWithTools;
'elicitation/create': ElicitResult;
'sampling/createMessage': CreateMessageResult | CreateMessageResultWithTools | CreateTaskResult;
'elicitation/create': ElicitResult | CreateTaskResult;
'roots/list': ListRootsResult;
'tasks/get': GetTaskResult;
'tasks/result': Result;
Expand Down
Loading
Loading