Skip to main content

Logging

Neva integrates with Rust's tracing ecosystem to emit structured log messages. When a client asks for them, these log messages are forwarded to it as MCP log notifications (notifications/message).

Logging is Request-Scoped​

MCP 2026-07-28 removed the logging/setLevel handshake. There is no global log level to negotiate any more — instead each request opts in for itself, by carrying the desired minimum severity in _meta["io.modelcontextprotocol/logLevel"].

While the server handles that request, it emits notifications/message at or above the requested severity and suppresses the rest. A request with no requested level produces no log notifications at all.

notifications/message itself is kept by the spec, but is deprecated — it exists for migration. For anything operational, prefer your host's own telemetry pipeline.

Under legacy-spec

The global model comes back: logging/setLevel, plus with_logging(handle) and set_log_level(). Those APIs do not exist in the default build. See Legacy spec.

Setup​

Add Neva's notification layer to your tracing_subscriber registry. No reload handle and no with_logging() registration are needed — the layer resolves each event's request-scoped level on its own:

use neva::prelude::*;
use neva::types::notification;
use tracing_subscriber::prelude::*;

#[tokio::main]
async fn main() {
tracing_subscriber::registry()
.with(notification::fmt::layer()) // Route logs to the requesting client
.init();

App::new()
.with_options(|opt| opt.with_default_http())
.run()
.await;
}

Over stdio, use Neva's NotificationFormatter instead — every supported stdio setup keeps working unchanged, including a formatter-only subscriber:

tracing_subscriber::registry()
.with(tracing_subscriber::fmt::layer()
.event_format(notification::NotificationFormatter))
.init();

If you need the request-scoped level resolved from a typed span extension rather than from the formatter itself, add notification::fmt::span_context() alongside your own layers.

Delivery​

Request-scoped notifications flow on the originating request's response stream, per the spec:

TransportHow they arrive
stdioInterleaved on stdout
Streamable HTTPThe POST that opted in gets a text/event-stream reply carrying its notifications/message and notifications/progress, followed by the response

Every other POST stays a single JSON object — except a subscriptions/listen request, which streams for its own reasons. Logs are not subscribable: they belong to the request that asked for them, so a listen stream carries its own request-scoped logs and no one else's.

The suppression rule — no logLevel, no notifications/message — holds on every transport.

Emitting Log Messages from Tools​

Use standard tracing macros inside your handlers:

#[tool]
async fn my_tool() {
tracing::info!(logger = "my_tool", "Processing started");
tracing::warn!(logger = "my_tool", "Something looks off");
tracing::debug!(logger = "my_tool", "Debug details here");
}

The optional logger field is forwarded to the client as part of the notification payload, allowing clients to identify the source of each log entry.

Log Levels​

Neva maps tracing severity levels to MCP log levels as follows:

tracing levelMCP log level
ERRORerror
WARNwarning
INFOinfo
DEBUGdebug
TRACEdebug

Requesting Logs from the Client​

A Neva client asks for logs with McpOptions::with_log_level, which stamps the level onto every request's _meta:

use neva::prelude::*;
use neva::types::notification::LoggingLevel;

#[allow(deprecated)]
let mut client = Client::new()
.with_options(|opt| opt
.with_log_level(LoggingLevel::Info)
.with_default_http());

The method carries #[deprecated] on arrival, mirroring the schema.

Progress Notifications via Tracing​

For long-running tools, Neva also uses tracing to emit progress notifications (notifications/progress). See the Progress guide for details.

Learn By Example​

Here you may find the full example.