use rust_mcp_sdk::schema::{LoggingLevel, LoggingMessageNotificationParams};
use serde_json::Value;
use tokio::sync::mpsc;
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: None,
}
}
#[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;