Advanced Topics

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:

ChannelGoes toAPI
ClientThe end user / agent UI (Cursor, Claude, Inspector)log.notify(level, data) + .debug / .info / .warning / .error
ServerYour dev terminal and drains (Axiom, Sentry, OTLP, …)log.set / log.event / log.setUser / log.setSession / log.evlog

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

Add useMcpLogger and evlog-style wide events

Setup

Client notifications work out of the box. For server-side wide events, install evlog and register evlog/nuxt:

pnpm add evlog
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 for the full schema.

Auto-tagged fields

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

FieldSource
mcp.transport / mcp.route / mcp.session_id / mcp.method / mcp.request_idTransport headers
mcp.tool / mcp.resource / mcp.promptJSON-RPC payload
user.id / user.email / user.nameevent.context.user (from your auth middleware — better-auth, API key, …)
session.idevent.context.session.id
service<evlog.env.service>/mcp (or slugified mcp.name) — auto-injected on /mcp and /mcp/**

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:

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

Force on / opt out

mcp.logging valueBehavior
undefined (default)On if evlog/nuxt is registered, off otherwise.
trueAsserts evlog/nuxt is registered. Build throws if it isn't.
falseOpt out. log.notify(...) keeps working.

Usage

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

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:

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

MethodChannelDescription
notify(level, data, logger?)clientSend a notifications/message. Drops silently when filtered out by logging/setLevel.
notify.debug / info / warning / errorclientLevel shortcuts.
set(fields)serverMerge fields into the wide event.
event(name, fields?)serverAppend a discrete event to the wide event's requestLogs.
setUser({ id, email, name })serverTag the canonical user schema (auto-filled from event.context.user).
setSession({ id })serverTag session.id (auto-filled from event.context.session).
evlogserverUnderlying RequestLogger for fork, error, getContext, …

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:

server/plugins/evlog-axiom.ts
import { createAxiomDrain } from 'evlog/adapters/axiom'

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', createAxiomDrain())
})
.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.

See the evlog docs for the full list of adapters, sampling, redaction, and how to build custom drains.
useMcpLogger() requires nitro.experimental.asyncContext: true (default since Nuxt 3.8+).

Next Steps

  • Middleware — capture requestId, timing, and tool names in your handler middleware.
  • Sessions — tag every wide event with session.id automatically.
  • Authentication — soft auth populates user.id / user.email on every event.
  • evlog docs — drains, sampling, redaction, and custom adapters.
Copyright © 2026