docs

MCP Monitoring

See which tools your own MCP server handles, which clients and users call them, how fast they answer and how often they fail.

MCP Monitoring is for teams that ship a Model Context Protocol server inside their application. It answers which tools get called, by which clients and users, how long they take and how often they fail. It is about the server you run. To let an assistant read your Unhandled data, see unhandled's MCP server instead. Open it from MCP Monitoring in a project's sidebar (/o/:org/projects/:project/mcp).

Set up

Instrument the server with @unhandled/instrumentation-mcp-server, which works with @modelcontextprotocol/sdk 1.0.0 and later. Call instrumentMcpServer() before connect():

ts
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { instrumentMcpServer } from '@unhandled/instrumentation-mcp-server'

const server = new McpServer({ name: 'shop-assistant', version: '2.3.0' })
instrumentMcpServer(server, { recordInputs: true })

The instrumentation page covers the options, span names and recorded inputs and outputs. Spans from any other tracer show up here too, as long as they carry the attributes below.

What Unhandled reads

A span belongs to MCP as soon as it has mcp.method.name. The dashboard then uses:

AttributeUsed for
mcp.method.nameTool calls are spans with the value tools/call. The Methods table counts every value
mcp.tool.nameTools table and tool pages
mcp.client.name, mcp.client.versionClients table
user.idUsers table and unique users
mcp.tool.result.is_errorError counting
rpc.jsonrpc.error_code, rpc.jsonrpc.error_messageError counting and the error shown on a call

A tool call counts as failed when mcp.tool.result.is_error is true, when the span status is error, or when it has a JSON-RPC error code. That covers both a tool that returns isError: true and a request the server rejects.

The instrumentation also sets mcp.server.name and mcp.server.version on every span. The dashboard does not split by server, so give each server its own service if you run several.

Users

The instrumentation does not know who is calling. To fill the Users table, set user.id on the request span from your tool handler. With the AdonisJS agent, setUser() does it for the active span:

ts
import { z } from 'zod'
import { setUser } from '@unhandled/agent-adonisjs'
import Order from '#models/order'

server.registerTool(
  'find_order',
  { inputSchema: { orderId: z.string() } },
  async ({ orderId }, extra) => {
    if (extra.authInfo) setUser({ id: extra.authInfo.clientId })
    const order = await Order.findOrFail(orderId)
    return { content: [{ type: 'text', text: `Order ${order.id} is ${order.status}` }] }
  }
)

Calls without user.id still count everywhere except the Users table and unique users.

Dashboard

Six cards summarize tool calls in the selected range: Tool calls, Avg latency, P95 latency, Error rate, Unique users and Unique clients. Two charts follow: Tool calls, split into successful and failed calls, and Duration, with average and p95 latency over time.

TableRowsColumns
Toolsup to 100 tools, busiest firstcalls, p50, p95, p99, error rate, last seen
Clientsup to 100 clientsversions, calls, errors, average, p95, users, last seen
Usersup to 100 users with a user.idcalls, errors, clients, last seen
Methodsevery JSON-RPC methodcalls, errors

The cards, charts and the first three tables only count tools/call spans. Methods shows everything else the server handled too, such as initialize, tools/list and resources/read.

Tool pages

Clicking a tool opens its page with calls, average, p95 and p99 latency, error rate and unique users, the same two charts for that tool alone, and its 50 most recent calls. Each call shows its time, client, user, duration, status, error message and a shortened trace ID (hover it for the full ID). Open /o/:org/projects/:project/traces/:traceId with that ID to see the full trace behind a slow or failed call.

Not recorded

The instrumentation does not emit session IDs, request IDs, the transport name or the negotiated protocol version. Prompt and resource requests appear in the Methods table and in traces, but the dashboard has no per-prompt or per-resource view.