MCP Elicitation Implementation Plan
Current State
We have a custom generator-based elicitation system:
- Tools can
yield { ask: 'text', message: '...' }to request user input - The runtime's
createInputProvider()handles these yields - Works great for CLI (
readline) and our playground
Problem
MCP clients expect standard elicitation/create JSON-RPC method, not custom generator yields.
According to MCP spec (2025-06-18):
json
{
"jsonrpc": "2.0",
"id": 2,
"method": "elicitation/create",
"params": {
"message": "Please provide your contact information",
"requestedSchema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"email": {"type": "string", "format": "email"}
},
"required": ["name", "email"]
}
}
}Solution Architecture
1. Declare Elicitation Capability
During MCP server initialization, declare support:
typescript
server.setRequestHandler(InitializeRequestSchema, async () => {
return {
protocolVersion: "2024-11-05",
capabilities: {
tools: {},
prompts: {},
resources: {},
elicitation: { // ← ADD THIS
supported: true
}
},
serverInfo: {
name: "photon",
version: PHOTON_VERSION
}
};
});2. Bridge Generator Yields to MCP Elicitation
Modify createInputProvider() to:
- When tool yields
{ ask: '...', message: '...' }, convert to MCP elicitation schema - Send
elicitation/createrequest to MCP client (if in MCP mode) - Wait for client response
- Return the validated user input to generator
typescript
private createInputProvider(mcpServer?: Server): InputProvider {
return async (ask: AskYield): Promise<any> => {
// If we're in MCP mode, use elicitation/create
if (mcpServer) {
const schema = this.askYieldToJSONSchema(ask);
const response = await mcpServer.request({
method: 'elicitation/create',
params: {
message: ask.message,
requestedSchema: schema
}
});
if (response.action === 'accept') {
return this.extractValueFromResponse(response.content, ask);
} else if (response.action === 'decline') {
throw new Error('User declined input request');
} else {
throw new Error('User cancelled operation');
}
}
// Fallback to CLI readline (current implementation)
switch (ask.ask) {
case 'text':
return await elicitPrompt(ask.message, ask.default);
// ... rest of current implementation
}
};
}3. Convert Ask Yields to JSON Schema
Helper to convert our custom ask types to MCP elicitation schema:
typescript
private askYieldToJSONSchema(ask: AskYield): JSONSchema {
switch (ask.ask) {
case 'text':
return {
type: 'object',
properties: {
value: {
type: 'string',
description: ask.message
}
},
required: ['value']
};
case 'password':
return {
type: 'object',
properties: {
value: {
type: 'string',
description: ask.message,
format: 'password' // UI hint
}
},
required: ['value']
};
case 'confirm':
return {
type: 'object',
properties: {
confirmed: {
type: 'boolean',
description: ask.message
}
},
required: ['confirmed']
};
case 'select':
return {
type: 'object',
properties: {
selected: {
type: 'string',
enum: ask.options,
description: ask.message
}
},
required: ['selected']
};
case 'number':
return {
type: 'object',
properties: {
value: {
type: 'number',
description: ask.message,
...(ask.min !== undefined && { minimum: ask.min }),
...(ask.max !== undefined && { maximum: ask.max })
}
},
required: ['value']
};
// ... other types
}
}4. Update Playground to Support Elicitation
The playground already has SSE for progress - extend it for elicitation:
typescript
// Frontend: Listen for elicitation requests
eventSource.addEventListener('elicitation', (e) => {
const { message, schema } = JSON.parse(e.data);
// Show modal dialog with form based on schema
showElicitationModal(message, schema, (userInput) => {
// Send response back
fetch('/api/elicitation/respond', {
method: 'POST',
body: JSON.stringify({
action: 'accept',
content: userInput
})
});
});
});Implementation Status
Phase 1: Core Support (Completed)
- [x] Add elicitation capability to server initialization
- [x] Create input provider that bridges yields to MCP elicitation
- [x] Detect if running in MCP vs CLI mode
- [x] Bridge generator yields to MCP elicitation in MCP mode (
src/mcp-elicitation.ts)
Phase 2: Transports (Completed)
- [x] STDIO: Full MCP elicitation support via
server.elicitInput() - [x] SSE: Bidirectional via HTTP POST callbacks for Beam UI
- [x] Beam UI: Shows elicitation modals for interactive asks
Phase 3: Testing (Completed)
- [x] Test with kitchen-sink's
askUserName()method - [x] Test common ask types (text, confirm, select)
- [x] Test in Claude Desktop (stdio transport)
- [x] Test in Beam UI (SSE transport)
Edge Cases
- Nested elicitation: Tool yields multiple asks in sequence → Queue them
- Timeout: User doesn't respond → Configurable timeout, then cancel
- Invalid response: Schema validation fails → Re-prompt or error
- Client doesn't support elicitation: Fall back to error or skip
Benefits
✅ Standard MCP compliance - works with all MCP clients
✅ Backward compatible - CLI still uses readline
✅ Better UX - Clients can show native UI (forms, modals)
✅ Type safety - JSON Schema validation built-in
✅ Progressive enhancement - Works without elicitation too
