---
title: "Capture MCP logs and wide events"
description: "Stream logs to MCP clients and capture structured wide events with useMcpLogger()."
canonical_url: "https://mcp-toolkit.nuxt.dev/advanced/logging"
---
# Capture MCP logs and wide events

> Stream logs to MCP clients and capture structured wide events with useMcpLogger().

`useMcpLogger()` exposes two channels with two audiences:

<table>
<thead>
  <tr>
    <th>
      Channel
    </th>
    
    <th>
      Goes to
    </th>
    
    <th>
      API
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <strong>
        Client
      </strong>
    </td>
    
    <td>
      The end user / agent UI (Cursor, Claude, Inspector)
    </td>
    
    <td>
      <code>
        log.notify(level, data)
      </code>
      
       + <code>
        .debug
      </code>
      
       / <code>
        .info
      </code>
      
       / <code>
        .warning
      </code>
      
       / <code>
        .error
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Server
      </strong>
    </td>
    
    <td>
      Your dev terminal and drains (Axiom, Sentry, OTLP, …)
    </td>
    
    <td>
      <code>
        log.set
      </code>
      
       / <code>
        log.event
      </code>
      
       / <code>
        log.setUser
      </code>
      
       / <code>
        log.setSession
      </code>
      
       / <code>
        log.evlog
      </code>
    </td>
  </tr>
</tbody>
</table>

The client channel always works. The server channel needs `evlog/nuxt`.

<prompt :actions="["copy","cursor","windsurf"]" description="Add useMcpLogger and evlog-style wide events" icon="i-lucide-scroll-text">

Add structured logging to my Nuxt MCP server (@nuxtjs/mcp-toolkit).

- Use useMcpLogger() inside a tool handler (auto-imported)
- log.notify.info({ … }) sends notifications/message to the connected MCP client (respects logging/setLevel)
- log.set({ … }) accumulates context onto the request's evlog wide event
- log.event(name, fields?) captures a discrete event in the same wide event
- log.setUser({ id, email, name }) / log.setSession({ id }) for canonical schema
- log.evlog gives you the underlying RequestLogger
- Install evlog and register evlog/nuxt for server-side wide events; configure from the top-level evlog: { … } key in nuxt.config

Docs: [https://mcp-toolkit.nuxt.dev/advanced/logging](https://mcp-toolkit.nuxt.dev/advanced/logging)

</prompt>

## Setup

Client notifications work out of the box. For server-side wide events, install [evlog](https://evlog.dev) and register `evlog/nuxt`:

<code-group>

```bash [pnpm]
pnpm add evlog
```

```bash [npm]
npm install evlog
```

```bash [yarn]
yarn add evlog
```

```bash [bun]
bun add evlog
```

</code-group>

```typescript [nuxt.config.ts]
export default defineNuxtConfig({
  modules: ['evlog/nuxt', '@nuxtjs/mcp-toolkit'],
  evlog: {
    env: { service: 'my-app' },
  },
})
```

That's it. Configure drains, sampling, redaction, and exclude patterns from the same `evlog: { … }` block — see the [evlog docs](https://evlog.dev) for the full schema.

### Auto-tagged fields

Every MCP wide event lands in your drain pre-tagged:

<table>
<thead>
  <tr>
    <th>
      Field
    </th>
    
    <th>
      Source
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        mcp.transport
      </code>
      
       / <code>
        mcp.route
      </code>
      
       / <code>
        mcp.session_id
      </code>
      
       / <code>
        mcp.method
      </code>
      
       / <code>
        mcp.request_id
      </code>
    </td>
    
    <td>
      Transport headers
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        mcp.tool
      </code>
      
       / <code>
        mcp.resource
      </code>
      
       / <code>
        mcp.prompt
      </code>
    </td>
    
    <td>
      JSON-RPC payload
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        user.id
      </code>
      
       / <code>
        user.email
      </code>
      
       / <code>
        user.name
      </code>
    </td>
    
    <td>
      <code>
        event.context.user
      </code>
      
       (from your auth middleware — better-auth, API key, …)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        session.id
      </code>
    </td>
    
    <td>
      <code>
        event.context.session.id
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        service
      </code>
    </td>
    
    <td>
      <code>
        <evlog.env.service>/mcp
      </code>
      
       (or slugified <code>
        mcp.name
      </code>
      
      ) — auto-injected on <code>
        /mcp
      </code>
      
       and <code>
        /mcp/**
      </code>
    </td>
  </tr>
</tbody>
</table>

Batched JSON-RPC calls flip the singular keys (`method`, `tool`, …) to plural arrays (`methods`, `tools`, …).

### Override the auto service

Pin your own value to opt out of the derived `<service>/mcp`:

```typescript [nuxt.config.ts]
evlog: {
  env: { service: 'my-app' },
  routes: {
    '/mcp': { service: 'my-app/agents' },
    '/mcp/**': { service: 'my-app/agents' },
  },
}
```

### Force on / opt out

<table>
<thead>
  <tr>
    <th>
      <code>
        mcp.logging
      </code>
      
       value
    </th>
    
    <th>
      Behavior
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        undefined
      </code>
      
       (default)
    </td>
    
    <td>
      On if <code>
        evlog/nuxt
      </code>
      
       is registered, off otherwise.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        true
      </code>
    </td>
    
    <td>
      Asserts <code>
        evlog/nuxt
      </code>
      
       is registered. Build throws if it isn't.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        false
      </code>
    </td>
    
    <td>
      Opt out. <code>
        log.notify(...)
      </code>
      
       keeps working.
    </td>
  </tr>
</tbody>
</table>

## Usage

`useMcpLogger()` is auto-imported. Call it inside a tool, resource, or prompt handler:

```typescript [server/mcp/tools/charge.ts]
import { z } from 'zod'
import { defineMcpTool } from '@nuxtjs/mcp-toolkit/server'

export default defineMcpTool({
  name: 'charge_card',
  inputSchema: { userId: z.string(), amount: z.number().int() },
  handler: async ({ userId, amount }) => {
    const log = useMcpLogger('billing')

    log.set({ billing: { amount } })
    await log.notify.info({ msg: 'starting charge', amount })

    try {
      const receipt = await charge(userId, amount)
      log.event('charge_completed', { receiptId: receipt.id })
      return `Charged ${amount}.`
    }
    catch (err) {
      log.evlog.error('charge failed', err)
      await log.notify.error({ msg: 'charge failed', error: String(err) })
      throw err
    }
  },
})
```

In your dev terminal:

```bash
INFO [my-app/mcp] POST /mcp 200 in 18ms
├─ mcp: transport=streamable-http route=/mcp method=tools/call tool=charge_card
├─ user: id=user-42 email=alice@example.com
├─ billing: amount=1000
└─ requestLogs: 0={"level":"info","message":"charge_completed",...}
```

### API

<table>
<thead>
  <tr>
    <th>
      Method
    </th>
    
    <th>
      Channel
    </th>
    
    <th>
      Description
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        notify(level, data, logger?)
      </code>
    </td>
    
    <td>
      client
    </td>
    
    <td>
      Send a <code>
        notifications/message
      </code>
      
      . Drops silently when filtered out by <code>
        logging/setLevel
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        notify.debug
      </code>
      
       / <code>
        info
      </code>
      
       / <code>
        warning
      </code>
      
       / <code>
        error
      </code>
    </td>
    
    <td>
      client
    </td>
    
    <td>
      Level shortcuts.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        set(fields)
      </code>
    </td>
    
    <td>
      server
    </td>
    
    <td>
      Merge fields into the wide event.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        event(name, fields?)
      </code>
    </td>
    
    <td>
      server
    </td>
    
    <td>
      Append a discrete event to the wide event's <code>
        requestLogs
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        setUser({ id, email, name })
      </code>
    </td>
    
    <td>
      server
    </td>
    
    <td>
      Tag the canonical <code>
        user
      </code>
      
       schema (auto-filled from <code>
        event.context.user
      </code>
      
      ).
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        setSession({ id })
      </code>
    </td>
    
    <td>
      server
    </td>
    
    <td>
      Tag <code>
        session.id
      </code>
      
       (auto-filled from <code>
        event.context.session
      </code>
      
      ).
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        evlog
      </code>
    </td>
    
    <td>
      server
    </td>
    
    <td>
      Underlying <a href="https://evlog.dev/docs/api/request-logger" rel="nofollow">
        <code>
          RequestLogger
        </code>
      </a>
      
       for <code>
        fork
      </code>
      
      , <code>
        error
      </code>
      
      , <code>
        getContext
      </code>
      
      , …
    </td>
  </tr>
</tbody>
</table>

`log.notify` always resolves and never throws — safe on hot paths. The server methods throw `McpObservabilityNotEnabledError` if `evlog/nuxt` isn't registered.

## Drains

Ship every MCP wide event to **Axiom, Sentry, OTLP, HyperDX, Datadog, Better Stack, or PostHog** with one Nitro plugin:

```typescript [server/plugins/evlog-axiom.ts]
import { createAxiomDrain } from 'evlog/adapters/axiom'

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', createAxiomDrain())
})
```

```bash [.env]
NUXT_AXIOM_TOKEN=xaat-...
NUXT_AXIOM_DATASET=mcp-server
```

The hook is additive — register multiple drains in parallel. Custom drains are just `(ctx) => Promise<void>` registered on the same hook.

<callout icon="i-lucide-arrow-up-right" target="_blank" to="https://evlog.dev">

See the [evlog docs](https://evlog.dev) for the full list of adapters, sampling, redaction, and how to build custom drains.

</callout>

<callout icon="i-lucide-triangle-alert" color="warning">

`useMcpLogger()` requires `nitro.experimental.asyncContext: true` (default since Nuxt 3.8+).

</callout>

## Next Steps

- [Middleware](https://mcp-toolkit.nuxt.dev/advanced/middleware) — capture `requestId`, timing, and tool names in your handler middleware.
- [Sessions](https://mcp-toolkit.nuxt.dev/advanced/sessions) — tag every wide event with `session.id` automatically.
- [Authentication](https://mcp-toolkit.nuxt.dev/examples/authentication) — soft auth populates `user.id` / `user.email` on every event.
- [evlog docs](https://evlog.dev) — drains, sampling, redaction, and custom adapters.


## Sitemap

See the full [sitemap](https://mcp-toolkit.nuxt.dev/sitemap.md) for all pages.
