MCP Server

Trace your own MCP server. One runless span per tools/call, with no parent run or session lifecycle to manage.

trackMcp / track_mcp traces an MCP server you operate. It writes one runless span per tools/call, with no parent run and no session bookkeeping. Use it instead of wrapAgent whenever you're the MCP server.

Why MCP is different

An MCP server proxies tool calls but never sees the user's prompt or the model's final answer. Those live in the client (Claude, Cursor, ChatGPT). A normal run wrapping an MCP session would have empty input and output, and MCP has no clean session-end signal, so such runs get stuck in running. trackMcp sidesteps both: each tool call is a self-contained span tagged agent_name='MCP', queryable by distinct_id and conversation_id (the Mcp-Session-Id).

Quick start

Call it from inside your tools/call handler, once per tool call.

async function handleToolCall(req, toolName, args) {
  const t0 = Date.now();
  try {
    const result = await TOOLS[toolName](args);
    await trodo.trackMcp({
      tool: toolName,
      distinctId: req.userEmail,                // required
      sessionId: req.headers['mcp-session-id'], // auto-uuid'd if omitted
      input: args,
      output: result,
      durationMs: Date.now() - t0,
      clientLabel: req.clientLabel,             // 'anthropic' / 'cursor' / etc.
    });
    return result;
  } catch (e) {
    await trodo.trackMcp({
      tool: toolName, distinctId: req.userEmail,
      sessionId: req.headers['mcp-session-id'],
      input: args, error: String(e), durationMs: Date.now() - t0,
    });
    throw e;
  }
}
async def handle_tool_call(req, tool_name, arguments):
    t0 = time.perf_counter()
    try:
        result = await TOOLS[tool_name](arguments)
        trodo.track_mcp(
            tool=tool_name,
            distinct_id=req.user_email,                   # required
            session_id=req.headers.get('mcp-session-id'), # auto-uuid'd if omitted
            input=arguments,
            output=result,
            duration_ms=int((time.perf_counter() - t0) * 1000),
            client_label=req.client_label,                # 'anthropic' / 'cursor' / etc.
        )
        return result
    except Exception as e:
        trodo.track_mcp(
            tool=tool_name, distinct_id=req.user_email,
            session_id=req.headers.get('mcp-session-id'),
            input=arguments, error=str(e),
            duration_ms=int((time.perf_counter() - t0) * 1000),
        )
        raise

The call returns the span_id if you want it for correlation; otherwise ignore it.

Parameters

Parameter (Node / Python)Notes
toolRequired. The tool name; becomes name = "tool.<tool>".
distinctId / distinct_idRequired. End-user attribution (email or stable user id). Runless spans without it are rejected.
inputThe tool's arguments. Truncated at 64 KB.
outputThe full tool result. Pass the whole payload; don't pre-summarise.
errorIf set, marks the span status='error' and stores the message.
durationMs / duration_msTool wall-clock time. Defaults to 0.
sessionId / session_idThe Mcp-Session-Id; stored as conversation_id. A fresh UUID per call if omitted.
clientLabel / client_labelWhich MCP client called (anthropic, cursor, chatgpt).
attributesFree-form extra attributes for filterable scalars.
agentName / agent_nameDefaults to "MCP"; override only for a custom tag.

Latency

By default the call waits for the span POST (~50 to 200 ms). If MCP latency matters more than guaranteed delivery, fire-and-forget it: don't await in Node, or run it in a background thread/task in Python. The SDK swallows network errors internally, so a dropped span never breaks your handler.

Next

On this page