docs/tutorial/logging.md
Log from a tool the way you log from any other Python function: with the standard library.
MCP has a protocol-level logging capability: a server could push its log messages to the client as notifications, through methods on the Context object. The 2026-07-28 revision of the spec deprecates that capability and does not replace it, so this tutorial doesn't teach it. The full list of what's deprecated and what to do instead is in Deprecated features.
What you do instead is what you do in every other Python program: the standard library.
--8<-- "docs_src/logging/tutorial001.py"
logging.getLogger(__name__) gives you a logger named after your module. Create it once, at the top.logger.info(...) like in any other function. Nothing to inject, nothing to await, nothing MCP-specific.!!! check Call the tool and look at the whole result:
```python
result.content # [TextContent(text="Found 3 books matching 'dune'.")]
result.structured_content # {'result': "Found 3 books matching 'dune'."}
```
The log line is nowhere in it. Logging is for **you**, the person operating the server. The model
never sees it. If the model should read something, `return` it.
For a stdio server this question matters more than usual. The host launched your server as a subprocess and is reading MCP messages from its stdout. Standard error is yours.
The standard library already does the right thing: log output goes to sys.stderr by default. Your logger.info(...) lines land in the terminal (or wherever the host collects the subprocess's stderr), and the protocol stream stays clean.
!!! tip
Never print() in a stdio server. print writes to stdout, and stdout is the wire: one stray
line and the client is trying to parse it as JSON-RPC.
`logger.debug("got here")` is the same one line of effort and goes to the right place.
You don't have to call logging.basicConfig() yourself. Constructing an MCPServer already did, with a handler pointed at standard error, at the level you pass as log_level=, so MCPServer("Bookshop", log_level="DEBUG") is all it takes to see your logger.debug(...) lines.
The default is "INFO".
logging.basicConfig() never replaces handlers that already exist. If you configure logging yourself before creating the server, your configuration wins.
Run the server with the MCP Inspector:
uv run mcp dev server.py
Call search_books from the Tools tab. The Inspector shows you the result: only the return value. The line
Searching for 'dune'
went to standard error: the terminal, not the wire.
!!! info If what you actually want is tracing (every request, how long it took, whether it failed), you don't want log lines, you want spans. Your server already emits them: the SDK traces every message with OpenTelemetry out of the box. See OpenTelemetry.
logger = logging.getLogger(__name__) at module level, logger.info(...) in the tool. That's the whole pattern.return does.print() in a stdio server.MCPServer(..., log_level="DEBUG") sets the level, and a logging configuration you made first is left alone.Next: the in-memory client that has been running every example on these pages, and how to point it at your own server, in Testing.