turbomcp_client/sampling.rs
1//! MCP-Compliant Client-Side Sampling Support
2//!
3//! This module provides the correct MCP architecture for handling sampling requests.
4//! The client's role is to:
5//! 1. Receive sampling/createMessage requests from servers
6//! 2. Present them to users for approval (human-in-the-loop)
7//! 3. Delegate to external LLM services (which can be MCP servers themselves)
8//! 4. Return standardized results
9//!
10//! ## MCP Compliance
11//!
12//! Unlike embedding LLM APIs directly (anti-pattern), this implementation:
13//! - Delegates to external services
14//! - Maintains protocol boundaries
15//! - Enables composition and flexibility
16//! - Provides maximum developer experience through simplicity
17
18use std::future::Future;
19use std::pin::Pin;
20use std::sync::Arc;
21use turbomcp_protocol::types::{CreateMessageRequest, CreateMessageResult};
22
23/// Boxed future type alias for sampling operations
24pub type BoxSamplingFuture<'a, T> =
25 Pin<Box<dyn Future<Output = Result<T, Box<dyn std::error::Error + Send + Sync>>> + Send + 'a>>;
26
27/// MCP-compliant sampling handler trait
28///
29/// The client receives sampling requests and delegates to configured LLM services.
30/// This maintains separation of concerns per MCP specification.
31pub trait SamplingHandler: Send + Sync + std::fmt::Debug {
32 /// Handle a sampling/createMessage request from a server
33 ///
34 /// This method should:
35 /// 1. Present the request to the user for approval
36 /// 2. Delegate to an external LLM service (could be another MCP server)
37 /// 3. Present the result to the user for review
38 /// 4. Return the approved result
39 ///
40 /// # Arguments
41 ///
42 /// * `request_id` - The JSON-RPC request ID from the server for proper response correlation
43 /// * `request` - The sampling request parameters
44 fn handle_create_message(
45 &self,
46 request_id: String,
47 request: CreateMessageRequest,
48 ) -> BoxSamplingFuture<'_, CreateMessageResult>;
49}
50
51/// Default implementation that delegates to external MCP servers
52///
53/// This is the "batteries included" approach - it connects to LLM MCP servers
54/// but maintains protocol compliance.
55#[derive(Debug)]
56pub struct DelegatingSamplingHandler {
57 /// Client instances for LLM MCP servers
58 llm_clients: Vec<Arc<dyn LLMServerClient>>,
59 /// User interaction handler
60 user_handler: Arc<dyn UserInteractionHandler>,
61}
62
63/// Interface for connecting to LLM MCP servers
64pub trait LLMServerClient: Send + Sync + std::fmt::Debug {
65 /// Forward a sampling request to an LLM MCP server
66 fn create_message(
67 &self,
68 request: CreateMessageRequest,
69 ) -> BoxSamplingFuture<'_, CreateMessageResult>;
70
71 /// Get server capabilities/model info
72 fn get_server_info(&self) -> BoxSamplingFuture<'_, LlmServerInfo>;
73}
74
75/// Interface for user interaction (human-in-the-loop)
76pub trait UserInteractionHandler: Send + Sync + std::fmt::Debug {
77 /// Present sampling request to user for approval
78 fn approve_request(&self, request: &CreateMessageRequest) -> BoxSamplingFuture<'_, bool>;
79
80 /// Present result to user for review
81 fn approve_response(
82 &self,
83 request: &CreateMessageRequest,
84 response: &CreateMessageResult,
85 ) -> BoxSamplingFuture<'_, Option<CreateMessageResult>>;
86}
87
88/// LLM-server descriptor used by sampling handlers for model selection.
89#[derive(Debug, Clone)]
90pub struct LlmServerInfo {
91 pub name: String,
92 pub models: Vec<String>,
93 pub capabilities: Vec<String>,
94}
95
96impl SamplingHandler for DelegatingSamplingHandler {
97 fn handle_create_message(
98 &self,
99 _request_id: String,
100 request: CreateMessageRequest,
101 ) -> BoxSamplingFuture<'_, CreateMessageResult> {
102 Box::pin(async move {
103 // 1. Human-in-the-loop: Get user approval
104 if !self.user_handler.approve_request(&request).await? {
105 // FIXED: Return HandlerError::UserCancelled (code -1) instead of string error
106 // This ensures the error code is preserved when sent back to the server
107 return Err(Box::new(crate::handlers::HandlerError::UserCancelled)
108 as Box<dyn std::error::Error + Send + Sync>);
109 }
110
111 // 2. Select appropriate LLM server based on model preferences
112 let selected_client = self.select_llm_client(&request).await?;
113
114 // 3. Delegate to external LLM MCP server
115 let result = selected_client.create_message(request.clone()).await?;
116
117 // 4. Present result for user review
118 let approved_result = self
119 .user_handler
120 .approve_response(&request, &result)
121 .await?;
122
123 Ok(approved_result.unwrap_or(result))
124 })
125 }
126}
127
128impl DelegatingSamplingHandler {
129 /// Create new handler with LLM server clients
130 pub fn new(
131 llm_clients: Vec<Arc<dyn LLMServerClient>>,
132 user_handler: Arc<dyn UserInteractionHandler>,
133 ) -> Self {
134 Self {
135 llm_clients,
136 user_handler,
137 }
138 }
139
140 /// Select best LLM client based on model preferences
141 async fn select_llm_client(
142 &self,
143 _request: &CreateMessageRequest,
144 ) -> Result<Arc<dyn LLMServerClient>, Box<dyn std::error::Error + Send + Sync>> {
145 // This is where the intelligence goes - matching model preferences
146 // to available LLM servers, exactly as the MCP spec describes
147
148 if let Some(first_client) = self.llm_clients.first() {
149 Ok(first_client.clone())
150 } else {
151 // FIXED: Return HandlerError::Configuration instead of string error
152 // This ensures proper error code mapping (-32601)
153 Err(Box::new(crate::handlers::HandlerError::Configuration {
154 message: "No LLM servers configured".to_string(),
155 }))
156 }
157 }
158}
159
160/// **Development-only** user handler that auto-approves every sampling
161/// request and every response without prompting.
162///
163/// MCP specifies that sampling MUST have human-in-the-loop approval
164/// (`schema.ts:2316-2333` security note). This implementation defeats
165/// that — use it for tests, demos, and local CLI tools, never in a
166/// deployed agent that processes untrusted prompts. Logs a warning at
167/// construction time so the choice shows up in operator-visible output.
168#[derive(Debug)]
169pub struct AutoApprovingUserHandler;
170
171impl AutoApprovingUserHandler {
172 /// Construct an auto-approving handler. Emits a `tracing::warn!`
173 /// to make the unsafe-by-default behavior auditable in deployed logs.
174 #[must_use]
175 pub fn new() -> Self {
176 tracing::warn!(
177 "AutoApprovingUserHandler constructed; sampling requests will be \
178 approved without human review. Do not use in production agents."
179 );
180 Self
181 }
182}
183
184impl Default for AutoApprovingUserHandler {
185 fn default() -> Self {
186 Self::new()
187 }
188}
189
190impl UserInteractionHandler for AutoApprovingUserHandler {
191 fn approve_request(&self, _request: &CreateMessageRequest) -> BoxSamplingFuture<'_, bool> {
192 Box::pin(async move {
193 Ok(true) // Auto-approve for development
194 })
195 }
196
197 fn approve_response(
198 &self,
199 _request: &CreateMessageRequest,
200 _response: &CreateMessageResult,
201 ) -> BoxSamplingFuture<'_, Option<CreateMessageResult>> {
202 Box::pin(async move {
203 Ok(None) // Auto-approve, don't modify
204 })
205 }
206}