Capture MCP logs and wide events
useMcpLogger() exposes two channels with two audiences:
| Channel | Goes to | API |
|---|---|---|
| Client | The end user / agent UI (Cursor, Claude, Inspector) | log.notify(level, data) + .debug / .info / .warning / .error |
| Server | Your 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
npm install evlog
yarn add evlog
bun add evlog
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:
| Field | Source |
|---|---|
mcp.transport / mcp.route / mcp.session_id / mcp.method / mcp.request_id | Transport headers |
mcp.tool / mcp.resource / mcp.prompt | JSON-RPC payload |
user.id / user.email / user.name | event.context.user (from your auth middleware — better-auth, API key, …) |
session.id | event.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:
evlog: {
env: { service: 'my-app' },
routes: {
'/mcp': { service: 'my-app/agents' },
'/mcp/**': { service: 'my-app/agents' },
},
}
Force on / opt out
mcp.logging value | Behavior |
|---|---|
undefined (default) | On if evlog/nuxt is registered, off otherwise. |
true | Asserts evlog/nuxt is registered. Build throws if it isn't. |
false | Opt out. log.notify(...) keeps working. |
Usage
useMcpLogger() is auto-imported. Call it inside a tool, resource, or prompt handler:
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
| Method | Channel | Description |
|---|---|---|
notify(level, data, logger?) | client | Send a notifications/message. Drops silently when filtered out by logging/setLevel. |
notify.debug / info / warning / error | client | Level shortcuts. |
set(fields) | server | Merge fields into the wide event. |
event(name, fields?) | server | Append a discrete event to the wide event's requestLogs. |
setUser({ id, email, name }) | server | Tag the canonical user schema (auto-filled from event.context.user). |
setSession({ id }) | server | Tag session.id (auto-filled from event.context.session). |
evlog | server | Underlying 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:
import { createAxiomDrain } from 'evlog/adapters/axiom'
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('evlog:drain', createAxiomDrain())
})
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.
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.idautomatically. - Authentication — soft auth populates
user.id/user.emailon every event. - evlog docs — drains, sampling, redaction, and custom adapters.