Structured logging notifications from MCP servers
Declare the server’s logging capability, emit `notifications/message` objects with a severity, optional logger name, and bounded structured data, then let the client set its minimum level with `logging/setLevel`. Keep raw logs out of the transport stream, route notifications separately, and redact credentials and tool payloads before emission.
Prerequisites
Implement both sides against MCP protocol revision 2025-11-25. The server needs to advertise the logging capability during initialization, and the client needs a handler for server-to-client JSON-RPC notifications. The 2025-11-25 MCP logging utility defines the capability, severity ordering, level-setting request, and notification method used below.
If the server uses standard input and output, treat stdout as protocol-only from the start. The distinction matters most when running an MCP server over the standard input and output transport, because an ordinary log line written there can no longer be decoded as a JSON-RPC message.
-
Advertise logging and register the client handler
Include an empty
loggingobject in the server capabilities returned during initialization:{ "capabilities": { "logging": {} } }Do this before sending log notifications. The capability tells the client that the server implements the logging utility; without it, the client has no negotiated basis for sending
logging/setLevelor expectingnotifications/message.On the client, register
notifications/messageas its own incoming method. Do not send a response to it: it is a JSON-RPC notification, so it has no request ID. Route it to the client’s log viewer, diagnostic sink, or stderr handler instead of the tool-result path. That separation prevents an operational event from being mistaken for returned model context. -
Emit the schema-defined notification, not a formatted line
Build every server log event as a
notifications/messageenvelope. The 2025-11-25 MCP schema reference defines three relevant parameters: requiredlevel, optionallogger, and requireddata.datamay be any JSON-serializable value, including a string or object.{ "jsonrpc": "2.0", "method": "notifications/message", "params": { "level": "warning", "logger": "asset-import", "data": { "event": "input_rejected", "reason": "unsupported_media_type" } } }Use
levelfor severity, not for the subsystem name. Useloggerwhen the client would benefit from filtering several components independently; omit it when the server has only one meaningful source. Put machine-readable event details indatarather than flattening them into a sentence that the client would have to parse again.Arbitrary does not mean unlimited. A stable object shape makes downstream filtering easier, while accepting every local object encourages accidental payload and secret leakage. Define a small allowlist of fields for each event before connecting the logging call to tool execution.
-
Let the client choose its minimum level
After initialization, the client can set the minimum logging level it wants the server to emit by sending
logging/setLevel:{ "jsonrpc": "2.0", "id": 7, "method": "logging/setLevel", "params": { "level": "warning" } }The 2025-11-25 levels, from least to most severe, are
debug,info,notice,warning,error,critical,alert, andemergency. Withwarningselected, the server should sendwarningand every more severe level, while suppressingdebug,info, andnoticenotifications.Apply the filter before serializing and sending the notification. Filtering only in the client still spends transport and parsing work on messages the client declined. A low threshold such as
debugsuits a bounded diagnosis because it exposes the most detail; it is the wrong standing choice when the extra events obscure failures or increase log volume. A higher threshold reduces noise but can hide the sequence leading to a failure.Do not rely on an implicit default. If the client has not sent
logging/setLevel, the specification permits the server to choose which messages it sends. A client that needs repeatable behavior should therefore set its threshold explicitly after initialization and again after establishing a new connection. -
Keep protocol logs distinct from failures and raw output
A server log notification reports an event; it is not a tool result and does not itself indicate that a JSON-RPC request failed. Keep it separate from the response and error paths described in distinguishing JSON-RPC protocol errors from MCP tool failures. An
error-level notification can accompany a failed operation, but its severity label does not replace the operation’s defined response.The notification travels as a valid JSON-RPC envelope. The server must not write human-readable log text as stray bytes into the JSON-RPC transport stream. For a stdio server, send the structured notification through the same protocol writer and reserve ordinary stderr for local process diagnostics. For other transports, use the transport’s normal JSON-RPC notification mechanism rather than injecting text around encoded messages.
This costs a little routing code on both sides, but it preserves framing and lets the client filter by level and logger without scraping console text. It is the wrong place for diagnostics intended only for the server operator; those belong in the server’s own logging sink rather than being sent to every connected client.
-
Redact sensitive context and bound the data field
Treat
dataas a deliberately small diagnostic record. MCP tool arguments, resource contents, and results can contain user text, credentials, internal paths, or other sensitive context. Do not copy a complete request or tool result into a notification. Large tool payloads also duplicate data on the connection and recreate the same handling problem as MCP tool results large enough to overflow the context window.The OWASP Logging Cheat Sheet’s data-exclusion guidance, accessed 2026-08-26 says access tokens, authentication passwords, database connection strings, encryption keys, session identifiers, and sensitive personal data should usually be removed, masked, sanitized, hashed, or encrypted rather than recorded directly. Apply that rule before constructing the notification, not after the client has stored it.
Prefer an event name, outcome, component name, and an existing correlation identifier over bodies and credentials. Include a count or size only when the server already knows it; do not manufacture measurements for the log. Bound strings and collections, and replace omitted content with an explicit marker such as
payload_omitted: trueso an operator knows the absence is deliberate.Redaction gives up some one-message debugging detail. That is the correct trade when the alternative is placing secrets or entire tool payloads into every downstream log sink. If a field cannot be made safe and still useful, leave it out.
-
Verify filtering, framing, and redaction together
Test the connection with at least two thresholds. Set
debugand confirm that a debug fixture arrives; setwarningand confirm that the same fixture is suppressed while a warning fixture arrives. Verify a message both with and withoutlogger, and testdataas a string and as an object because both are allowed.Inspect the raw transport during the test. It should contain complete JSON-RPC envelopes with no unframed console lines. Confirm that log notifications never enter tool results or JSON-RPC error handling. Finally, run representative sensitive and oversized tool inputs through the logging path and assert that credentials, request bodies, resource contents, and full tool results are absent.
Expected result
Done means the server advertises logging, the client explicitly selects a minimum severity, and the server emits only schema-shaped notifications/message events at that level or higher. Each event has a level, may have a logger name, and carries bounded, redacted data. The JSON-RPC stream contains no raw log lines, credentials, or copied tool payloads.
Sources
- 2025-11-25 MCP logging utilitymodelcontextprotocol.io
- 2025-11-25 MCP schema referencemodelcontextprotocol.io
- OWASP Logging Cheat Sheet’s data-exclusion guidance, accessed 2026-08-26cheatsheetseries.owasp.org
See also
Implement MCP 2026-07-28 multi round-trip requests with safe retries, opaque state, elicitation handling, and replay protection.
OAuth binds an MCP server connection to one product environment with no secret on disk. Header auth works headless and holds several environments at once.
Build an MCP scope challenge flow that starts with limited access, explains step-up consent, handles denial, and retries safely.
REST is deterministic code you can test and review; an MCP server lets a model decide per item. How cost, failure, auditability split and which to build first.