* 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.
182 lines
6.3 KiB
Rust
182 lines
6.3 KiB
Rust
pub mod progress;
|
|
use crate::{
|
|
error::ErrorData as McpError,
|
|
model::*,
|
|
service::{NotificationContext, RequestContext, RoleClient, Service, ServiceRole},
|
|
};
|
|
|
|
impl<H: ClientHandler> Service<RoleClient> for H {
|
|
async fn handle_request(
|
|
&self,
|
|
request: <RoleClient as ServiceRole>::PeerReq,
|
|
context: RequestContext<RoleClient>,
|
|
) -> Result<<RoleClient as ServiceRole>::Resp, McpError> {
|
|
match request {
|
|
ServerRequest::PingRequest(_) => self.ping(context).await.map(ClientResult::empty),
|
|
ServerRequest::CreateMessageRequest(request) => self
|
|
.create_message(request.params, context)
|
|
.await
|
|
.map(ClientResult::CreateMessageResult),
|
|
ServerRequest::ListRootsRequest(_) => self
|
|
.list_roots(context)
|
|
.await
|
|
.map(ClientResult::ListRootsResult),
|
|
ServerRequest::CreateElicitationRequest(request) => self
|
|
.create_elicitation(request.params, context)
|
|
.await
|
|
.map(ClientResult::CreateElicitationResult),
|
|
}
|
|
}
|
|
|
|
async fn handle_notification(
|
|
&self,
|
|
notification: <RoleClient as ServiceRole>::PeerNot,
|
|
context: NotificationContext<RoleClient>,
|
|
) -> Result<(), McpError> {
|
|
match notification {
|
|
ServerNotification::CancelledNotification(notification) => {
|
|
self.on_cancelled(notification.params, context).await
|
|
}
|
|
ServerNotification::ProgressNotification(notification) => {
|
|
self.on_progress(notification.params, context).await
|
|
}
|
|
ServerNotification::LoggingMessageNotification(notification) => {
|
|
self.on_logging_message(notification.params, context).await
|
|
}
|
|
ServerNotification::ResourceUpdatedNotification(notification) => {
|
|
self.on_resource_updated(notification.params, context).await
|
|
}
|
|
ServerNotification::ResourceListChangedNotification(_notification_no_param) => {
|
|
self.on_resource_list_changed(context).await
|
|
}
|
|
ServerNotification::ToolListChangedNotification(_notification_no_param) => {
|
|
self.on_tool_list_changed(context).await
|
|
}
|
|
ServerNotification::PromptListChangedNotification(_notification_no_param) => {
|
|
self.on_prompt_list_changed(context).await
|
|
}
|
|
};
|
|
Ok(())
|
|
}
|
|
|
|
fn get_info(&self) -> <RoleClient as ServiceRole>::Info {
|
|
self.get_info()
|
|
}
|
|
}
|
|
|
|
#[allow(unused_variables)]
|
|
pub trait ClientHandler: Sized + Send + Sync + 'static {
|
|
fn ping(
|
|
&self,
|
|
context: RequestContext<RoleClient>,
|
|
) -> impl Future<Output = Result<(), McpError>> + Send + '_ {
|
|
std::future::ready(Ok(()))
|
|
}
|
|
|
|
fn create_message(
|
|
&self,
|
|
params: CreateMessageRequestParam,
|
|
context: RequestContext<RoleClient>,
|
|
) -> impl Future<Output = Result<CreateMessageResult, McpError>> + Send + '_ {
|
|
std::future::ready(Err(
|
|
McpError::method_not_found::<CreateMessageRequestMethod>(),
|
|
))
|
|
}
|
|
|
|
fn list_roots(
|
|
&self,
|
|
context: RequestContext<RoleClient>,
|
|
) -> impl Future<Output = Result<ListRootsResult, McpError>> + Send + '_ {
|
|
std::future::ready(Ok(ListRootsResult::default()))
|
|
}
|
|
|
|
/// Handle an elicitation request from a server asking for user input.
|
|
///
|
|
/// This method is called when a server needs interactive input from the user
|
|
/// during tool execution. Implementations should present the message to the user,
|
|
/// collect their input according to the requested schema, and return the result.
|
|
///
|
|
/// # Arguments
|
|
/// * `request` - The elicitation request with message and schema
|
|
/// * `context` - The request context
|
|
///
|
|
/// # Returns
|
|
/// The user's response including action (accept/decline/cancel) and optional data
|
|
///
|
|
/// # Default Behavior
|
|
/// The default implementation automatically declines all elicitation requests.
|
|
/// Real clients should override this to provide user interaction.
|
|
fn create_elicitation(
|
|
&self,
|
|
request: CreateElicitationRequestParam,
|
|
context: RequestContext<RoleClient>,
|
|
) -> impl Future<Output = Result<CreateElicitationResult, McpError>> + Send + '_ {
|
|
// Default implementation declines all requests - real clients should override this
|
|
let _ = (request, context);
|
|
std::future::ready(Ok(CreateElicitationResult {
|
|
action: ElicitationAction::Decline,
|
|
content: None,
|
|
}))
|
|
}
|
|
|
|
fn on_cancelled(
|
|
&self,
|
|
params: CancelledNotificationParam,
|
|
context: NotificationContext<RoleClient>,
|
|
) -> impl Future<Output = ()> + Send + '_ {
|
|
std::future::ready(())
|
|
}
|
|
fn on_progress(
|
|
&self,
|
|
params: ProgressNotificationParam,
|
|
context: NotificationContext<RoleClient>,
|
|
) -> impl Future<Output = ()> + Send + '_ {
|
|
std::future::ready(())
|
|
}
|
|
fn on_logging_message(
|
|
&self,
|
|
params: LoggingMessageNotificationParam,
|
|
context: NotificationContext<RoleClient>,
|
|
) -> impl Future<Output = ()> + Send + '_ {
|
|
std::future::ready(())
|
|
}
|
|
fn on_resource_updated(
|
|
&self,
|
|
params: ResourceUpdatedNotificationParam,
|
|
context: NotificationContext<RoleClient>,
|
|
) -> impl Future<Output = ()> + Send + '_ {
|
|
std::future::ready(())
|
|
}
|
|
fn on_resource_list_changed(
|
|
&self,
|
|
context: NotificationContext<RoleClient>,
|
|
) -> impl Future<Output = ()> + Send + '_ {
|
|
std::future::ready(())
|
|
}
|
|
fn on_tool_list_changed(
|
|
&self,
|
|
context: NotificationContext<RoleClient>,
|
|
) -> impl Future<Output = ()> + Send + '_ {
|
|
std::future::ready(())
|
|
}
|
|
fn on_prompt_list_changed(
|
|
&self,
|
|
context: NotificationContext<RoleClient>,
|
|
) -> impl Future<Output = ()> + Send + '_ {
|
|
std::future::ready(())
|
|
}
|
|
|
|
fn get_info(&self) -> ClientInfo {
|
|
ClientInfo::default()
|
|
}
|
|
}
|
|
|
|
/// Do nothing, with default client info.
|
|
impl ClientHandler for () {}
|
|
|
|
/// Do nothing, with a specific client info.
|
|
impl ClientHandler for ClientInfo {
|
|
fn get_info(&self) -> ClientInfo {
|
|
self.clone()
|
|
}
|
|
}
|