use async_trait::async_trait;
use pulseengine_mcp_protocol::{Error, LogLevel};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::fmt;
use std::sync::Arc;
use std::time::Duration;
#[derive(Debug)]
pub enum ToolContextError {
NotificationFailed(String),
RequestFailed(String),
Timeout,
Declined(String),
NotAvailable,
Serialization(String),
Transport(String),
}
impl fmt::Display for ToolContextError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NotificationFailed(msg) => write!(f, "Notification failed: {msg}"),
Self::RequestFailed(msg) => write!(f, "Request failed: {msg}"),
Self::Timeout => write!(f, "Request timed out"),
Self::Declined(msg) => write!(f, "Client declined: {msg}"),
Self::NotAvailable => write!(f, "Tool context not available"),
Self::Serialization(msg) => write!(f, "Serialization error: {msg}"),
Self::Transport(msg) => write!(f, "Transport error: {msg}"),
}
}
}
impl std::error::Error for ToolContextError {}
impl From<ToolContextError> for Error {
fn from(err: ToolContextError) -> Self {
Error::internal_error(err.to_string())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateMessageRequest {
pub messages: Vec<SamplingMessage>,
pub max_tokens: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub model_preferences: Option<ModelPreferences>,
#[serde(skip_serializing_if = "Option::is_none")]
pub system_prompt: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop_sequences: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub temperature: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include_context: Option<IncludeContext>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<Value>,
}
impl Default for CreateMessageRequest {
fn default() -> Self {
Self {
messages: vec![],
max_tokens: 1000,
model_preferences: None,
system_prompt: None,
stop_sequences: None,
temperature: None,
include_context: None,
meta: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SamplingMessage {
pub role: SamplingRole,
pub content: SamplingContent,
}
impl SamplingMessage {
pub fn user(text: impl Into<String>) -> Self {
Self {
role: SamplingRole::User,
content: SamplingContent::Text { text: text.into() },
}
}
pub fn assistant(text: impl Into<String>) -> Self {
Self {
role: SamplingRole::Assistant,
content: SamplingContent::Text { text: text.into() },
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SamplingRole {
User,
Assistant,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum SamplingContent {
Text {
text: String,
},
Image {
data: String,
#[serde(rename = "mimeType")]
mime_type: String,
},
}
impl SamplingContent {
pub fn as_text(&self) -> Option<&str> {
match self {
Self::Text { text } => Some(text),
_ => None,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ModelPreferences {
#[serde(skip_serializing_if = "Option::is_none")]
pub cost_priority: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub speed_priority: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub intelligence_priority: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hints: Option<Vec<ModelHint>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelHint {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum IncludeContext {
None,
ThisServer,
AllServers,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateMessageResult {
pub role: SamplingRole,
pub content: SamplingContent,
pub model: String,
pub stop_reason: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ElicitationRequest {
pub message: String,
pub requested_schema: Value,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<Value>,
}
impl ElicitationRequest {
pub fn text(message: impl Into<String>) -> Self {
Self {
message: message.into(),
requested_schema: serde_json::json!({
"type": "object",
"properties": {
"value": { "type": "string" }
},
"required": ["value"]
}),
meta: None,
}
}
pub fn with_schema(message: impl Into<String>, schema: Value) -> Self {
Self {
message: message.into(),
requested_schema: schema,
meta: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ElicitationResult {
pub action: ElicitationAction,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<Value>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ElicitationAction {
Accept,
Decline,
Cancel,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LogNotificationParams {
pub level: LogLevel,
#[serde(skip_serializing_if = "Option::is_none")]
pub logger: Option<String>,
pub data: Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProgressNotificationParams {
pub progress_token: String,
pub progress: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub total: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
}
#[async_trait]
pub trait NotificationSender: Send + Sync {
async fn send_notification(&self, method: &str, params: Value) -> Result<(), ToolContextError>;
}
#[async_trait]
pub trait RequestSender: Send + Sync {
async fn send_request(
&self,
method: &str,
params: Value,
timeout: Duration,
) -> Result<Value, ToolContextError>;
}
#[async_trait]
pub trait ToolContext: Send + Sync {
async fn send_log(
&self,
level: LogLevel,
logger: Option<&str>,
data: Value,
) -> Result<(), ToolContextError>;
async fn send_progress(
&self,
progress: u64,
total: Option<u64>,
) -> Result<(), ToolContextError>;
async fn send_progress_with_message(
&self,
progress: u64,
total: Option<u64>,
message: String,
) -> Result<(), ToolContextError>;
async fn request_sampling(
&self,
request: CreateMessageRequest,
timeout: Duration,
) -> Result<CreateMessageResult, ToolContextError>;
async fn request_elicitation(
&self,
request: ElicitationRequest,
timeout: Duration,
) -> Result<ElicitationResult, ToolContextError>;
fn request_id(&self) -> &str;
fn tool_name(&self) -> &str;
fn progress_token(&self) -> Option<&str>;
fn session_id(&self) -> Option<&str>;
}
pub struct DefaultToolContext {
request_id: String,
tool_name: String,
progress_token: Option<String>,
session_id: Option<String>,
notification_sender: Arc<dyn NotificationSender>,
request_sender: Arc<dyn RequestSender>,
}
impl DefaultToolContext {
pub fn new(
request_id: impl Into<String>,
tool_name: impl Into<String>,
progress_token: Option<String>,
session_id: Option<String>,
notification_sender: Arc<dyn NotificationSender>,
request_sender: Arc<dyn RequestSender>,
) -> Self {
Self {
request_id: request_id.into(),
tool_name: tool_name.into(),
progress_token,
session_id,
notification_sender,
request_sender,
}
}
}
#[async_trait]
impl ToolContext for DefaultToolContext {
async fn send_log(
&self,
level: LogLevel,
logger: Option<&str>,
data: Value,
) -> Result<(), ToolContextError> {
let params = LogNotificationParams {
level,
logger: logger.map(String::from),
data,
};
let value = serde_json::to_value(¶ms)
.map_err(|e| ToolContextError::Serialization(e.to_string()))?;
self.notification_sender
.send_notification("notifications/message", value)
.await
}
async fn send_progress(
&self,
progress: u64,
total: Option<u64>,
) -> Result<(), ToolContextError> {
let Some(token) = &self.progress_token else {
return Ok(());
};
let params = ProgressNotificationParams {
progress_token: token.clone(),
progress,
total,
message: None,
};
let value = serde_json::to_value(¶ms)
.map_err(|e| ToolContextError::Serialization(e.to_string()))?;
self.notification_sender
.send_notification("notifications/progress", value)
.await
}
async fn send_progress_with_message(
&self,
progress: u64,
total: Option<u64>,
message: String,
) -> Result<(), ToolContextError> {
let Some(token) = &self.progress_token else {
return Ok(());
};
let params = ProgressNotificationParams {
progress_token: token.clone(),
progress,
total,
message: Some(message),
};
let value = serde_json::to_value(¶ms)
.map_err(|e| ToolContextError::Serialization(e.to_string()))?;
self.notification_sender
.send_notification("notifications/progress", value)
.await
}
async fn request_sampling(
&self,
request: CreateMessageRequest,
timeout: Duration,
) -> Result<CreateMessageResult, ToolContextError> {
let params = serde_json::to_value(&request)
.map_err(|e| ToolContextError::Serialization(e.to_string()))?;
let response = self
.request_sender
.send_request("sampling/createMessage", params, timeout)
.await?;
serde_json::from_value(response).map_err(|e| ToolContextError::Serialization(e.to_string()))
}
async fn request_elicitation(
&self,
request: ElicitationRequest,
timeout: Duration,
) -> Result<ElicitationResult, ToolContextError> {
let params = serde_json::to_value(&request)
.map_err(|e| ToolContextError::Serialization(e.to_string()))?;
let response = self
.request_sender
.send_request("elicitation/create", params, timeout)
.await?;
serde_json::from_value(response).map_err(|e| ToolContextError::Serialization(e.to_string()))
}
fn request_id(&self) -> &str {
&self.request_id
}
fn tool_name(&self) -> &str {
&self.tool_name
}
fn progress_token(&self) -> Option<&str> {
self.progress_token.as_deref()
}
fn session_id(&self) -> Option<&str> {
self.session_id.as_deref()
}
}
tokio::task_local! {
pub static TOOL_CONTEXT: Arc<dyn ToolContext>;
}
pub fn current_context() -> Arc<dyn ToolContext> {
TOOL_CONTEXT.with(|ctx| ctx.clone())
}
pub fn try_current_context() -> Option<Arc<dyn ToolContext>> {
TOOL_CONTEXT.try_with(|ctx| ctx.clone()).ok()
}
pub async fn with_context<F, T>(context: Arc<dyn ToolContext>, f: F) -> T
where
F: std::future::Future<Output = T>,
{
TOOL_CONTEXT.scope(context, f).await
}
use pulseengine_mcp_transport::{
NotificationSender as StreamingNotificationSender, StreamingNotification, Transport,
TransportError,
};
pub struct TransportBridge {
transport: Arc<dyn Transport>,
session_id: Option<String>,
streaming_sender: Option<StreamingNotificationSender>,
}
impl TransportBridge {
pub fn new(transport: Arc<dyn Transport>, session_id: Option<String>) -> Self {
let streaming_sender = pulseengine_mcp_transport::try_notification_sender();
Self {
transport,
session_id,
streaming_sender,
}
}
}
#[async_trait]
impl NotificationSender for TransportBridge {
async fn send_notification(&self, method: &str, params: Value) -> Result<(), ToolContextError> {
eprintln!(
"[DEBUG] TransportBridge::send_notification: method={}, session_id={:?}, has_streaming_sender={}",
method,
self.session_id,
self.streaming_sender.is_some()
);
tracing::debug!(
method = %method,
session_id = ?self.session_id,
has_streaming_sender = self.streaming_sender.is_some(),
"TransportBridge: sending notification"
);
if let Some(ref sender) = self.streaming_sender {
let notification = StreamingNotification {
id: None, method: method.to_string(),
params: params.clone(),
};
if sender.send(notification).is_ok() {
eprintln!(
"[DEBUG] Notification sent via captured streaming channel: method={method}"
);
tracing::debug!(method = %method, "Notification sent via captured streaming channel");
return Ok(());
}
eprintln!("[DEBUG] Captured streaming channel closed, falling back: method={method}");
}
eprintln!("[DEBUG] Falling back to transport notification: method={method}");
let result = self
.transport
.send_notification(self.session_id.as_deref(), method, params)
.await;
match &result {
Ok(()) => {
eprintln!("[DEBUG] Notification sent successfully via transport: method={method}");
tracing::debug!(method = %method, "Notification sent successfully via transport");
}
Err(e) => {
eprintln!("[DEBUG] Notification failed: method={method}, error={e}");
tracing::warn!(method = %method, error = %e, "Notification failed");
}
}
result.map_err(|e| match e {
TransportError::SessionNotFound(id) => {
ToolContextError::NotificationFailed(format!("Session not found: {id}"))
}
TransportError::ChannelClosed => {
ToolContextError::NotificationFailed("Channel closed".to_string())
}
TransportError::NotSupported(msg) => {
ToolContextError::NotificationFailed(format!("Not supported: {msg}"))
}
other => ToolContextError::Transport(other.to_string()),
})
}
}
#[async_trait]
impl RequestSender for TransportBridge {
async fn send_request(
&self,
method: &str,
params: Value,
timeout: Duration,
) -> Result<Value, ToolContextError> {
let request_id = uuid::Uuid::new_v4().to_string();
eprintln!(
"[DEBUG] TransportBridge::send_request: method={}, request_id={}, has_streaming_sender={}",
method,
request_id,
self.streaming_sender.is_some()
);
if let Some(ref sender) = self.streaming_sender {
let response_rx = self.transport.register_pending_request(&request_id);
if let Some(rx) = response_rx {
let request = StreamingNotification {
id: Some(request_id.clone()),
method: method.to_string(),
params: params.clone(),
};
if sender.send(request).is_ok() {
eprintln!(
"[DEBUG] Request sent via streaming channel: method={method}, id={request_id}"
);
match tokio::time::timeout(timeout, rx).await {
Ok(Ok(response)) => {
eprintln!("[DEBUG] Received response for request {request_id}");
if let Some(error) = response.get("error") {
return Err(ToolContextError::RequestFailed(error.to_string()));
}
return Ok(response);
}
Ok(Err(_)) => {
eprintln!("[DEBUG] Response channel closed for request {request_id}");
return Err(ToolContextError::RequestFailed(
"Response channel closed".to_string(),
));
}
Err(_) => {
eprintln!(
"[DEBUG] Timeout waiting for response to request {request_id}"
);
return Err(ToolContextError::Timeout);
}
}
}
eprintln!(
"[DEBUG] Streaming channel send failed for request {request_id}, falling back"
);
} else {
eprintln!("[DEBUG] Could not register pending request {request_id}, falling back");
}
}
eprintln!("[DEBUG] Falling back to transport.send_request: method={method}");
self.transport
.send_request(self.session_id.as_deref(), method, params, timeout)
.await
.map_err(|e| match e {
TransportError::SessionNotFound(id) => {
ToolContextError::RequestFailed(format!("Session not found: {id}"))
}
TransportError::Timeout => ToolContextError::Timeout,
TransportError::ChannelClosed => {
ToolContextError::RequestFailed("Channel closed".to_string())
}
TransportError::NotSupported(msg) => {
ToolContextError::RequestFailed(format!("Not supported: {msg}"))
}
other => ToolContextError::Transport(other.to_string()),
})
}
}
pub fn create_tool_context(
transport: Arc<dyn Transport>,
request_id: impl Into<String>,
tool_name: impl Into<String>,
progress_token: Option<String>,
session_id: Option<String>,
) -> Arc<dyn ToolContext> {
let bridge = Arc::new(TransportBridge::new(
Arc::clone(&transport),
session_id.clone(),
));
Arc::new(DefaultToolContext::new(
request_id,
tool_name,
progress_token,
session_id,
bridge.clone(),
bridge,
))
}
pub struct NoOpToolContext {
request_id: String,
tool_name: String,
}
impl NoOpToolContext {
pub fn new(request_id: impl Into<String>, tool_name: impl Into<String>) -> Self {
Self {
request_id: request_id.into(),
tool_name: tool_name.into(),
}
}
}
#[async_trait]
impl ToolContext for NoOpToolContext {
async fn send_log(
&self,
_level: LogLevel,
_logger: Option<&str>,
_data: Value,
) -> Result<(), ToolContextError> {
Ok(())
}
async fn send_progress(
&self,
_progress: u64,
_total: Option<u64>,
) -> Result<(), ToolContextError> {
Ok(())
}
async fn send_progress_with_message(
&self,
_progress: u64,
_total: Option<u64>,
_message: String,
) -> Result<(), ToolContextError> {
Ok(())
}
async fn request_sampling(
&self,
_request: CreateMessageRequest,
_timeout: Duration,
) -> Result<CreateMessageResult, ToolContextError> {
Err(ToolContextError::NotAvailable)
}
async fn request_elicitation(
&self,
_request: ElicitationRequest,
_timeout: Duration,
) -> Result<ElicitationResult, ToolContextError> {
Err(ToolContextError::NotAvailable)
}
fn request_id(&self) -> &str {
&self.request_id
}
fn tool_name(&self) -> &str {
&self.tool_name
}
fn progress_token(&self) -> Option<&str> {
None
}
fn session_id(&self) -> Option<&str> {
None
}
}
#[cfg(test)]
pub mod mock {
use super::*;
use std::sync::Mutex;
pub struct MockToolContext {
request_id: String,
tool_name: String,
progress_token: Option<String>,
pub logs: Mutex<Vec<LogNotificationParams>>,
pub progress: Mutex<Vec<ProgressNotificationParams>>,
pub sampling_response: Mutex<Option<CreateMessageResult>>,
pub elicitation_response: Mutex<Option<ElicitationResult>>,
}
impl MockToolContext {
pub fn new(tool_name: impl Into<String>) -> Self {
Self {
request_id: uuid::Uuid::new_v4().to_string(),
tool_name: tool_name.into(),
progress_token: Some("test-progress-token".to_string()),
logs: Mutex::new(vec![]),
progress: Mutex::new(vec![]),
sampling_response: Mutex::new(None),
elicitation_response: Mutex::new(None),
}
}
pub fn with_progress_token(tool_name: impl Into<String>, token: impl Into<String>) -> Self {
Self {
request_id: uuid::Uuid::new_v4().to_string(),
tool_name: tool_name.into(),
progress_token: Some(token.into()),
logs: Mutex::new(vec![]),
progress: Mutex::new(vec![]),
sampling_response: Mutex::new(None),
elicitation_response: Mutex::new(None),
}
}
pub fn set_sampling_response(&self, response: CreateMessageResult) {
*self.sampling_response.lock().unwrap() = Some(response);
}
pub fn set_elicitation_response(&self, response: ElicitationResult) {
*self.elicitation_response.lock().unwrap() = Some(response);
}
pub fn get_logs(&self) -> Vec<LogNotificationParams> {
self.logs.lock().unwrap().clone()
}
pub fn get_progress(&self) -> Vec<ProgressNotificationParams> {
self.progress.lock().unwrap().clone()
}
}
#[async_trait]
impl ToolContext for MockToolContext {
async fn send_log(
&self,
level: LogLevel,
logger: Option<&str>,
data: Value,
) -> Result<(), ToolContextError> {
self.logs.lock().unwrap().push(LogNotificationParams {
level,
logger: logger.map(String::from),
data,
});
Ok(())
}
async fn send_progress(
&self,
progress: u64,
total: Option<u64>,
) -> Result<(), ToolContextError> {
if let Some(token) = &self.progress_token {
self.progress
.lock()
.unwrap()
.push(ProgressNotificationParams {
progress_token: token.clone(),
progress,
total,
message: None,
});
}
Ok(())
}
async fn send_progress_with_message(
&self,
progress: u64,
total: Option<u64>,
message: String,
) -> Result<(), ToolContextError> {
if let Some(token) = &self.progress_token {
self.progress
.lock()
.unwrap()
.push(ProgressNotificationParams {
progress_token: token.clone(),
progress,
total,
message: Some(message),
});
}
Ok(())
}
async fn request_sampling(
&self,
_request: CreateMessageRequest,
_timeout: Duration,
) -> Result<CreateMessageResult, ToolContextError> {
self.sampling_response
.lock()
.unwrap()
.clone()
.ok_or(ToolContextError::NotAvailable)
}
async fn request_elicitation(
&self,
_request: ElicitationRequest,
_timeout: Duration,
) -> Result<ElicitationResult, ToolContextError> {
self.elicitation_response
.lock()
.unwrap()
.clone()
.ok_or(ToolContextError::NotAvailable)
}
fn request_id(&self) -> &str {
&self.request_id
}
fn tool_name(&self) -> &str {
&self.tool_name
}
fn progress_token(&self) -> Option<&str> {
self.progress_token.as_deref()
}
fn session_id(&self) -> Option<&str> {
None
}
}
}