Commit Graph

14 Commits

Author SHA1 Message Date
Alan Tse
e670bd18f7 feat(mcp): migrate to MCP Python SDK v2
`pip install mcp` now installs 2.x by default (v1 is
maintenance-only going forward), and nothing in this repo pinned a
version, so a fresh install already broke: v2 removed
mcp.server.fastmcp entirely, no compat shim.

- Import/class: mcp.server.fastmcp.FastMCP ->
  mcp.server.mcpserver.MCPServer.
- Transport options (host, port, sse_path, streamable_http_path)
  moved off the constructor and the settings object onto run();
  mutating mcp_server.settings.port now raises ValueError.
- _http_ping and the startup message read _SSE_PATH/
  _STREAMABLE_HTTP_PATH constants instead of settings.sse_path/
  settings.streamable_http_path -- v2 no longer exposes them to
  read back. Values match the SDK's own defaults in both versions
  ("/sse", "/mcp"), so served paths are unchanged.
- Adds extra/mcp/requirements.txt (mcp>=2.0.0,<3) -- there was no
  dependency manifest at all before this.

Decorator API is unchanged; every tool/resource here was already
async def, so the "sync handlers now run on worker threads" change
doesn't apply.

Verified in an isolated venv (mcp 2.0.0): module imports cleanly
with all 10 tools and 2 resources registered, the server starts
and serves streamable-http on the expected path, and _http_ping
correctly reports a running instance as alive.
2026-08-21 20:42:28 -07:00
Bartosz Taudul
8fd29b1038 Merge pull request #1441 from alandtse/fix/mcp-live-memory-limit
fix(mcp): cap live sessions to avoid OOM
2026-07-31 13:03:45 +02:00
Alan Tse
613524247f docs(mcp): document file-idle eviction TTL
The guide claimed "two backstops" and only described the LRU cap
and the disconnected-live TTL, omitting the new file-idle TTL
entirely -- a cold agent reading it would have no idea idle
file-loaded captures now expire too, or what env var controls it.
2026-07-30 22:07:54 -07:00
Alan Tse
a418ec6a03 fix(mcp): evict idle file-loaded captures too
Automatic eviction only ever covered disconnected live instances
(_evict_disconnected_idle explicitly skipped anything with a
path), so a file-loaded capture -- potentially many GB -- stayed
resident forever until the TRACY_MCP_MAX_INSTANCES cap forced an
LRU eviction to make room. list_instances already documented
"unload_capture instead of waiting for automatic eviction" as the
alternative, but no automatic path actually existed for this case.

Generalizes the sweep into _evict_idle: file-loaded instances now
get their own idle-since-last-use TTL (TRACY_MCP_FILE_IDLE_TTL_S,
default 1800s matching the disconnected-live TTL). Safe to evict
on a timer since they're already durably on disk -- load_capture
brings them back. Connected live instances are untouched, same as
before.
2026-07-30 22:07:33 -07:00
Alan Tse
00c2245c49 fix(mcp): recover cleanly from a hung server
Addresses the "server just vanishes" pattern from
alandtse/tracy#2, where Unable to connect gives no way to tell a
segfault apart from a hang and required manually killing an
unresponsive process before restart.

- faulthandler.enable() at startup writes a thread-state
  traceback to tracy_mcp.crash.log on a genuine fatal crash
  (works on Windows via SetUnhandledExceptionFilter).
- The periodic sweep loop now also logs a heartbeat (uptime,
  instance/task counts, evictions) so a hung event loop is
  distinguishable from a dead process by the last timestamp on
  disk.
- _is_our_server_running() now backs its os.kill(pid, 0) check
  with an HTTP self-ping. A deadlocked-but-alive process passed
  the old PID-only check, silently blocking restart; a
  non-responsive server is now reported by PID and a fresh
  instance starts on a new port instead.
2026-07-30 22:06:57 -07:00
Alan Tse
e4fab6af3a fix(mcp): cap live sessions to avoid OOM
live_connect wrapped Worker(addr, port) with no memoryLimit, so
the binding default (-1, unlimited) applied. A long-lived live
session on a busy target grows unbounded -- every zone/message/
memory event stays resident until disconnect -- and can OOM-kill
the whole server process rather than just that one instance.

Worker already has a graceful cap: TracyWorker.cpp's receive loop
checks memoryLimit and calls QueryTerminate() + a clean disconnect
once exceeded, instead of continuing to grow. Just wasn't wired up
from Python.

Adds memory_limit_mb to live_connect (defaults to
TRACY_MCP_LIVE_MEMORY_LIMIT_MB, 8192 if unset; 0 disables),
converts to bytes for the Worker constructor, and reports the
active limit in the connect response.
2026-07-30 21:48:06 -07:00
Alan Tse
5f2d14338e docs(mcp): fix eval-guide staleness
Two staleness gaps a cold agent would hit: get_sections()'s
documented {start, end, text} shape didn't match the actual
return value once category was added, and is_background_done()
-- introduced to close the stats-read race exercised while
testing the lock fix -- wasn't mentioned anywhere, so a cold
agent had no way to discover it short of dir(ctx).
2026-07-19 22:32:06 -07:00
Alan Tse
bfcec08e93 feat(mcp): expose background-load readiness
Smoke-testing the lock fix against a real 143M-zone capture
surfaced a second concurrent-mutation source: file loads spawn a
background thread that finishes populating zone/symbol statistics
after Worker construction returns, so get_all_zone_stats() et al.
could silently come back empty or partial with no way to tell
"not built yet" from "genuinely empty".

Adds is_background_done (a relaxed atomic, same as is_connected,
so no locked() wrapper needed) and surfaces it as background_done
in list_instances, with load_capture's docstring pointing callers
at it.
2026-07-19 22:32:04 -07:00
Alan Tse
9bbbec42f4 fix(mcp): cap tracy_mcp instance memory growth
tracy_mcp.py runs as a long-lived singleton shared across every MCP
client, and the `instances` dict never evicted entries — each
live_connect/load_capture materializes a full trace (zones, messages,
callstacks, memory events) with no size cap, so private bytes grow
without bound across recording sessions unless unload_capture is
called manually every time.

Add two backstops, both env-configurable: an LRU cap
(TRACY_MCP_MAX_INSTANCES, default 4) enforced on live_connect/
load_capture that never evicts a still-connected live instance, and a
periodic sweep that drops a disconnected live instance once it has
sat idle past TRACY_MCP_DISCONNECTED_TTL_S (default 1800s), keeping a
grace window for post-session analysis. list_instances now reports
connected/idle_seconds so callers can spot stale sessions, and
unload_capture/eviction now explicitly call Worker.shutdown() instead
of relying on GC. Document the lifecycle expectations (call
unload_capture proactively; treat eviction as a backstop) in
eval_guide.md.
2026-07-07 00:23:47 -07:00
Alan Tse
bee6ac566e docs(mcp): drop dangling tracy://catalog reference
eval_guide.md referenced a tracy://catalog resource that was never
registered (only tracy://prompt and tracy://eval-guide exist), so an
agent following the guide would try to read a nonexistent resource.
Remove the references; the worked snippets the catalog described are
already inlined under "Common query patterns".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016EvfzHvUsDBSAwEzTfLTtA
2026-06-19 15:51:51 -07:00
Alan Tse
6e4041b14d feat(mcp): expose sections accessor in eval bindings
Bind Worker::GetSections() as get_sections() so the TracySectionEnter /
TracySectionLeave instrumentation added to the client is reachable from
the MCP eval tool's ctx object. Returns a list of {start, end, text}
dicts with nanosecond timestamps, matching the existing list-of-dict
accessor convention. Documented in eval_guide.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016EvfzHvUsDBSAwEzTfLTtA
2026-06-19 15:44:12 -07:00
Alexey Shekhirin
96c681cf61 feat(mcp): default to streamable-http transport 2026-06-19 16:42:14 +01:00
Alan Tse
33fe84532e Add save_trace MCP tool for snapshotting live or loaded captures.
- save_worker binding: wraps Worker::Write under
  Worker::ObtainLockForMainThread() so live instances yield their
  receive thread cooperatively for the save's duration — the same
  pattern View::Save uses in the GUI.
- save_trace MCP tool: defaults to async_mode=True for multi-GB
  traces; reuses the existing Task/executor machinery so callers
  poll via the task tool. Path resolution mirrors load_capture.
- manual/tracy.tex: add save_trace bullet to the MCP tool list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 01:23:31 -07:00
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