feat: Add MCP Elicitation support (#332)
* feat: implement MCP elicitation support for interactive user input Adds comprehensive elicitation functionality according to MCP 2025-06-18 specification: Core Features: - ElicitationAction enum (Accept, Decline, Cancel) - CreateElicitationRequestParam and CreateElicitationResult structures - Protocol version V_2025_06_18 with elicitation methods - Full JSON-RPC integration with method constants Capabilities Integration: - ElicitationCapability with schema validation support - ClientCapabilities builder pattern integration - enable_elicitation() and enable_elicitation_schema_validation() methods Handler Support: - create_elicitation method in ClientHandler and ServerHandler traits - Integration with existing request/response union types - Async/await compatible implementation Service Layer: - Basic create_elicitation method via macro expansion - Four convenience methods for common scenarios: * elicit_confirmation() - yes/no questions * elicit_text_input() - string input with optional requirements * elicit_choice() - selection from multiple options * elicit_structured_input() - complex data via JSON Schema Comprehensive Testing: - 11 test cases covering all functionality aspects - JSON serialization/deserialization validation - MCP specification compliance verification - Error handling and edge cases - Performance benchmarks - Capabilities integration tests All tests pass and code follows project standards. * feat: add typed elicitation API with enhanced error handling - Add new 'elicitation' feature that depends on 'client' and 'schemars' - Implement elicit<T>() method for type-safe elicitation with automatic schema generation - Remove convenience methods (elicit_confirmation, elicit_text_input, elicit_choice) - Add ElicitationError enum with detailed error variants: - Service: underlying service errors - UserDeclined: user cancelled or declined request - ParseError: response parsing failed with context - NoContent: no response content provided - Update documentation with comprehensive examples and error handling - Add comprehensive tests for typed elicitation and error handling * fix: correct elicitation direction to comply with MCP 2025-06-18 - Remove CreateElicitationRequest from ClientRequest - clients cannot initiate elicitation - Move elicit methods from client to server - servers now request user input - Add comprehensive direction tests verifying Server→Client→Server flow - Maintain CreateElicitationResult in ClientResult for proper responses - Update handlers to reflect correct message routing - Add elicitation feature flag for typed schema generation Fixes elicitation direction to match specification where servers request interactive user input from clients, not the reverse. * feat: add elicitation capability checking for server methods - Add supports_elicitation() method to check client capabilities - Add CapabilityNotSupported error variant to ElicitationError - Update elicit_structured_input() to check capabilities before execution - Update elicit<T>() method to check capabilities before execution - Add comprehensive tests for capability checking functionality - Tests verify that servers check client capabilities before sending elicitation requests - Ensures compliance with MCP 2025-06-18 specification requirement * fix: json rpc message schema * fix: doc tests * fix: cargo nightly fmt checks * fix: clippy * refactor: separate elicitation methods into dedicated impl block for RoleServer - Move (supports_elicitation, elicit_structured_input, elicit) to separate impl block - Move ElicitationError definition to elicitation methods section - Keep base methods (create_message, list_roots, notify_*) in main impl block with macro - Add section comments to distinguish general and elicitation-specific methods * revert: rollback LATEST protocol version to V_2025_03_26 * fix: remove protocol version assertions - Remove assertions for V_2025_06_18 protocol version * fix: fmt checks * feat: add timeout support for elicitation methods - Add peer_req_with_timeout macro variants for timeout-enabled methods - Implement create_elicitation_with_timeout() method - Implement elicit_with_timeout() for typed elicitation with timeout - Refactor elicit() to use elicit_with_timeout() internally - Add 8 comprehensive timeout tests covering validation, error handling, and realistic scenarios - Fix elicitation feature dependencies in Cargo.toml - Add proper feature gates for elicitation-specific code * feat: add timeout validation to prevent DoS attacks - Add InvalidTimeout error variant for comprehensive validation - Implement validate_timeout function with security limits (1ms-300s) - Integrate validation into peer_req_with_timeout macros - Add comprehensive security tests for timeout validation - Prevent DoS attacks through unreasonable timeout values * feat: separate UserDeclined and UserCancelled elicitation errors According to MCP specification and PR feedback, decline and cancel actions should be handled differently: - UserDeclined: explicit user rejection (clicked "Decline", "No", etc.) - UserCancelled: dismissal without explicit choice (closed dialog, Escape, etc.) Changes: - Split ElicitationError::UserDeclined into two distinct error types - Update error handling logic to map each ElicitationAction correctly - Improve documentation with proper action semantics - Add comprehensive tests for new error types and action mapping - Update examples to demonstrate proper error handling This provides better error granularity allowing servers to handle explicit declines vs cancellations appropriately as per MCP spec. * feat: add compile-time type safety for elicitation methods Add ElicitationSafe trait and elicit_safe\! macro to ensure elicit<T>() methods are only used with types that generate appropriate JSON object schemas, addressing type safety concerns from PR feedback. Features: - ElicitationSafe marker trait for compile-time constraints - elicit_safe\! macro for opt-in type safety declaration - Updated elicit<T> and elicit_with_timeout<T> to require ElicitationSafe bound - Comprehensive documentation with examples and rationale - Full test coverage for new type safety features This prevents common mistakes like: - elicit::<String>() - primitives not suitable for object schemas - elicit::<Vec<i32>>() - arrays don't match client expectations Breaking change: Existing code must add elicit_safe\!(TypeName) declarations for types used with elicit methods. This is an intentional safety improvement. * Revert "feat: add timeout validation to prevent DoS attacks" This reverts commit 829624212b4fb55ec32566329af7ec195c987ef5. * fix: correct doctest example in elicit_safe macro documentation - Remove invalid async/await usage in doctest example - Comment out the actual usage line to show intent without compilation errors - Maintain clear documentation of the macro's purpose and usage * refactor: remove redundant elicitation direction tests - Remove test_elicitation_not_in_client_request (duplicated functionality) - Remove redundant ServerRequest match in test_elicitation_direction_server_to_client - Direction compliance is already verified by the remaining comprehensive test - Reduces test fragility and maintenance burden * feat: add elicitation example with user name collection - Add elicitation server example demonstrating real MCP usage - Implement greet_user tool with context.peer.elicit::<T>() API - Show type-safe elicitation with elicit_safe! macro - Include reset_name tool and MCP Inspector instructions - Update examples documentation and dependencies * fix: add Default impl to ElicitationServer for clippy Resolves clippy::new_without_default warning by implementing Default trait for ElicitationServer struct.
This commit is contained in:
parent
4e6c57ef14
commit
81ff3159f9
3 changed files with 2287 additions and 4 deletions
|
|
@ -14,6 +14,8 @@ rmcp = { workspace = true, features = [
|
|||
"transport-io",
|
||||
"transport-streamable-http-server",
|
||||
"auth",
|
||||
"elicitation",
|
||||
"schemars",
|
||||
] }
|
||||
tokio = { version = "1", features = [
|
||||
"macros",
|
||||
|
|
@ -34,7 +36,7 @@ tracing-subscriber = { version = "0.3", features = [
|
|||
futures = "0.3"
|
||||
rand = { version = "0.9", features = ["std"] }
|
||||
axum = { version = "0.8", features = ["macros"] }
|
||||
schemars = { version = "1.0" }
|
||||
schemars = "1.0"
|
||||
reqwest = { version = "0.12", features = ["json"] }
|
||||
chrono = "0.4"
|
||||
uuid = { version = "1.6", features = ["v4", "serde"] }
|
||||
|
|
@ -87,3 +89,7 @@ path = "src/sampling_stdio.rs"
|
|||
[[example]]
|
||||
name = "servers_structured_output"
|
||||
path = "src/structured_output.rs"
|
||||
|
||||
[[example]]
|
||||
name = "servers_elicitation_stdio"
|
||||
path = "src/elicitation_stdio.rs"
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ A server using streamable HTTP transport for MCP communication, with axum.
|
|||
### Counter Streamable HTTP Server with Hyper (`counter_hyper_streamable_http.rs`)
|
||||
|
||||
A server using streamable HTTP transport for MCP communication, with hyper.
|
||||
|
||||
- Runs on HTTP with streaming capabilities
|
||||
- Provides counter tools via HTTP streaming
|
||||
- Demonstrates streamable HTTP transport configuration
|
||||
|
|
@ -72,6 +73,16 @@ A simplified OAuth example showing basic token-based authentication.
|
|||
- Simplified authentication flow
|
||||
- Good starting point for adding authentication to MCP servers
|
||||
|
||||
### Elicitation Demo (`elicitation_stdio.rs`)
|
||||
|
||||
A working MCP server demonstrating elicitation for user name collection.
|
||||
|
||||
- Real MCP server using rmcp library
|
||||
- `context.peer.elicit::<T>()` API usage
|
||||
- Type-safe elicitation with `elicit_safe!` macro
|
||||
- JSON Schema validation with schemars
|
||||
- Tools: `greet_user` (collects name), `reset_name` (clears stored name)
|
||||
|
||||
## How to Run
|
||||
|
||||
Each example can be run using Cargo:
|
||||
|
|
@ -97,6 +108,9 @@ cargo run --example servers_complex_auth_sse
|
|||
|
||||
# Run the simple OAuth SSE server
|
||||
cargo run --example servers_simple_auth_sse
|
||||
|
||||
# Run the elicitation standard I/O server
|
||||
cargo run --example servers_elicitation_stdio
|
||||
```
|
||||
|
||||
## Testing with MCP Inspector
|
||||
|
|
@ -120,6 +134,7 @@ These examples use the following main dependencies:
|
|||
- `uuid`: UUID generation (used in OAuth examples)
|
||||
- `chrono`: Date and time handling (used in OAuth examples)
|
||||
- `rand`: Random number generation (used in OAuth examples)
|
||||
- `schemars`: JSON Schema generation (used in elicitation examples)
|
||||
|
||||
## Common Module
|
||||
|
||||
|
|
@ -129,4 +144,4 @@ The `common/` directory contains shared code used across examples:
|
|||
- `calculator.rs`: Calculator tool examples
|
||||
- `generic_service.rs`: Generic service implementations
|
||||
|
||||
This modular approach allows for code reuse and demonstrates how to structure larger MCP server applications.
|
||||
This modular approach allows for code reuse and demonstrates how to structure larger MCP server applications.
|
||||
|
|
|
|||
143
examples/servers/src/elicitation_stdio.rs
Normal file
143
examples/servers/src/elicitation_stdio.rs
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
//! Simple MCP Server with Elicitation
|
||||
//!
|
||||
//! Demonstrates user name collection via elicitation
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use rmcp::{
|
||||
ErrorData as McpError, ServerHandler, ServiceExt, elicit_safe,
|
||||
handler::server::{router::tool::ToolRouter, tool::Parameters},
|
||||
model::*,
|
||||
schemars::JsonSchema,
|
||||
service::{RequestContext, RoleServer},
|
||||
tool, tool_handler, tool_router,
|
||||
transport::stdio,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::Mutex;
|
||||
use tracing_subscriber::{self, EnvFilter};
|
||||
|
||||
/// User information request
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[schemars(description = "User information")]
|
||||
pub struct UserInfo {
|
||||
#[schemars(description = "User's name")]
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
// Mark as safe for elicitation
|
||||
elicit_safe!(UserInfo);
|
||||
|
||||
/// Simple tool request
|
||||
#[derive(Debug, Deserialize, JsonSchema)]
|
||||
pub struct GreetRequest {
|
||||
pub greeting: String,
|
||||
}
|
||||
|
||||
/// Simple server with elicitation
|
||||
#[derive(Clone)]
|
||||
pub struct ElicitationServer {
|
||||
user_name: Arc<Mutex<Option<String>>>,
|
||||
tool_router: ToolRouter<ElicitationServer>,
|
||||
}
|
||||
|
||||
impl Default for ElicitationServer {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[tool_router]
|
||||
impl ElicitationServer {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
user_name: Arc::new(Mutex::new(None)),
|
||||
tool_router: Self::tool_router(),
|
||||
}
|
||||
}
|
||||
|
||||
#[tool(description = "Greet user with name collection")]
|
||||
async fn greet_user(
|
||||
&self,
|
||||
context: RequestContext<RoleServer>,
|
||||
Parameters(request): Parameters<GreetRequest>,
|
||||
) -> Result<CallToolResult, McpError> {
|
||||
// Check if we have user name
|
||||
let current_name = self.user_name.lock().await.clone();
|
||||
|
||||
let user_name = if let Some(name) = current_name {
|
||||
name
|
||||
} else {
|
||||
// Request user name via elicitation
|
||||
match context
|
||||
.peer
|
||||
.elicit::<UserInfo>("Please provide your name".to_string())
|
||||
.await
|
||||
{
|
||||
Ok(Some(user_info)) => {
|
||||
let name = user_info.name.clone();
|
||||
*self.user_name.lock().await = Some(name.clone());
|
||||
name
|
||||
}
|
||||
Ok(None) => "Guest".to_string(), // Never happen if client checks schema
|
||||
Err(_) => "Unknown".to_string(),
|
||||
}
|
||||
};
|
||||
|
||||
Ok(CallToolResult::success(vec![Content::text(format!(
|
||||
"{} {}!",
|
||||
request.greeting, user_name
|
||||
))]))
|
||||
}
|
||||
|
||||
#[tool(description = "Reset stored user name")]
|
||||
async fn reset_name(&self) -> Result<CallToolResult, McpError> {
|
||||
*self.user_name.lock().await = None;
|
||||
Ok(CallToolResult::success(vec![Content::text(
|
||||
"User name reset. Next greeting will ask for name again.".to_string(),
|
||||
)]))
|
||||
}
|
||||
}
|
||||
|
||||
#[tool_handler]
|
||||
impl ServerHandler for ElicitationServer {
|
||||
fn get_info(&self) -> ServerInfo {
|
||||
ServerInfo {
|
||||
capabilities: ServerCapabilities::builder().enable_tools().build(),
|
||||
server_info: Implementation::from_build_env(),
|
||||
instructions: Some(
|
||||
"Simple server demonstrating elicitation for user name collection".to_string(),
|
||||
),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(EnvFilter::from_default_env())
|
||||
.init();
|
||||
|
||||
println!("Simple MCP Elicitation Demo");
|
||||
|
||||
// Get current executable path for Inspector
|
||||
let current_exe = std::env::current_exe()
|
||||
.map(|path| path.display().to_string())
|
||||
.unwrap();
|
||||
|
||||
println!("To test with MCP Inspector:");
|
||||
println!("1. Run: npx @modelcontextprotocol/inspector");
|
||||
println!("2. Enter server command: {}", current_exe);
|
||||
|
||||
let service = ElicitationServer::new()
|
||||
.serve(stdio())
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
tracing::error!("serving error: {:?}", e);
|
||||
})?;
|
||||
|
||||
service.waiting().await?;
|
||||
Ok(())
|
||||
}
|
||||
Loading…
Reference in a new issue