MCPDBWizard

Writing  · 

MCP logging: what the protocol deprecated, and where your server's logs should go

“MCP logging” means at least three different things, and most of the confusion about it comes from treating them as one. Which one you want depends on who is going to read the log:

Each has its own answer, and only one of them is part of the MCP specification — the one that has just been deprecated.

1. Logging over the protocol: deprecated

MCP defines a logging utility: a server sends notifications/message frames to the client, at one of eight syslog-style levels from debug to emergency. As of protocol revision 2026-07-28 it is deprecated, together with Roots and Sampling, under SEP-2577. The spec’s deprecation registry says new implementations should not adopt it, and it becomes eligible for removal in the first revision released on or after 28 July 2027.

Two things are worth knowing if you already use it.

The opt-in mechanism has changed. Older revisions had a logging/setLevel request that set a threshold for the whole session. In 2026-07-28 a client opts in per request, with an io.modelcontextprotocol/logLevel key in that request’s _meta — and a server must not send log notifications for a request that omits it. Code written against the old model is two steps behind: first the mechanism moved, then the feature was deprecated.

Nothing was wrong with sending logs to the client; it was just the wrong place for most logs. A log frame goes to whoever called the tool, which is rarely the person who needs to read it. The spec’s own migration path says as much: stderr for local servers, OpenTelemetry for observability.

2. Logs for whoever runs the server

Over stdio, stdout is the protocol. A local server talks to its client by writing JSON-RPC frames to stdout, so anything else written there — a startup banner, a stray println, a logging framework with a console appender — lands in the middle of the protocol stream. The client then reports a parse error or a dropped connection, and the error message names nothing to do with logging. It is the most common way to break a stdio server, and it is easy to misdiagnose because the server looks perfectly healthy from its own side.

Write to stderr instead. The client captures it. Claude Desktop, for example, keeps it in ~/Library/Logs/Claude/mcp*.log on macOS and %APPDATA%\Claude\logs on Windows, which is the first place to look when a local server fails to start.

Over HTTP, nobody captures stderr for you. The client is on the other end of a network connection and never sees your process’s output, so logs go wherever the rest of your services’ logs go — container stdout picked up by a log driver, a file with rotation, a collector. This is ordinary server logging, and the stdout rule no longer applies: over HTTP, stdout carries nothing but your logs.

What to log for each tool call

One line per call, carrying: the tool name, how it ended, how long it took — and the names of the arguments, not their values.

The reason for leaving values out is specific to MCP. A model chose them, and a model’s input can include whatever was put in front of it: a customer’s email address pasted into a chat, a document it was asked to summarise, text written by someone trying to steer it. Log those values and your log becomes a store of data you never decided to keep, with none of the controls you would put on a store you had decided to keep. Names are enough to reconstruct what happened; values are what you record deliberately, somewhere built for it.

Record the outcome as a category, not just success or failure. “Not found”, “database error” and “the connection pool was exhausted” call for different responses, and the last one is a capacity problem that looks like a flood of unrelated failures if all you logged was “error”.

3. Observability: OpenTelemetry, not quite yet

For traces and metrics, the spec points at OpenTelemetry. That is the right destination, and it is worth designing towards — but OpenTelemetry’s MCP conventions are not stable yet. They have moved into its generative-AI semantic conventions, and the MCP method names in its attribute registry are still marked Development, which means attribute names can still change. Emitting against them today is fine as long as you expect to revisit it.

Metrics are the easier half and do not need to wait. Per-tool call counts, error counts and latency answer most operational questions — which tools are used, which are slow, which fail — and a Prometheus endpoint works with whatever is already scraping your other services.

4. Audit: there is no standard at all

A log and an audit trail are different things, and MCP has no convention for the second. A log is written by the process it describes, for the people debugging it; it can be rotated away, and nothing guarantees it is complete. An audit record has to answer who called what, with what, and was it allowed after the fact — which means it has to be written somewhere the audited process cannot quietly lose or rewrite, and kept for a period you chose on purpose.

That is also where argument values belong, if you need them at all: in a store with retention and access controls, not in an application log.

How does MCP DB Wizard fit into all this?

MCP DB Wizard is a generator: you point it at an Oracle schema, tick the tables, PL/SQL routines, sequences and tested SQL statements of your own that one job needs, and it builds an MCP server exposing those and nothing else. Every generated server takes the positions above, and the logging page has the detail.

One line per tool call, names only:

MCP-CALL {"tool":"get_customer","outcome":"ok","ms":12,"args":["p_id"]}

Outcomes are ok, not-found, document-changed, pool-exhausted, database-error or error. The backend is chosen when the code is generated but can be overridden at run time with MCPDBWIZARD_LOG_BACKEND, and a backend that cannot be loaded stops the server rather than silently falling back — otherwise you would believe the logs had moved when they had not. HTTP is the deployed shape, so the stdout footgun does not arise; if you run a server over stdio, point its logging at stderr.

Protocol logging: implemented, reluctantly, and silent unless asked for. We would not have adopted a deprecated feature by choice. But the MCP Java SDK we build on (2.0.0) adds the logging capability to every server unconditionally, overriding whatever capabilities the server declares — we have reported it upstream. That left two options: advertise logging and implement it, or advertise it and send nothing. The second is a server that makes a claim it does not honour, so generated servers send one notifications/message per tool call at debug, with the same names-only content. A client that never asks for debug receives nothing, so it costs nothing. The SDK supports the older session-wide logging/setLevel request, not the per-request _meta form, and that is the level control it honours.

Metrics, not yet OpenTelemetry. Set PROMETHEUS_SERVER=YES and each server exposes per-tool counts, latencies and outcomes — see metrics and Grafana. There is no OpenTelemetry output yet, for the reason given above.

A separate audit trail. Every install keeps a local trail of tool calls, names only by default, kept for a window you set, and it can be streamed to Kafka, Splunk, S3 or syslog through a spool that survives an outage — see auditing. The log tells you what the server did; the audit trail is the record you can hand to someone else.

How do I get MCP DB Wizard?

It’s available from our GitHub repo as a Docker image, and the quickstart will get you a running server against your own schema.