
# Logging

> `logging/setLevel`, legacy only.

> [!NOTE]
> `logging/setLevel` and the Logging feature as a whole were deprecated in `2026-07-28`. Modern clients set the level per request via `_meta["io.modelcontextprotocol/logLevel"]` — see [request-scoped streaming](/protocols/modern/streaming).

Legacy clients set a server-wide log level once, and the server pushes `notifications/message` at or above that level over the [SSE stream](/protocols/legacy/sse).

> [!IMPORTANT]
> Logging is **opt-in on the server**. `logging/setLevel` is only served by a server that declares the `logging` capability — set [`logging: true`](/api/options), or an `onLogLevelSet` callback, which implies it. Without either, the method answers `-32601 Method not found`, no level is ever in force, and `ctx.log()` emits nothing.

```ts
defineMcpHandler({
  name: "my-server",
  version: "1.0.0",
  onLogLevelSet(level, event) {
    console.log(`Client requested log level: ${level}`);
  },
  tools: [/* ... */],
});
```

The level is validated against the eight MCP levels before your callback runs:

`debug` · `info` · `notice` · `warning` · `error` · `critical` · `alert` · `emergency`

Declaring `onLogLevelSet` is one of the two things that advertise `logging: {}` in the `initialize` capabilities — and, therefore, one of the two things that make this method exist at all; see the note on `logging: true` below. The callback is a notification hook, not bookkeeping: `h3-mcp` already records the level per session, so nothing you write has to.

> [!IMPORTANT]
> Log messages are sent to the client. Treat them as output for a third party: no credentials, no tokens, no internal hostnames or stack traces. Everything that applies to [error messages](/guide/errors#messages-are-not-forwarded) applies here.

To actually send a log message, use `event.context.mcp.log()` — the same call on both eras. It gates on the level in force (here, the one this session set), which only a server that declared the capability can ever have, so the note below is a prerequisite rather than an aside:

```ts
event.context.mcp?.log?.("warning", { retries: 3 }, "importer");
```

On legacy the message travels on the request's own SSE response, so the client must send
`Accept: text/event-stream` with the call. Anything logged below the requested level, or by a
request whose client asked for no logs at all, is dropped.

:read-more{to="/protocols/modern/streaming" title="Request-scoped streaming"}

> [!IMPORTANT]
> A server that sends `notifications/message` MUST declare the `logging` capability, so `h3-mcp` declares it and gates emission on the same switch. If a handler calls `ctx.log()` on a server with no `onLogLevelSet`, set `logging: true` — it declares the capability on **both** eras (`initialize` on legacy, `server/discover` on modern) and is what allows a level to come into force. Nothing can detect statically that a handler logs, so this is the explicit switch, and **without it `ctx.log()` is a silent no-op**.
