Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.trodo.ai/docs/llms.txt

Use this file to discover all available pages before exploring further.

track_mcp (Python) / trackMcp (Node) is the dedicated entry point for tracing an MCP server. It writes one runless span per tools/call — no parent agent run, no session/sweeper bookkeeping. Requires SDK version >= 2.3.0.
Use this for MCP servers only. If you’re tracing an agent that owns the conversation (chat, planner, RAG pipeline), use wrapAgent. For websocket-pinned chats and scheduled jobs that bridge multiple HTTP requests, use startRun / endRun.

Why MCP is different

The MCP server proxies tool calls but never sees the user’s prompt or the LLM’s final answer — those live inside the client (Claude.ai, Cursor, ChatGPT, Claude Desktop). A traditional Trodo Run wrapping an MCP session would have empty input / output and no signal for clustering. And MCP has no clean session-end signal in either transport (HTTP or stdio), so Runs end up stuck in running forever unless you bolt on a sweeper. track_mcp bypasses all of that: each tool call is a self-contained span row, queryable by agent_name='MCP', distinct_id, and conversation_id (the Mcp-Session-Id).

Quick start

import time, trodo

trodo.init(site_id=os.environ["TRODO_SITE_ID"])

# Inside your MCP server's tools/call handler:
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
That’s the complete integration. The function returns the span_id if you want it for cross-system correlation; otherwise ignore the return value.

What auto-fills

FieldDefault behaviour
span_idUUID v4, returned by the function
session_id / conversation_idWhat you pass; if omitted, a fresh UUID per call
agent_name"MCP" (override via agent_name= / agentName: for custom tags)
kind"tool"
name"tool.<tool>"
started_atnow() - duration_ms
ended_atnow()
status"error" if error is set, else "ok"
You’re responsible for: tool name, distinct_id (the user), input, output, and duration_ms. Everything else has a sane default.

Parameters

tool
string
required
The tool name. Becomes name = "tool.<tool>".
distinct_id / distinctId
string
required
End-user attribution. Use the user’s email if your MCP auth flow gives you one (OAuth introspection typically does), else a stable user id. Runless spans without this are rejected by the backend with 400.
input
any
The tool’s arguments. Truncated at 64 KB on the server side.
output
any
The tool’s full result payload. Pass the whole structured result — don’t pre-summarise.
error
string | null
If set, the span is marked status="error" and this string is stored as error_message.
duration_ms / durationMs
number
Tool wall-clock duration. Defaults to 0 if omitted.
session_id / sessionId
string
The Mcp-Session-Id from the MCP client. Stored as conversation_id for grouping. If omitted, the SDK mints a fresh UUID per call.
client_label / clientLabel
string
Which MCP client called you — "anthropic", "cursor", "chatgpt", etc. Merged into attributes as mcp_client_label.
attributes
object
Free-form extra attributes. Use this for filterable scalars like counts, status flags, or summary strings.
agent_name / agentName
string
default:"\"MCP\""
Override only if you need a custom tag (e.g. "MCP_internal" for a separate internal MCP). Defaults to "MCP".

Querying the data

In the dashboard, MCP traffic is grouped by agent_name='MCP' with run_id IS NULL. The hosted endpoint:
GET /api/agentic/mcp/activity?team_id=X&window_days=7
Returns per-tool aggregates (calls, error rate, p95 latency, unique users, unique sessions). Or query directly:
SELECT
  name AS tool,
  COUNT(*)                            AS calls,
  COUNT(*) FILTER (WHERE status='error') AS errors,
  AVG(duration_ms)                    AS avg_ms,
  COUNT(DISTINCT distinct_id)         AS users,
  COUNT(DISTINCT conversation_id)     AS sessions
FROM agent_spans
WHERE team_id = $1
  AND agent_name = 'MCP'
  AND run_id IS NULL
  AND started_at > NOW() - INTERVAL '7 days'
GROUP BY name
ORDER BY calls DESC;

Latency considerations

By default track_mcp waits for the span POST to complete before returning. That’s ~50–200 ms of HTTP RTT added to your MCP response. If MCP latency matters more than guaranteed delivery, fire-and-forget:
import asyncio
asyncio.create_task(asyncio.to_thread(
    trodo.track_mcp,
    tool=tool_name, distinct_id=user_email, ...,
))
The SDK already swallows network errors internally so a fire-and-forget call won’t crash your handler.

Output capture rules

The same three rules from wrapAgent apply:
  1. Await the full result before serialising. Never write a streaming handle into output.
  2. Output is the FULL payload, not a metadata summary. Pass the entire ToolResult; the SDK truncates at 64 KB.
  3. Use attributes for filterable scalars. Counts, status flags, the human summary string.

Raw-HTTP fallback

If you can’t take an SDK dependency:
POST https://sdkapi.trodo.ai/api/sdk/spans/append
Content-Type: application/json
X-Trodo-Site-Id: <site_id>

{
  "spans": [{
    "span_id": "<uuid>",
    "kind": "tool",
    "name": "tool.<tool_name>",
    "status": "ok",
    "input":  "<json string>",
    "output": "<json string>",
    "tool_name": "<tool_name>",
    "started_at": "<ISO 8601>",
    "ended_at":   "<ISO 8601>",
    "duration_ms": 120,
    "agent_name":      "MCP",
    "distinct_id":     "[email protected]",
    "conversation_id": "<Mcp-Session-Id>",
    "attributes": { "mcp_client_label": "anthropic" }
  }]
}
Use the SDK if you can — same wire format, less boilerplate, automatic 64 KB truncation, and version-pinned schema.

Common pitfalls

PitfallSymptomFix
Forgot distinct_id / distinctIdSDK throws before postingAlways populate it. Email > stable id > opaque token hash.
Used wrapAgent or startRun for the MCP pathRun rows stuck in running forever; empty input/outputSwitch to track_mcp / trackMcp.
Pre-summarised output to save spaceDashboard shows {"count": 5} instead of the actual dataPass the full payload. The SDK truncates at 64 KB on its own.
Awaited span POST in a high-throughput MCP server and felt latencyEach tool call adds ~50–200 ms RTTFire-and-forget (see above).