# 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)**.
