use crate::shared::MessageRegistry;
#[cfg(not(feature = "legacy-spec"))]
use crate::types::Message;
#[cfg(not(feature = "legacy-spec"))]
use crate::types::notification::LoggingLevel;
use crate::types::notification::{Notification, formatter::build_notification};
use once_cell::sync::Lazy;
use std::io::{self, Write};
use tokio::sync::mpsc::{Sender, channel};
use tracing::{
field::Field,
span::Attributes,
{Event, Id, Subscriber, field::Visit},
};
use tracing_subscriber::{
registry::LookupSpan,
{Layer, layer::Context},
};
const MCP_SESSION_ID: &str = "mcp_session_id";
#[cfg(not(feature = "legacy-spec"))]
pub(super) const MCP_LOG_LEVEL: &str = "mcp_log_level";
pub(crate) static LOG_REGISTRY: Lazy<MessageRegistry> = Lazy::new(MessageRegistry::new);
#[cfg(not(feature = "legacy-spec"))]
pub(crate) static REQUEST_NOTIFICATIONS: Lazy<dashmap::DashMap<uuid::Uuid, Sender<Message>>> =
Lazy::new(dashmap::DashMap::new);
#[cfg(all(not(feature = "legacy-spec"), feature = "http-server"))]
pub(crate) fn register_request_sink(
id: uuid::Uuid,
capacity: usize,
) -> tokio::sync::mpsc::Receiver<Message> {
let (tx, rx) = channel::<Message>(capacity);
REQUEST_NOTIFICATIONS.insert(id, tx);
rx
}
#[cfg(all(not(feature = "legacy-spec"), feature = "http-server"))]
pub(crate) fn unregister_request_sink(id: &uuid::Uuid) {
REQUEST_NOTIFICATIONS.remove(id);
}
pub fn layer() -> MpscLayer {
let (tx, mut rx) = channel::<Notification>(100);
tokio::spawn(async move {
while let Some(notification) = rx.recv().await {
let _ = LOG_REGISTRY.send(notification.into());
}
});
MpscLayer {
sender: NotificationSender::new(tx),
}
}
#[derive(Debug)]
struct NotificationSender {
sender: Sender<Notification>,
}
impl NotificationSender {
fn new(sender: Sender<Notification>) -> Self {
Self { sender }
}
fn send_notification(&self, notification: Notification) {
let _ = self.sender.try_send(notification);
}
}
#[derive(Debug)]
pub struct MpscLayer {
sender: NotificationSender,
}
impl<S> Layer<S> for MpscLayer
where
S: Subscriber + for<'a> LookupSpan<'a>,
{
#[inline]
fn on_new_span(&self, attrs: &Attributes<'_>, id: &Id, ctx: Context<'_, S>) {
record_span_context(attrs, id, &ctx);
}
#[inline]
fn on_event(&self, event: &Event<'_>, ctx: Context<'_, S>) {
let notification = build_notification(event);
if let Some(span) = ctx.event_span(event) {
let mut notification = notification;
notification.session_id = span
.scope()
.find_map(|s| s.extensions().get::<uuid::Uuid>().cloned());
#[cfg(not(feature = "legacy-spec"))]
if notification.method.as_str() == crate::types::notification::commands::MESSAGE {
let requested = span.scope().find_map(|s| {
s.extensions()
.get::<super::formatter::MinLogSeverity>()
.map(|m| m.0)
});
let event_severity = super::formatter::notification_severity(¬ification)
.unwrap_or_else(|| LoggingLevel::from(event.metadata().level()).severity());
if !super::formatter::message_delivered(requested, event_severity) {
return;
}
}
#[cfg(not(feature = "legacy-spec"))]
if let Some(session_id) = notification.session_id
&& let Some(sink) = REQUEST_NOTIFICATIONS.get(&session_id)
{
let _ = sink.try_send(Message::Notification(notification));
return;
}
self.sender.send_notification(notification);
} else {
let mut stderr = io::stderr();
let json = serde_json::to_string(¬ification).unwrap();
let _ = writeln!(stderr, "{json}");
}
}
}
#[derive(Debug, Default)]
pub struct SpanContextLayer;
impl<S> Layer<S> for SpanContextLayer
where
S: Subscriber + for<'a> LookupSpan<'a>,
{
#[inline]
fn on_new_span(&self, attrs: &Attributes<'_>, id: &Id, ctx: Context<'_, S>) {
record_span_context(attrs, id, &ctx);
}
}
pub fn span_context() -> SpanContextLayer {
SpanContextLayer
}
#[inline]
fn record_span_context<S>(attrs: &Attributes<'_>, id: &Id, ctx: &Context<'_, S>)
where
S: Subscriber + for<'a> LookupSpan<'a>,
{
let mut visitor = SpanVisitor::default();
attrs.record(&mut visitor);
if let Some(span) = ctx.span(id) {
if let Some(mcp_session_id) = visitor.session_id {
span.extensions_mut().insert(mcp_session_id);
}
#[cfg(not(feature = "legacy-spec"))]
if let Some(min) = visitor.min_severity {
span.extensions_mut()
.insert(super::formatter::MinLogSeverity(min));
}
}
}
#[derive(Default)]
struct SpanVisitor {
session_id: Option<uuid::Uuid>,
#[cfg(not(feature = "legacy-spec"))]
min_severity: Option<u8>,
}
impl Visit for SpanVisitor {
#[inline]
fn record_str(&mut self, field: &Field, value: &str) {
if field.name() == MCP_SESSION_ID
&& let Ok(session_id) = uuid::Uuid::parse_str(value)
{
self.session_id = Some(session_id);
}
}
#[cfg(not(feature = "legacy-spec"))]
#[inline]
fn record_u64(&mut self, field: &Field, value: u64) {
if field.name() == MCP_LOG_LEVEL {
self.min_severity = Some(value as u8);
}
}
fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
if field.name() == MCP_SESSION_ID && self.session_id.is_none() {
let formatted = format!("{value:?}");
let stripped = formatted
.strip_prefix('"')
.and_then(|s| s.strip_suffix('"'))
.unwrap_or(&formatted);
if let Ok(session_id) = uuid::Uuid::parse_str(stripped) {
self.session_id = Some(session_id);
}
}
}
}
#[cfg(all(test, not(feature = "legacy-spec")))]
mod tests {
use crate::types::notification::{LoggingLevel, NotificationFormatter};
use std::io::Write;
use std::sync::{Arc, Mutex};
use tracing_subscriber::fmt::MakeWriter;
use tracing_subscriber::prelude::*;
#[derive(Clone)]
struct BufWriter(Arc<Mutex<Vec<u8>>>);
struct BufGuard(Arc<Mutex<Vec<u8>>>);
impl Write for BufGuard {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.0.lock().unwrap().extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
impl<'a> MakeWriter<'a> for BufWriter {
type Writer = BufGuard;
fn make_writer(&'a self) -> Self::Writer {
BufGuard(self.0.clone())
}
}
fn emit_within_request(log_level: Option<LoggingLevel>) -> Vec<String> {
let buf = Arc::new(Mutex::new(Vec::new()));
let subscriber = tracing_subscriber::registry()
.with(super::span_context())
.with(
tracing_subscriber::fmt::layer()
.event_format(NotificationFormatter)
.with_writer(BufWriter(buf.clone())),
);
tracing::subscriber::with_default(subscriber, || {
let span = match log_level {
Some(level) => {
tracing::info_span!("request", mcp_log_level = u64::from(level.severity()))
}
None => tracing::info_span!("request"),
};
let _entered = span.enter();
tracing::error!(logger = "tool", "error message");
tracing::warn!(logger = "tool", "warning message");
tracing::info!(logger = "tool", "info message");
tracing::debug!(logger = "tool", "debug message");
});
let raw = buf.lock().unwrap().clone();
String::from_utf8(raw)
.unwrap()
.lines()
.filter(|l| !l.trim().is_empty())
.map(str::to_owned)
.collect()
}
fn levels(lines: &[String]) -> Vec<String> {
lines
.iter()
.filter_map(|l| serde_json::from_str::<serde_json::Value>(l).ok())
.filter(|v| v["method"] == "notifications/message")
.filter_map(|v| v["params"]["level"].as_str().map(str::to_owned))
.collect()
}
#[test]
fn delivers_messages_at_or_above_requested_level() {
let lines = emit_within_request(Some(LoggingLevel::Warning));
let got = levels(&lines);
assert!(got.contains(&"error".to_owned()), "got: {got:?}");
assert!(got.contains(&"warning".to_owned()), "got: {got:?}");
assert!(!got.contains(&"info".to_owned()), "got: {got:?}");
assert!(!got.contains(&"debug".to_owned()), "got: {got:?}");
}
#[test]
fn delivers_everything_at_debug() {
let got = levels(&emit_within_request(Some(LoggingLevel::Debug)));
for lvl in ["error", "warning", "info", "debug"] {
assert!(got.contains(&lvl.to_owned()), "missing {lvl}, got: {got:?}");
}
}
#[test]
fn suppresses_all_messages_without_requested_level() {
let got = levels(&emit_within_request(None));
assert!(got.is_empty(), "expected none, got: {got:?}");
}
#[test]
fn preserves_mcp_specific_severity_past_tracing() {
use crate::types::notification::LogMessage;
let buf = Arc::new(Mutex::new(Vec::new()));
let subscriber = tracing_subscriber::registry()
.with(super::span_context())
.with(
tracing_subscriber::fmt::layer()
.event_format(NotificationFormatter)
.with_writer(BufWriter(buf.clone())),
);
tracing::subscriber::with_default(subscriber, || {
let span = tracing::info_span!(
"request",
mcp_log_level = u64::from(LoggingLevel::Emergency.severity())
);
let _entered = span.enter();
LogMessage::new(LoggingLevel::Emergency, None, None).write();
LogMessage::new(LoggingLevel::Error, None, None).write();
});
let raw = buf.lock().unwrap().clone();
let got: Vec<String> = String::from_utf8(raw)
.unwrap()
.lines()
.filter(|l| !l.trim().is_empty())
.map(str::to_owned)
.collect();
let got = levels(&got);
assert!(got.contains(&"emergency".to_owned()), "got: {got:?}");
assert!(!got.contains(&"error".to_owned()), "got: {got:?}");
}
#[tokio::test]
async fn routes_events_from_nested_spans_to_the_request_sink() {
use crate::types::Message;
use crate::types::notification::Notification;
let session_id = uuid::Uuid::new_v4();
let (sink_tx, mut sink_rx) = tokio::sync::mpsc::channel::<Message>(8);
super::REQUEST_NOTIFICATIONS.insert(session_id, sink_tx);
let (fallback_tx, mut fallback_rx) = tokio::sync::mpsc::channel::<Notification>(8);
let subscriber = tracing_subscriber::registry().with(super::MpscLayer {
sender: super::NotificationSender::new(fallback_tx),
});
tracing::subscriber::with_default(subscriber, || {
let request = tracing::info_span!(
"request",
mcp_session_id = session_id.to_string(),
mcp_log_level = u64::from(LoggingLevel::Debug.severity())
);
let _entered = request.enter();
let handler = tracing::info_span!("handler");
let _handler = handler.enter();
tracing::warn!(logger = "tool", "nested message");
});
super::REQUEST_NOTIFICATIONS.remove(&session_id);
let msg = sink_rx
.try_recv()
.expect("an event from a nested span must still reach the request sink");
let json = serde_json::to_value(&msg).unwrap();
assert_eq!(json["method"], "notifications/message");
assert_eq!(json["params"]["data"]["message"], "nested message");
assert!(
fallback_rx.try_recv().is_err(),
"request-scoped notification leaked to the legacy path"
);
}
}