use crate::context::Context;
use mcpkit_core::error::McpError;
use mcpkit_core::types::content::Role;
use mcpkit_core::types::sampling::{
CreateMessageRequest, CreateMessageResult, IncludeContext, ModelPreferences, SamplingMessage,
StopReason,
};
use std::future::Future;
use std::pin::Pin;
pub type BoxedSamplingFn = Box<
dyn for<'a> Fn(
CreateMessageRequest,
&'a Context<'a>,
) -> Pin<
Box<dyn Future<Output = Result<CreateMessageResult, McpError>> + Send + 'a>,
> + Send
+ Sync,
>;
pub struct SamplingService {
handler: Option<BoxedSamplingFn>,
}
impl Default for SamplingService {
fn default() -> Self {
Self::new()
}
}
impl SamplingService {
#[must_use]
pub fn new() -> Self {
Self { handler: None }
}
pub fn with_handler<F, Fut>(mut self, handler: F) -> Self
where
F: Fn(CreateMessageRequest, &Context<'_>) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<CreateMessageResult, McpError>> + Send + 'static,
{
self.handler = Some(Box::new(move |req, ctx| Box::pin(handler(req, ctx))));
self
}
#[must_use]
pub fn is_supported(&self) -> bool {
self.handler.is_some()
}
pub async fn create_message(
&self,
request: CreateMessageRequest,
ctx: &Context<'_>,
) -> Result<CreateMessageResult, McpError> {
let handler = self
.handler
.as_ref()
.ok_or_else(|| McpError::invalid_request("Sampling not supported"))?;
(handler)(request, ctx).await
}
}
pub struct SamplingRequestBuilder {
messages: Vec<SamplingMessage>,
model_preferences: Option<ModelPreferences>,
system_prompt: Option<String>,
include_context: Option<IncludeContext>,
max_tokens: Option<u32>,
temperature: Option<f64>,
stop_sequences: Vec<String>,
}
impl Default for SamplingRequestBuilder {
fn default() -> Self {
Self::new()
}
}
impl SamplingRequestBuilder {
#[must_use]
pub const fn new() -> Self {
Self {
messages: Vec::new(),
model_preferences: None,
system_prompt: None,
include_context: None,
max_tokens: None,
temperature: None,
stop_sequences: Vec::new(),
}
}
pub fn user(mut self, content: impl Into<String>) -> Self {
self.messages.push(SamplingMessage::user(content.into()));
self
}
pub fn assistant(mut self, content: impl Into<String>) -> Self {
self.messages
.push(SamplingMessage::assistant(content.into()));
self
}
#[must_use]
pub fn message(mut self, msg: SamplingMessage) -> Self {
self.messages.push(msg);
self
}
#[must_use]
pub fn model_preferences(mut self, prefs: ModelPreferences) -> Self {
self.model_preferences = Some(prefs);
self
}
#[must_use]
pub const fn include_context(mut self, context: IncludeContext) -> Self {
self.include_context = Some(context);
self
}
pub fn system_prompt(mut self, prompt: impl Into<String>) -> Self {
self.system_prompt = Some(prompt.into());
self
}
#[must_use]
pub const fn max_tokens(mut self, tokens: u32) -> Self {
self.max_tokens = Some(tokens);
self
}
#[must_use]
pub const fn temperature(mut self, temp: f64) -> Self {
self.temperature = Some(temp);
self
}
pub fn stop_sequence(mut self, seq: impl Into<String>) -> Self {
self.stop_sequences.push(seq.into());
self
}
#[must_use]
pub fn build(self) -> CreateMessageRequest {
CreateMessageRequest {
messages: self.messages,
model_preferences: self.model_preferences,
system_prompt: self.system_prompt,
include_context: self.include_context,
max_tokens: self.max_tokens.unwrap_or(1024),
temperature: self.temperature,
stop_sequences: if self.stop_sequences.is_empty() {
None
} else {
Some(self.stop_sequences)
},
metadata: None,
}
}
}
pub struct SamplingResultBuilder {
role: Role,
content: String,
model: String,
stop_reason: Option<StopReason>,
}
impl SamplingResultBuilder {
pub fn new(model: impl Into<String>) -> Self {
Self {
role: Role::Assistant,
content: String::new(),
model: model.into(),
stop_reason: None,
}
}
pub fn content(mut self, content: impl Into<String>) -> Self {
self.content = content.into();
self
}
#[must_use]
pub const fn stop_reason(mut self, reason: StopReason) -> Self {
self.stop_reason = Some(reason);
self
}
#[must_use]
pub const fn end_turn(mut self) -> Self {
self.stop_reason = Some(StopReason::EndTurn);
self
}
#[must_use]
pub const fn max_tokens_reached(mut self) -> Self {
self.stop_reason = Some(StopReason::MaxTokens);
self
}
#[must_use]
pub fn build(self) -> CreateMessageResult {
CreateMessageResult {
role: self.role,
content: mcpkit_core::types::content::Content::text(self.content),
model: self.model,
stop_reason: self.stop_reason,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sampling_request_builder() {
let request = SamplingRequestBuilder::new()
.system_prompt("You are a helpful assistant")
.user("Hello!")
.max_tokens(100)
.temperature(0.7)
.build();
assert_eq!(request.messages.len(), 1);
assert_eq!(request.max_tokens, 100);
assert_eq!(request.temperature, Some(0.7));
assert_eq!(
request.system_prompt.as_deref(),
Some("You are a helpful assistant")
);
}
#[test]
fn test_sampling_result_builder() {
let result = SamplingResultBuilder::new("gpt-4")
.content("Hello! How can I help you?")
.end_turn()
.build();
assert_eq!(result.role, Role::Assistant);
assert_eq!(result.model, "gpt-4");
assert_eq!(result.stop_reason, Some(StopReason::EndTurn));
}
#[test]
fn test_sampling_service_default() {
let service = SamplingService::new();
assert!(!service.is_supported());
}
#[tokio::test]
async fn test_sampling_service_with_handler() {
let service = SamplingService::new().with_handler(|_req, _ctx| async {
Ok(SamplingResultBuilder::new("test-model")
.content("Test response")
.end_turn()
.build())
});
assert!(service.is_supported());
}
}