# 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
    Don't `print()` in a stdio server. `print` writes to **stdout**, and stdout belongs to the protocol.
    While serving, the SDK diverts stdout that is actually *flushed* to stderr, so it can't corrupt the
    wire, but a `print()` in a block-buffered process usually sits unflushed in `sys.stdout`'s buffer
    until the interpreter drains it at exit, straight onto the protocol stream. Even when it is diverted,
    the line lands raw among the log output, with no level, no logger name, and no way to filter it.

    `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. The SDK diverts flushed stray stdout to stderr while serving, but an unflushed `print()` can still drain onto the wire at exit, and diverted lines arrive unlabeled; use `logging`, whose handler flushes every record.
* `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)**.
