MCP Tracing & Telemetry

Tracing an MCP server means seeing which tool the agent called, how long it took and what it threw. trace-mcp ships a pluggable observability bridge (P13) that emits OpenTelemetry-compatible spans for every AI provider call and every MCP tool invocation, so MCP tool traffic shows up in the same tracing backend as the rest of your stack. The default sink is noop — opt-in only, and switched on with the telemetry.* keys described in configuration. Turning it on also unlocks the persistent window values of analyze_perf, which is otherwise limited to the current session (analytics).

This page is about spans you export to your own collector. It is not the anonymous daily install ping, which is a separate subsystem reporting to the maintainer. That one’s published field list — everything it sends, both ways to turn it off, and how to delete the state it keeps — is on the privacy page, with the source in src/telemetry/usage-ping.ts. Nothing on this page phones home.

Quickstart (3 commands)

# 1. Stand up a local OTLP collector (Jaeger all-in-one).
cd ops/telemetry && docker compose up -d && cd -

# 2. Enable the bridge for this project (.trace.json is gitignored).
cat > .trace.json <<'JSON'
{
  "telemetry": {
    "observability": {
      "enabled": true,
      "sink": "otlp",
      "otlp": { "endpoint": "http://localhost:4318/v1/traces" }
    }
  }
}
JSON

# 3. Run anything — spans land in Jaeger UI at http://localhost:16686.
pnpm run build && node dist/cli.js eval run --dataset default

Where the traces appear

Open http://localhost:16686, pick trace-mcp from the Service dropdown, click Find Traces.

Span topology

Span name Origin Key attributes
tool.<name> src/server/tool-gate.ts tool.name, duration_ms, tool.is_error
ai.embed TrackedEmbeddingService.embed ai.provider, ai.model, ai.url, ai.input_size, ai.output_size, duration_ms
ai.embed_batch TrackedEmbeddingService.embedBatch as above + batch counts
ai.generate TrackedInferenceService.generate ai.provider, ai.model, ai.max_tokens, ai.temperature, ai.input_size, duration_ms
ai.generate_stream TrackedInferenceService.generateStream as above

Errors thrown by the wrapped call surface as a span exception event (exception.type, exception.message, exception.stacktrace) with span status ERROR. The error is rethrown so caller control flow is unchanged.

Switching sinks

In .trace.json (or ~/.trace/.config.json for a global default):

Goal Config
Disabled (default) telemetry.observability.enabled: false or omit the block
Local Jaeger / collector sink: "otlp", otlp.endpoint: "http://localhost:4318/v1/traces"
Hosted OTLP (e.g. Honeycomb) sink: "otlp", otlp.endpoint: "https://api.honeycomb.io/v1/traces", otlp.headers: { "x-honeycomb-team": "…" }
Langfuse cloud sink: "langfuse", langfuse.publicKey: "pk-…", langfuse.secretKey: "sk-…"
Fan-out to both sink: "multi" + both otlp and langfuse blocks
Probabilistic sampling sampleRate: 0.1 (10% of spans kept)

All sinks lazy-load — paying for noop is free, paying for otlp only loads when sink === "otlp".

Adding a custom span from your own code

The public API lives in src/telemetry/index.ts:

import {
  getGlobalTelemetrySink,
  instrumentAsync,
} from './telemetry/index.js';

// Time an async function and auto-record exceptions.
await instrumentAsync(
  getGlobalTelemetrySink(),
  'my.operation',
  { 'my.attr': 42 },
  async (span) => {
    // ... your work ...
    span.setAttribute('result.count', items.length);
    return items;
  },
);

// Manual span lifecycle.
const span = getGlobalTelemetrySink().startSpan('my.op', { foo: 'bar' });
try {
  // ...
  span.setStatus('ok');
} catch (err) {
  span.recordError(err);
  throw err;
} finally {
  span.end();
}

instrumentAiCall and instrumentToolCall convenience wrappers exist for the two standardised attribute schemas — prefer them when emitting AI or tool-flavoured spans so dashboards stay consistent.

Cleanup

cd ops/telemetry && docker compose down -v
rm .trace.json   # if you don't want telemetry enabled going forward

Performance

Frequently asked questions

What is MCP tracing?

MCP tracing instruments Model Context Protocol interactions between AI coding agents (such as Claude Code, Cursor, or Windsurf) and MCP servers. Instead of debugging tool timeouts or failed tool invocations through raw log tails, MCP tracing emits structured OpenTelemetry spans for every tool call (tool.<name>) and provider inference (ai.embed, ai.generate). This surfaces tool execution duration, error stack traces, and token costs in standard APM dashboards like Jaeger, Honeycomb, or Langfuse alongside your backend application traces.

How do I trace MCP tool calls with trace-mcp?

In your project repository, configure .trace.json to enable the observability bridge:

{
  "telemetry": {
    "observability": {
      "enabled": true,
      "sink": "otlp",
      "otlp": { "endpoint": "http://localhost:4318/v1/traces" }
    }
  }
}

Once configured, any command or client session running through trace-mcp flushes traces asynchronously to your configured OTLP endpoint without adding blocking network overhead to tool turns. For detailed configuration parameters, see the configuration guide.

Does MCP tracing export private repository code or credentials?

No. trace-mcp telemetry spans export operational metadata only: span names, execution durations (duration_ms), error exception types, input/output payload sizes in bytes, and model token counts. Raw source code files, git diffs, and API credentials are never included in OpenTelemetry attributes or events. Furthermore, the sink defaults to noop until you explicitly opt in, and all usage ping policies are documented on the privacy page.

Troubleshooting

Last updated: September 11, 2026