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//! ## Perfect 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 async_trait::async_trait;
19use std::sync::Arc;
20use turbomcp_protocol::types::{CreateMessageRequest, CreateMessageResult};
21
22/// MCP-compliant sampling handler trait
23///
24/// The client receives sampling requests and delegates to configured LLM services.
25/// This maintains perfect separation of concerns per MCP specification.
26#[async_trait]
27pub trait SamplingHandler: Send + Sync + std::fmt::Debug {
28 /// Handle a sampling/createMessage request from a server
29 ///
30 /// This method should:
31 /// 1. Present the request to the user for approval
32 /// 2. Delegate to an external LLM service (could be another MCP server)
33 /// 3. Present the result to the user for review
34 /// 4. Return the approved result
35 ///
36 /// # Arguments
37 ///
38 /// * `request_id` - The JSON-RPC request ID from the server for proper response correlation
39 /// * `request` - The sampling request parameters
40 async fn handle_create_message(
41 &self,
42 request_id: String,
43 request: CreateMessageRequest,
44 ) -> Result<CreateMessageResult, Box<dyn std::error::Error + Send + Sync>>;
45}
46
47/// Default implementation that delegates to external MCP servers
48///
49/// This is the "batteries included" approach - it connects to LLM MCP servers
50/// but maintains perfect protocol compliance.
51#[derive(Debug)]
52pub struct DelegatingSamplingHandler {
53 /// Client instances for LLM MCP servers
54 llm_clients: Vec<Arc<dyn LLMServerClient>>,
55 /// User interaction handler
56 user_handler: Arc<dyn UserInteractionHandler>,
57}
58
59/// Interface for connecting to LLM MCP servers
60#[async_trait]
61pub trait LLMServerClient: Send + Sync + std::fmt::Debug {
62 /// Forward a sampling request to an LLM MCP server
63 async fn create_message(
64 &self,
65 request: CreateMessageRequest,
66 ) -> Result<CreateMessageResult, Box<dyn std::error::Error + Send + Sync>>;
67
68 /// Get server capabilities/model info
69 async fn get_server_info(&self)
70 -> Result<ServerInfo, Box<dyn std::error::Error + Send + Sync>>;
71}
72
73/// Interface for user interaction (human-in-the-loop)
74#[async_trait]
75pub trait UserInteractionHandler: Send + Sync + std::fmt::Debug {
76 /// Present sampling request to user for approval
77 async fn approve_request(
78 &self,
79 request: &CreateMessageRequest,
80 ) -> Result<bool, Box<dyn std::error::Error + Send + Sync>>;
81
82 /// Present result to user for review
83 async fn approve_response(
84 &self,
85 request: &CreateMessageRequest,
86 response: &CreateMessageResult,
87 ) -> Result<Option<CreateMessageResult>, Box<dyn std::error::Error + Send + Sync>>;
88}
89
90/// Server information for model selection
91#[derive(Debug, Clone)]
92pub struct ServerInfo {
93 pub name: String,
94 pub models: Vec<String>,
95 pub capabilities: Vec<String>,
96}
97
98#[async_trait]
99impl SamplingHandler for DelegatingSamplingHandler {
100 async fn handle_create_message(
101 &self,
102 _request_id: String,
103 request: CreateMessageRequest,
104 ) -> Result<CreateMessageResult, Box<dyn std::error::Error + Send + Sync>> {
105 // 1. Human-in-the-loop: Get user approval
106 if !self.user_handler.approve_request(&request).await? {
107 // FIXED: Return HandlerError::UserCancelled (code -1) instead of string error
108 // This ensures the error code is preserved when sent back to the server
109 return Err(Box::new(crate::handlers::HandlerError::UserCancelled));
110 }
111
112 // 2. Select appropriate LLM server based on model preferences
113 let selected_client = self.select_llm_client(&request).await?;
114
115 // 3. Delegate to external LLM MCP server
116 let result = selected_client.create_message(request.clone()).await?;
117
118 // 4. Present result for user review
119 let approved_result = self
120 .user_handler
121 .approve_response(&request, &result)
122 .await?;
123
124 Ok(approved_result.unwrap_or(result))
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/// Default user handler that automatically approves (for development)
161#[derive(Debug)]
162pub struct AutoApprovingUserHandler;
163
164#[async_trait]
165impl UserInteractionHandler for AutoApprovingUserHandler {
166 async fn approve_request(
167 &self,
168 _request: &CreateMessageRequest,
169 ) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
170 Ok(true) // Auto-approve for development
171 }
172
173 async fn approve_response(
174 &self,
175 _request: &CreateMessageRequest,
176 _response: &CreateMessageResult,
177 ) -> Result<Option<CreateMessageResult>, Box<dyn std::error::Error + Send + Sync>> {
178 Ok(None) // Auto-approve, don't modify
179 }
180}