49b9bb6724
Deploy Docs / deploy-docs (push) Failing after 1s
Conformance Tests / client-conformance (push) Failing after 3s
Conformance Tests / server-conformance (push) Failing after 1s
GitHub Actions Security Analysis / zizmor (push) Failing after 1s
CI / checks (push) Failing after 59m20s
CI / all-green (push) Waiting to run
79 lines
3.7 KiB
Markdown
79 lines
3.7 KiB
Markdown
# Logging
|
|
|
|
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 these docs don't teach it. The full list of what's deprecated and what to do instead is in **[Deprecated features](../deprecated.md)**.
|
|
|
|
What you do instead is what you do in every other Python program: the standard library.
|
|
|
|
## A tool that logs
|
|
|
|
```python title="server.py" hl_lines="1 5 13"
|
|
--8<-- "docs_src/logging/tutorial001.py"
|
|
```
|
|
|
|
* `logging.getLogger(__name__)` gives you a logger named after your module. Create it once, at the top.
|
|
* Inside the tool you call `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.
|
|
|
|
## Where it goes
|
|
|
|
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.
|
|
|
|
## The level
|
|
|
|
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.
|
|
|
|
## Try it
|
|
|
|
Run the server with the MCP Inspector:
|
|
|
|
```console
|
|
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
|
|
|
|
```text
|
|
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](../run/opentelemetry.md)**.
|
|
|
|
## Recap
|
|
|
|
* The MCP protocol's logging capability is deprecated by the 2026-07-28 spec and not replaced. Don't build on it.
|
|
* `logger = logging.getLogger(__name__)` at module level, `logger.info(...)` in the tool. That's the whole pattern.
|
|
* Log output never reaches the model. Only the value you `return` does.
|
|
* Standard error is yours; stdout belongs to the protocol. Never `print()` in a stdio server.
|
|
* `MCPServer(..., log_level="DEBUG")` sets the level, and a logging configuration you made first is left alone.
|
|
|
|
Telling connected clients that something on your server changed (the tool list, a resource) is **[Subscriptions](subscriptions.md)**.
|