-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Description
would be great to get feedback on this, we will likely do this as a workaround to blocking elicitations calls
Problem
At the moment elicitations are blocking requests where the tool call is blocked client side. Tasks gets close to addressing, the tool call is no longer blocking but when Tasks go into the input_needed state they again becoming client and server blocking requests waiting on user input. While this works for stdio and dedicated user processes, it does not work when done at scale server side.
Proposal
This came out of a conversation I had with Claude on how to address
Non-Blocking Elicitations in MCP Tasks: Implementation Specification
Solution Overview
Use the _meta extension point in the MCP specification to embed elicitation requests directly in task responses, enabling fully non-blocking user input collection.
Architecture
- Capability Negotiation
Server declares task support for tools:
{
"capabilities": {
"tasks": {
"requests": {
"tools.call": true
}
}
}
}Server indicates which tools should use tasks:
{
"tools": [
{
"name": "book_dinner",
"description": "Book a dinner reservation",
"inputSchema": { ... },
"annotations": {
"taskHint": "always"
}
}
]
}- Initial Tool Call with Task Augmentation
Client sends task-augmented tool call:
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "book_dinner",
"arguments": {
"date": "2025-11-22",
"time": "19:00"
},
"task": {}
}
}- Server Returns CreateTaskResult with Embedded Elicitation
Server responds immediately (non-blocking) with elicitation in_meta:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"taskId": "task-dinner-123",
"status": "input_required",
"createdAt": "2025-11-21T10:00:00Z",
"_meta": {
"io.modelcontextprotocol/related-task": {
"taskId": "task-dinner-123"
},
"io.modelcontextprotocol/pending-elicitations": [
{
"elicitationId": "elicit-partysize-456",
"mode": "form",
"message": "How many people will be dining?",
"requestedSchema": {
"type": "object",
"properties": {
"partySize": {
"type": "number",
"minimum": 1,
"maximum": 20,
"title": "Number of guests"
}
},
"required": ["partySize"]
}
}
],
"io.modelcontextprotocol/model-immediate-response": "Checking availability for your reservation..."
}
}
}Key Points:
Server returns immediately, no blocking
Status is input_required
Elicitation embedded in _meta under custom key io.modelcontextprotocol/pending-elicitations
Optional model-immediate-response provides context for the LLM while waiting
- Client Presents Elicitation to User
Client extracts elicitation from_metaand displays form/prompt to user. No blocking calls needed. - Client Submits Elicitation Response via tasks/get
Client sends elicitation response embedded in tasks/get:
{
"jsonrpc": "2.0",
"id": 2,
"method": "tasks/get",
"params": {
"taskId": "task-dinner-123",
"_meta": {
"io.modelcontextprotocol/elicitation-response": {
"elicitationId": "elicit-partysize-456",
"action": "accept",
"content": {
"partySize": 4
}
}
}
}
}Why tasks/get?
Already part of the polling pattern clients use
Single round-trip: submit response + get updated status
Server can choose to acknowledge the response in the same call
Non-blocking: doesn't wait for task completion
- Server Processes Response and Completes Task
Server responds with updated task status:
Option A - Task still working:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"taskId": "task-dinner-123",
"status": "working",
"createdAt": "2025-11-21T10:00:00Z",
"_meta": {
"io.modelcontextprotocol/related-task": {
"taskId": "task-dinner-123"
}
}
}
}Option B - Task completed immediately:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"taskId": "task-dinner-123",
"status": "completed",
"createdAt": "2025-11-21T10:00:00Z",
"_meta": {
"io.modelcontextprotocol/related-task": {
"taskId": "task-dinner-123"
}
}
}
}- Client Retrieves Final Result
If task is completed, client calls tasks/result:
{
"jsonrpc": "2.0",
"id": 3,
"method": "tasks/result",
"params": {
"taskId": "task-dinner-123"
}
}Server returns the tool result:
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"content": [
{
"type": "text",
"text": "✓ Reservation confirmed for 4 people on November 22nd at 7:00 PM at Chez Claude. Confirmation #: RES-2024-789"
}
]
}
}Complete Flow Diagram
1. Client → Server: tools/call with task augmentation
2. Server → Client: CreateTaskResult (status=input_required) with elicitation in _meta
✓ Server is NON-BLOCKING, can handle thousands of waiting tasks
3. [User interacts with elicitation form]
4. Client → Server: tasks/get with elicitation response in _meta
5. Server → Client: Task (status=working or completed)
✓ Still NON-BLOCKING
6. [If status=completed]
Client → Server: tasks/result
7. Server → Client: Tool result
Total round-trips: 3 (vs. 4+ with traditional blocking elicitation)
Extension Keys
Custom _meta Keys Used
Compatibility
Spec-compliant: Uses only standard MCP task features + _meta extensions
Graceful degradation: Clients that don't understand custom _meta keys can still poll with tasks/get and call tasks/result when status is input_required (falls back to blocking behavior)
No protocol changes: Doesn't require changes to the MCP specification itself