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),
)
raiseThe call returns the span_id if you want it for correlation; otherwise ignore it.
Parameters
| Parameter (Node / Python) | Notes |
|---|---|
tool | Required. The tool name; becomes name = "tool.<tool>". |
distinctId / distinct_id | Required. End-user attribution (email or stable user id). Runless spans without it are rejected. |
input | The tool's arguments. Truncated at 64 KB. |
output | The full tool result. Pass the whole payload; don't pre-summarise. |
error | If set, marks the span status='error' and stores the message. |
durationMs / duration_ms | Tool wall-clock time. Defaults to 0. |
sessionId / session_id | The Mcp-Session-Id; stored as conversation_id. A fresh UUID per call if omitted. |
clientLabel / client_label | Which MCP client called (anthropic, cursor, chatgpt). |
attributes | Free-form extra attributes for filterable scalars. |
agentName / agent_name | Defaults 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
- Wrap your agent for agents that own the conversation
- Long-running & background runs for websocket chats and resumed jobs