Files
tracy/extra/mcp/eval_guide.md
Alan Tse 9a7233ced5 Add MCP server for AI-assisted trace analysis (#1347)
* Add MCP server for AI-assisted trace analysis.

Introduce an optional Model Context Protocol (MCP) server that lets AI
assistants analyze Tracy captures and live sessions through Tracy's own
server engine. The server runs as a Python sidecar and talks to the
existing C++ analysis code through new pybind11 bindings.

- python/bindings/ServerModule.cpp: TracyServerBindings module exposing
  Worker, file I/O, zones, GPU zones, frame data, plots, messages, locks,
  source locations, and summary statistics (zone/GPU child stats, frame
  timing, etc.).
- python/CMakeLists.txt: builds and installs TracyServerBindings alongside
  TracyClientBindings.
- extra/mcp/tracy_mcp.py: FastMCP SSE singleton with dynamic port
  discovery, PID-file based singleton detection, session-isolated worker
  instances, synchronous and background eval, task polling, and a
  shutdown tool to release the .pyd lock during development.
- extra/mcp/start_mcp.sh, .gitignore: launcher with local override hook;
  ignores generated port/pid files.
- manual/tracy.md: documents building, running, and integrating the
  server with an AI assistant.

* Improve Tracy MCP cold-start guidance.

Cold-start usability testing showed an LLM agent burned ~7 exploratory
calls discovering the ctx object model, time-unit conventions, and join
keys before producing useful analysis. Surface that information up front
through MCP resources and entry-point tool guidance.

- extra/mcp/eval_guide.md: new bindings-layer reference covering the
  Worker object graph (zone / GPU zone / frame / thread / message /
  plot / lock / memory entry points), nanosecond time units, ZoneStats
  field semantics including self-time via get_child_zone_stats, the
  opaque 'name (addr)[arch] <srcloc_id>' key format, and worked
  examples translating common queries into ctx Python.
- extra/mcp/tracy_mcp.py: expose system.prompt.md and eval_guide.md as
  MCP resources (tracy://prompt and tracy://eval-guide) so external
  agents and Tracy Assist share the same guidance source. Resource
  content is re-read per request — edits propagate without a server
  restart.
- Point load_capture and live_connect return values plus the eval tool
  description at the resources, so the agent reads them before its
  first eval rather than introspecting blind.
- Expand load_capture docstring: name the path parameter explicitly,
  show Windows path syntax, and direct agents to list_captures plus
  TRACY_CAPTURES_DIR for capture discovery.
- Probe is_connected() briefly after Worker construction in
  live_connect and surface an actionable error on silent handshake
  failures (typically a Tracy client/server version mismatch or
  TRACY_ON_DEMAND) instead of returning misleading success.

Reduces a fresh agent's cold-start overhead from 7 exploratory calls
to 4, where the remaining 4 are unavoidable harness/schema-fetch
overhead, not API-design friction.

* Detect Tracy protocol mismatches via UDP broadcast pre-flight.

Tracy clients announce themselves on UDP port 8086 every ~3 seconds with
a BroadcastMessage carrying the protocol version, listen port, and
program name (public/common/TracyProtocol.hpp). The Tracy GUI reads this
and refuses to attempt a TCP connection on protocol mismatch, surfacing
a precise error. live_connect previously had no equivalent check, so a
mismatch produced an opaque 2-second handshake timeout with no
diagnostic about what was wrong.

- Add a broadcast parser handling versions 0-3, with variable-length
  programName (Tracy sends only the actual name + null terminator on
  the wire, not the full 64-byte buffer).
- Add a non-blocking UDP listener that binds 8086 with SO_REUSEADDR
  and waits up to 3.5s — enough to guarantee catching at least one
  beat at the 3s broadcast cadence.
- Read our bindings' ProtocolVersion at startup by parsing
  TracyProtocol.hpp, so the comparison stays in sync with the build
  without new C++ wiring.
- live_connect runs the broadcast pre-flight before constructing
  Worker. On a matched listen_port with a differing protocol_version,
  it returns a single-line error naming the program, both versions,
  and the remediation, without ever opening a TCP connection. If no
  matching broadcast arrives, it falls through to the existing
  handshake probe, which now reports any other broadcasts seen as a
  hint (helpful when the target uses a non-default port).

* Add MCP Server section to LaTeX manual.

The markdown manual is auto-generated from the LaTeX source; add the
corresponding \subsection{MCP Server} so the two stay in sync.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Remove hand-written MCP section from tracy.md.

tracy.md is generated from tracy.tex via latex2md.sh. The MCP section
was previously written by hand directly in the markdown; now that the
LaTeX source has been updated, the markdown section should be
regenerated by running latex2md.sh rather than maintained manually.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 16:17:55 +02:00

3.2 KiB

Tracy MCP eval guide

This document covers the bindings-layer detail that the curated catalog (tracy://catalog) and analysis guidance (tracy://prompt) do not.

ctx

ctx is a TracyServerBindings.Worker — the same object Tracy Assist's C++ tools query through Worker::Get*. The pybind methods are the canonical data surface. Common entry points:

  • Zones: get_all_zone_stats() (every callsite, large), get_root_zone_stats() (top-level zones only, useful for "where is the program spending time"), get_zone_stats(srcloc_id), get_child_zone_stats(srcloc_id) (subtract for self-time), get_zone_durations(name), get_zone_count(), get_all_zone_source_locations()
  • GPU zones: get_all_gpu_zone_stats(), get_gpu_zone_durations(...), get_gpu_contexts()
  • Frames: get_frame_count(), get_frame_times(), get_frame_times_named(name), get_frame_boundaries(), get_zones_in_frame(...)
  • Threads: get_threads(), get_thread_name(tid), get_thread_context_switches(tid)
  • Messages / plots / locks / memory / callstacks: get_messages(), get_plots(), get_locks(), get_memory_events(), get_callstack_frames(...)
  • Capture metadata: get_capture_name(), get_capture_program(), get_first_time(), get_last_time(), get_resolution(), get_host_info()

Run print([m for m in dir(ctx) if not m.startswith('_')]) for the full list.

Units and conventions

  • All time values returned by Worker methods are nanoseconds (int). get_first_time() / get_last_time() bound the capture timeline.
  • ZoneStats fields: count, total, min, max, avg, sum_sq. total is the inclusive aggregate; use get_child_zone_stats(srcloc_id) to subtract child time when you need self-time.
  • get_all_zone_stats() returns dict[str, ZoneStats] keyed by an opaque label of the form 'name (addr)[arch] <srcloc_id>'. The trailing <id> is the source-location ID — the int accepted by get_zone_stats(int), get_zone_durations_by_id, and friends. Parse it with a regex if you need to join across calls.
  • Source-location IDs from get_all_zone_source_locations() are the join key between zone-name lookups and per-callsite queries.

Translating catalog entries to ctx Python

The catalog (tracy://catalog) lists curated queries. Each maps to a small Python snippet:

# zone_list — top 10 hottest zones by total time
top = sorted(ctx.get_all_zone_stats().items(),
             key=lambda kv: kv[1].total, reverse=True)[:10]
for k, v in top:
    print(f"{v.total/1e6:.2f}ms  count={v.count}  {k}")

# frame_list — primary frame set timing
times = ctx.get_frame_times()  # ns per frame
print(f"frames={len(times)}  avg={sum(times)/len(times)/1e6:.2f}ms  "
      f"p99={sorted(times)[int(len(times)*0.99)]/1e6:.2f}ms")

# zone_stats for a named zone — find the srcloc id, then drill in
import re
matches = [k for k in ctx.get_all_zone_stats() if k.startswith("MyFunc ")]
sid = int(re.search(r"<(\d+)>$", matches[0]).group(1))
stats = ctx.get_zone_stats(sid)

Async mode

For long-running queries pass async_mode=True to eval; it returns {task_id, status: "running"}. Poll with the task tool (action="poll", task_id=...).