use rmcp::model::LoggingLevel;
use serde_json::Value;
use std::cell::RefCell;
use std::future::Future;
use tokio::sync::mpsc;
tokio::task_local! {
static TASK_MCP_TASK_ID: RefCell<Option<String>>;
}
thread_local! {
static THREAD_MCP_TASK_ID: RefCell<Option<String>> = const { RefCell::new(None) };
}
#[derive(Debug, Clone, PartialEq)]
pub struct LoggingMessageNotificationParams {
pub level: LoggingLevel,
pub logger: Option<String>,
pub data: Value,
pub meta: Option<serde_json::Map<String, Value>>,
}
pub struct McpTaskIdGuard {
previous: Option<String>,
}
impl McpTaskIdGuard {
pub fn new(task_id: impl Into<String>) -> Self {
let previous = THREAD_MCP_TASK_ID.with(|slot| {
let prev = slot.borrow().clone();
*slot.borrow_mut() = Some(task_id.into());
prev
});
Self { previous }
}
}
impl Drop for McpTaskIdGuard {
fn drop(&mut self) {
THREAD_MCP_TASK_ID.with(|slot| {
*slot.borrow_mut() = self.previous.take();
});
}
}
pub async fn run_with_mcp_task_id<F, T>(task_id: String, f: F) -> T
where
F: Future<Output = T>,
{
let thread_previous = THREAD_MCP_TASK_ID.with(|slot| {
let prev = slot.borrow().clone();
*slot.borrow_mut() = Some(task_id.clone());
prev
});
let out = TASK_MCP_TASK_ID.scope(RefCell::new(Some(task_id)), f).await;
THREAD_MCP_TASK_ID.with(|slot| {
*slot.borrow_mut() = thread_previous;
});
out
}
pub fn current_mcp_task_id() -> Option<String> {
TASK_MCP_TASK_ID
.try_with(|slot| slot.borrow().clone())
.ok()
.flatten()
.or_else(|| THREAD_MCP_TASK_ID.with(|slot| slot.borrow().clone()))
}
fn log_meta_with_task_id() -> Option<serde_json::Map<String, Value>> {
current_mcp_task_id().map(|task_id| {
let mut meta = serde_json::Map::new();
meta.insert("taskId".into(), Value::String(task_id));
meta
})
}
pub fn level_to_mcp(level: &str) -> LoggingLevel {
match level {
"trace" | "debug" => LoggingLevel::Debug,
"info" => LoggingLevel::Info,
"notice" | "warn" | "warning" => LoggingLevel::Notice,
"error" => LoggingLevel::Error,
"critical" => LoggingLevel::Critical,
"alert" => LoggingLevel::Alert,
"emergency" => LoggingLevel::Emergency,
_ => LoggingLevel::Info,
}
}
pub fn log_channel(
buffer: usize,
) -> (
mpsc::Sender<LoggingMessageNotificationParams>,
mpsc::Receiver<LoggingMessageNotificationParams>,
) {
mpsc::channel(buffer)
}
pub fn log_params(
level: LoggingLevel,
logger: Option<String>,
message: impl Into<Value>,
) -> LoggingMessageNotificationParams {
LoggingMessageNotificationParams {
data: message.into(),
level,
logger,
meta: log_meta_with_task_id(),
}
}
#[cfg(feature = "tracing")]
mod tracing_layer {
use super::*;
use tracing::Subscriber;
use tracing_subscriber::Layer;
use tracing_subscriber::layer::Context;
#[derive(Clone)]
pub struct ClapMcpTracingLayer {
tx: mpsc::Sender<LoggingMessageNotificationParams>,
logger_name: String,
}
impl ClapMcpTracingLayer {
pub fn new(tx: mpsc::Sender<LoggingMessageNotificationParams>) -> Self {
Self {
tx,
logger_name: "app".to_string(),
}
}
pub fn with_logger_name(mut self, name: impl Into<String>) -> Self {
self.logger_name = name.into();
self
}
}
impl<S> Layer<S> for ClapMcpTracingLayer
where
S: Subscriber,
{
fn on_event(&self, event: &tracing::Event<'_>, _ctx: Context<'_, S>) {
let mut visitor = LogVisitor::default();
event.record(&mut visitor);
let message = visitor.message.unwrap_or_else(|| format!("{:?}", event));
let level = level_to_mcp(match *event.metadata().level() {
tracing::Level::TRACE => "trace",
tracing::Level::DEBUG => "debug",
tracing::Level::INFO => "info",
tracing::Level::WARN => "warn",
tracing::Level::ERROR => "error",
});
let params = log_params(level, Some(self.logger_name.clone()), message);
let _ = self.tx.try_send(params);
}
}
#[derive(Default)]
struct LogVisitor {
message: Option<String>,
}
impl tracing::field::Visit for LogVisitor {
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
if field.name() == "message" {
self.message = Some(format!("{:?}", value));
}
}
fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
if field.name() == "message" {
self.message = Some(value.to_string());
}
}
}
}
#[cfg(feature = "tracing")]
pub use tracing_layer::ClapMcpTracingLayer;
#[cfg(feature = "log")]
mod log_bridge {
use super::*;
use log::Log;
use std::sync::Arc;
pub struct ClapMcpLogBridge {
tx: Arc<mpsc::Sender<LoggingMessageNotificationParams>>,
logger_name: String,
}
impl ClapMcpLogBridge {
pub fn new(tx: mpsc::Sender<LoggingMessageNotificationParams>) -> Self {
Self {
tx: Arc::new(tx),
logger_name: "app".to_string(),
}
}
pub fn with_logger_name(mut self, name: impl Into<String>) -> Self {
self.logger_name = name.into();
self
}
}
impl Log for ClapMcpLogBridge {
fn enabled(&self, _metadata: &log::Metadata) -> bool {
true
}
fn log(&self, record: &log::Record) {
let level = level_to_mcp(match record.level() {
log::Level::Trace => "trace",
log::Level::Debug => "debug",
log::Level::Info => "info",
log::Level::Warn => "warn",
log::Level::Error => "error",
});
let message = record.args().to_string();
let params = log_params(level, Some(self.logger_name.clone()), message);
let _ = self.tx.try_send(params);
}
fn flush(&self) {}
}
}
#[cfg(feature = "log")]
pub use log_bridge::ClapMcpLogBridge;