pix-mcp
# pix-mcp
An MCP server for **[Microsoft PIX for Windows](https://devblogs.microsoft.com/pix/)** GPU/timing captures and **GDK PIX Xbox GPU captures**.
It drives `pixtool.exe`, PIX's own CLI, so anything PIX can do headlessly is reachable here. Every one of pixtool's 24 commands is covered by a tool, plus an escape hatch (`pix_run_pixtool`) for raw pipelines. On top of that, pix-mcp adds the parts pixtool leaves to you: an artifact cache (a 4.5 GB capture takes ~18 s to open, *every time*), paged responses, id-based navigation, and correct handling of two pixtool behaviours that are easy to get silently wrong (see [pixtool quirks](#pixtool-quirks-worth-knowing)).
pixtool is not all of PIX, though. The queue list, the resource table, and the Pipeline State view exist only in the UI — there is no command for any of them. And pixtool can *record* a timing capture but has no command that opens one, so the CPU/GPU timeline was unreadable headlessly. So pix-mcp also ships a small C# sidecar that hosts PIX's own analysis engines in-process, which is where [deep analysis](#deep-analysis) and [timing captures](#timing-captures) come from. It is optional: without it everything else still works.
Windows-only, by nature of PIX.
## Install
Requires Python 3.10+ and [PIX for Windows](https://devblogs.microsoft.com/pix/download/). The server finds `pixtool.exe` under `C:\Program Files\Microsoft PIX\<version>\`, preferring the newest install.
```console
pip install -e .
```
Add it to an MCP client — for Claude Code:
```console
claude mcp add pix -- pix-mcp
```
Or, without installing, point at the checkout:
```json
{
"mcpServers": {
"pix": {
"command": "python",
"args": ["-m", "pix_mcp"],
"env": { "PYTHONPATH": "C:\\path\\to\\pix-mcp\\src" }
}
}
}
```
Start any session with `pix_get_server_status`; it reports which PIX installs were found, whether pixtool answers, and the current limitations.
### The analysis bridge (optional)
The [deep-analysis tools](#deep-analysis) and the [timing-capture tools](#timing-captures) go through `bridge/`, a C# sidecar that loads PIX's own analysis assemblies. Building it needs the [.NET 8+ SDK](https://dotnet.microsoft.com/download) and a local PIX install:
```console
dotnet build bridge/pixbridge.csproj -c Release
```
Or ask the server to do it: `pix_build_bridge`. `pix_get_bridge_status` says whether it is available and why not. Everything else works without it.
### Xbox GPU captures (.xpix)
Xbox captures use GDK PIX, not the Windows `pixtool.exe`. The server recognizes the
`3XIP` container and its GPU platform tag, reports `capture_kind: "xbox_gpu"`, and
routes it to an independent `xbox-bridge/` process. Install the Microsoft GDK and
the .NET 8 Windows Desktop runtime plus a .NET 8 or newer SDK, then build:
```powershell
dotnet build xbox-bridge/pixbridge-xbox.csproj -c Release
# For a nondefault GDK location:
dotnet build xbox-bridge/pixbridge-xbox.csproj -c Release '-p:GdkPixDir=C:\GDK\bin'
```
The same build is available as `pix_build_bridge(platform="xbox")`. Check it with
`pix_get_bridge_status(platform="xbox")`; server status reports Windows and Xbox
availability independently. SDK assemblies and capture files are not included in Git.
The workflow is:
1. `pix_open_capture(capture_path="C:\\captures\\GPU 1.xpix")`.
2. Read `pix_list_queues`, `pix_list_gpu_events`, `pix_list_resources` and the stored
screenshot with `pix_save_screenshot`. These do not start GPU replay.
3. Call `pix_start_analysis(capture_id=..., device_address="<devkit IP>")` to authorize
replay. The address must match the analysis device configured in GDK PIX.
Replay can interrupt the title running on that devkit. `wait_seconds=0` starts
it without waiting; subsequent calls poll the same replay.
Windows-only `pix_open_capture` flags, including `remote`, are rejected for Xbox.
4. Once `ready` is true, collect exact counter names, rank timings, inspect pipeline
bindings, and export RTV/depth PNGs with `pix_save_resource(global_id=..., rtv=0, ...)`.
5. `pix_close_capture` releases the native document. LRU eviction and file changes
also release it. A hung bridge is terminated so later requests can recover.
Xbox timing uses the GDK's **nanosecond counters**, reported as `time_unit`.
Use `leaves_only=true` for timing rankings: API draws and their native child events
can carry the same duration. Queue-relative `index` and native `global_id` are
different IDs; pipeline queries use the former and resource exports the latter.
| Xbox capability | Support |
| --- | --- |
| Event paging/search/tree and CSV export | Supported, with cached repeat queries |
| Counter catalog and selected per-event counters | Supported; select at most 64 new counters per call |
| GPU replay and timing ranking | Explicit devkit selection required |
| Resource descriptors and names | Supported |
| Pipeline resource bindings and slots | Supported after replay |
| Stored screenshot and bound RTV/depth export | PNG; resource export requires an explicit `global_id` |
| Floating-point texture PNG | PIX-rendered display visualization; values can clip to the display range |
| Root-signature layout and complete resource view history | Not implemented; returns `unsupported_backend_operation` |
| Windows pixtool-only operations, Xbox live capture/recapture | Not implemented by this backend |
The GDK adapter uses the local SDK's Automation runtime plus internal document
and request types, so compatibility must be rechecked after a GDK update.
Compatibility testing covers a limited set of SDK builds and Xbox GPU capture
variants; other versions and variants require separate validation.
### Environment
| Variable | Purpose |
| --- | --- |
| `PIX_MCP_PIXTOOL_PATH` | Full path to a specific `pixtool.exe`, skipping discovery. |
| `PIX_MCP_PIX_DIR` | A PIX install directory to search instead of the default locations. |
| `PIX_MCP_CACHE_DIR` | Where extracted artifacts live. Defaults to `%TEMP%\pix-mcp-cache`. |
| `PIX_MCP_TIMEOUT_SECONDS` | Per-invocation pixtool timeout. Defaults to 1800. |
| `PIX_MCP_MAX_SESSIONS` | Open captures kept before the least-recently-used is evicted. Defaults to 8. |
| `PIX_MCP_BRIDGE_PATH` | Full path to `pixbridge.exe`, skipping discovery. |
| `PIX_MCP_BRIDGE_TIMEOUT_SECONDS` | Per-request bridge timeout. Defaults to 300. |
| `PIX_MCP_BRIDGE_DISABLED` | Set to `1` to leave the bridge alone entirely. |
| `PIX_MCP_BRIDGE_AUTOBUILD` | Set to `1` to build the bridge on first use if it is missing. |
| `PIX_MCP_XBOX_PIX_DIR` | GDK `bin` directory containing `Microsoft.PIX.Automation.dll`. |
| `PIX_MCP_XBOX_BRIDGE_PATH` | Full path to `pixbridge-xbox.exe`. |
| `PIX_MCP_XBOX_BRIDGE_DISABLED` | Set to `1` to disable the Xbox backend. |
## Tools
### Capture lifecycle
| Tool | |
| --- | --- |
| `pix_open_capture` | Register a `.wpix` or Xbox GPU `.xpix` and get a `capture_id` plus an overview. |
| `pix_get_capture_overview` | Frame, event, and category statistics. |
| `pix_discover_queues` | Find which named contexts the capture exposes. |
| `pix_list_open_captures` · `pix_close_capture` · `pix_clear_capture_cache` | Session bookkeeping. |
### Events
| Tool | |
| --- | --- |
| `pix_list_events` | Paged event list, filterable by category or name. |
| `pix_search_events` | Glob search over event names. |
| `pix_get_event` | One event by global id, with its ancestry. |
| `pix_get_event_tree` | Depth- and node-bounded hierarchy. |
| `pix_export_event_list` | Write the full CSV somewhere you choose. |
### Counters
| Tool | |
| --- | --- |
| `pix_list_counters` | The catalog, grouped and searchable (2,365 counters on an RTX 5080). |
| `pix_get_counter_hotspots` | Rank events by a counter; picks a duration counter if you don't name one. |
| `pix_collect_counters` | Attach chosen counter values to events. |
| `pix_suggest_counter_pattern` | Turn a counter name into the regex that selects it. |
| `pix_save_high_frequency_counters` | Time-series counter capture to CSV. |
### Deep analysis
Through the [bridge](#the-analysis-bridge-optional), not pixtool. Each of these answers something pixtool has no command for, and none of them re-open the capture.
| Tool | |
| --- | --- |
| `pix_list_queues` | The real queue table: id, name, type, event count. |
| `pix_list_gpu_events` | Events from one queue, optionally with the decoded D3D12 call and its arguments. |
| `pix_list_resources` | PIX's resource table — every texture and buffer, with dimensions, format, and debug name. |
| `pix_get_resource` | One resource in full: descriptor, heap, initial state, view summary. |
| `pix_get_resource_views` | Every view of a resource **and the events that bind it** — where a texture is actually used, and as what. |
| `pix_get_pipeline_state` | PIX's Pipeline State view for one event: program type, root signature, bound views. |
| `pix_get_root_signature` | The binding layout alone, when the bound-view detail is noise. |
| `pix_start_analysis` | Replay the capture on a GPU, which is where PIX's timing comes from. ~25 s for a 4.5 GB capture, once. |
| `pix_get_event_timing` | Per-event GPU timing off that timeline: start and duration for every event in a queue, ranked by cost on request. |
| `pix_get_bridge_status` · `pix_build_bridge` | Bridge health and one-command build. |
`pix_get_event_timing` is the cheap way to find the expensive work in a frame.
`pix_get_counter_hotspots` replays the capture once per counter; this replays it once for
everything, then reads a timeline that already exists. The ranking is dominated by marker
scopes — `Frame`, then `Scene`, then `PostProcessing` — which is the shape of the frame;
`leaves_only` ranks the individual draws and dispatches inside instead.
### Timing captures
`.wpix` names two unrelated file formats. A **GPU capture** is one frame, frozen and replayable — PIX's own `GFXA` container, and what every tool above reads. A **timing capture** is seconds of the whole machine's CPU and GPU timeline, and it is a SQLite database. Nothing that reads one can read the other, which is why PIX's GPU-capture API rejects a timing capture with `SQLITE_NOTADB` rather than a format error. `pix_open_capture` tells them apart by their header — the extension settles nothing — and reports `capture_kind`; a tool handed the wrong kind says so by name instead of failing somewhere deeper.
pixtool records these (`pix_capture_timing`) and cannot open them. The bridge can: PIX's timing views run on a managed assembly beside pixtool, and it turns out to run standalone — no UI, no GPU, no document.
| Tool | |
| --- | --- |
| `pix_get_timing_summary` | The window, the machine, and which tables actually have data. Start here. |
| `pix_list_timing_processes` | Every process alive during the capture, busiest first — a timing capture is machine-wide. |
| `pix_list_timing_threads` | One process's threads, with what was recorded for each. |
| `pix_get_cpu_samples` | Where one thread's CPU time went: when it ran, and on which core. |
| `pix_get_context_switches` | Every time a thread came on or off a core, and the wait reason it left for. |
| `pix_get_core_activity` | The inverse view: everything that ran on one core, whoever owned it. |
| `pix_list_timing_gpu_queues` | The GPU queues the capture saw. |
| `pix_get_thread_timeline` · `pix_get_gpu_timeline` | Named PIX scopes on a thread or a queue, with nesting. |
| `pix_get_gpu_frames` | The present timeline, one row per frame, plus frame pacing over the page. |
| `pix_get_residency_operations` | What the GPU made resident, evicted, or paged — and which object it was. |
| `pix_list_timing_counters` | Counters reported live during the capture, per process. |
| `pix_list_storage_devices` · `pix_get_file_io` | Disk activity: which device, which file, how long it blocked. |
| `pix_get_page_faults` | One thread's page faults, hard ones separated from the cheap kinds. |
| `pix_list_metrics` · `pix_get_metric_values` | Any metric as a series over time: a counter's values, one core's utilisation, one process's video memory. |
| `pix_get_api_objects` | Named D3D12 work on one thread — a pipeline compile, a resource creation — as spans. |
| `pix_get_command_lists` | Which command lists a queue executed, and which of them went in on one `ExecuteCommandLists`. |
| `pix_get_allocation_moves` | Allocations the driver migrated between heaps, or demoted out of video memory for good. |
| `pix_list_custom_data_types` · `pix_get_custom_data` | Lanes the title recorded itself, plus PIX's own per-monitor VSync train. |
| `pix_list_controllers` · `pix_get_controller_input` | Controller reads — where an end-to-end input-latency measurement starts. |
`pix_get_context_switches` is the one that separates a thread that is slow because it is
computing from one that is slow because it is blocked — a distinction a sample count cannot
make, since a blocked thread simply produces fewer samples and looks idle. File I/O, page
faults and residency are the same argument in three other places: a thread waiting on a
synchronous read is off-core and reads as idle, and an allocation evicted and paged back over
PCIe costs milliseconds on a frame whose draw calls are unchanged. None of it is visible in a
GPU capture at all.
`pix_get_core_activity` answers the question the per-thread tools cannot: not "where did my
thread's time go" but "was this core mine at all". It reports `idle_share_of_page` separately
from `busiest_process_share`, because process 0 is Windows' Idle process — counting it as the
busiest would turn "this core was a third idle" into "something else owned a third of this
core", which is the opposite finding.
`pix_get_metric_values` is the other half of that answer, and the only tool here that returns a
*series* rather than a list of events. The same core can be asked about twice — once for one
process, once for `all_processes` — and the gap between the two answers is exactly how much of
a busy core belonged to somebody else. The other lanes are the counters over time (the catalog
`pix_list_timing_counters` returns is only their definitions), and per-process video memory.
**Latency is reachable here and nowhere else.** A GPU capture is one frame with no before and
no after, so it cannot contain the span a player actually feels. A timing capture can:
`pix_get_controller_input` timestamps the read, `pix_get_gpu_frames` timestamps the present,
and the monitor lane in `pix_get_custom_data` timestamps the VSync that put the result on the
display. PIX's own Dynamic Latency Instrumentation, when a device has it, anchors the first of
those to the hardware rather than inferring it — `latency_instrumented` on
`pix_list_controllers` says which.
**Times are nanoseconds** on the capture's own clock, and every window argument is in those
units. `pix_get_timing_summary` reports both `duration_seconds` (first event to last) and
`recorded_seconds` (the requested recording window); they differ because trailing ETW events
land after the stop.
**What was verified how.** Checked against real rows on real captures — a 144 MB attach to a
running Unreal editor and a 1 GB 180-second launch capture, the latter reading 17.6 M context
switches: the summary, process, thread, CPU-sample, context-switch and GPU-queue queries;
`pix_list_timing_counters` (12 counters, `Local Budget` / `Local Usage` / `Local Resident` in
MB, grouped per adapter); `pix_get_core_activity` (3,080 samples on core 0, cross-process, and
a core 63.8% owned by a process that was not the captured title); and
`pix_get_residency_operations` (4 `MakeResident` rows naming an `nv.sl.chi.heap.gpu` descriptor
heap).
Also on real rows:
* `pix_get_metric_values` on the counter lane, checked against the bytes rather than against
PIX: the `Local Budget` counter for the discrete adapter returns 15947.792384 MB, and
decoding the `PixCounterRange` blob out of the capture's own table gives the same double.
That is the RTX 5080's 16 GB. Ask for it by `metric_id` from `pix_list_timing_counters` and
by that capture's ids — the same machine numbers its adapter groups differently between two
recordings, so the counter that is 7 in one is 1 in the other.
* The same tool on the `cpu_usage` lane, over all 85 samples of the 8.9-second capture at its
100 ms cadence: core 0 averaged 30.9% across every process and core 12 averaged 75.2% —
the same imbalance `pix_get_core_activity` found from the other direction. Asking again with
`process_id` instead of `all_processes` is what makes it a finding: the captured editor
accounts for 8.1% of core 0 but only **1.2% of core 12**, so that core was busy with
somebody else almost entirely.
* `pix_get_custom_data` on the monitor lane: 10,879 VSync markers over 180 seconds, reported
as 16.666 ms median / 60.0 Hz — and a longest interval of 33.346 ms, which is exactly two
refresh periods, so the display missed one. On the shorter capture the longest gap is
49.995 ms, three periods. `pix_list_custom_data_types` names the lane
`Monitor #1 (NVIDIA GeForce RTX 5080)`.
* `pix_get_api_objects`: one span, `Pipeline State Compilation`, 177.7 µs — on one thread out
of the 554 that process had.
* `pix_get_metric_values` on the `api_object_memory` lane: the captured editor's D3D12 objects
grew 131,072 → 196,608 bytes, which is the sum PIX records against those objects. Every
other process on the machine returns an empty series with its reason, which is the correct
answer for a process that never created a D3D12 object.
The API object result is also the caveat. `pix_get_timing_summary` reports `api_objects: true`
from the object *catalog*, but the events are per thread and there may be a single one in the
whole capture, so querying an arbitrary thread returns nothing and looks broken.
`api_object_events` on each `pix_list_timing_threads` row is how to find a thread that has any,
and the empty answer says so.
The rest run clean and return `S_OK` with **zero rows on every capture obtainable here**, for
reasons that are facts about the recording rather than about the code:
* `pix_get_thread_timeline`, `pix_get_gpu_timeline`, `pix_get_gpu_frames` and the PIX-marker
path need a process that calls `PIXBeginEvent` and presents a D3D12 swapchain. No
PIX-instrumented title was available to capture.
* `pix_list_storage_devices`, `pix_get_file_io`, `pix_get_page_faults` and the two device
lanes of `pix_get_metric_values` need ETW providers that `pixtool take-new-timing-capture`
cannot enable — its only options are `--duration`, `--sampleRate`, `--noCpuSamples`,
`--noCallstacks` and `--noGpuTimings`. Confirmed by scanning 129 threads across six
processes for a single page fault, and by both captures reporting zero storage devices. A
capture recorded from the PIX UI with file I/O turned on will populate them.
* `pix_get_command_lists`, `pix_get_allocation_moves`, `pix_list_controllers` and
`pix_get_controller_input` have empty source tables in both captures:
`ApiQueueExecutionCommandList`, `AllocationMigrations`, `DemotedAllocations`, `InputDevice`
and `InputMarkerRange` all hold zero rows. No controller was attached, video memory was
never short, and neither capture carries GPU timings.
Those last row counts are *counted*, not inferred, and that is worth stating because it is the
one place this file format helps: a timing capture is a SQLite database, so
`tools/probe_capture_tables.py` opens it with Python's own `sqlite3` and counts the table
directly. "The query found nothing" and "there is nothing to find" are different claims, and
this is the only part of the surface where the second one can be checked without going through
PIX at all. Field mapping for the still-unpopulated tables above comes from PIX's own query
metadata rather than from observed rows. Each returns a note explaining which kind of empty it
is, so an empty answer is never ambiguous between "no data" and "broken".
Two things about recording, both learned the hard way: a process `pixtool launch` starts
**dies when pixtool exits**, so launch and capture must be one command line; and attaching to
an already-running process gives you the full CPU tables but **no GPU frames and no PIX
events**, because D3D12 instrumentation has to be in the process before it creates its device.
`pix_run_debug_layer` · `pix_collect_occupancy` · `pix_perform_playback` · `pix_recapture_playback` · `pix_recapture_region`
### Resources and export
`pix_save_screenshot` · `pix_save_resource` · `pix_export_to_cpp` · `pix_upgrade_capture`
### Taking new captures
`pix_capture_app` · `pix_capture_uwp_app` · `pix_capture_attached_process` · `pix_capture_programmatic` · `pix_capture_timing`
These launch or attach to a process, so they are annotated `openWorldHint`. pixtool has no daemon: launching a target and capturing from it must happen inside **one** pixtool process, which is why these are single tools rather than a launch/capture pair.
### Escape hatch
`pix_run_pixtool` builds an arbitrary command pipeline. Use it for anything the typed tools don't reach.
## pixtool quirks worth knowing
These cost real time to discover, and both fail *quietly* — worth knowing whether or not you use this server.
**`--counters` takes a regular expression, not a glob.** pixtool expands `*` to `.*` and then runs a case-sensitive `regex_search`. So a name copied straight out of `list-counters` usually matches **nothing**: `TOP to EOP Duration (ns)` reads as a group around a literal `ns`, and `.` matches any character. You get an empty column rather than an error. pix-mcp escapes and anchors names passed as `counters`, so they select exactly themselves; `counter_regexes` takes raw patterns when you actually want a family. An unbalanced paren is worse — it surfaces as an opaque `PIXTOOL99999 ... 0x8000ffff` that aborts the rest of the pipeline, *after* the capture open has been paid for, so patterns are validated up front.
**Quotes must open after the `=`.** pixtool parses the raw command line itself rather than going through `CommandLineToArgvW`, and only honours a quote that begins after the `=`. `--queue-name="3D Queue"` parses; `"--queue-name=3D Queue"` — which is what Python's `subprocess` list quoting produces — is rejected as an invalid command line. pix-mcp quotes the value, not the token. A value containing a double quote cannot be expressed at all, so those are rejected with an explanation rather than silently mangled.
Two smaller ones: a failing command **aborts the remaining commands** in the pipeline, so batched extraction puts the most important command first; and pixtool exits non-zero even when `--help` succeeds.
## Design notes
**Every query re-opens the capture.** pixtool has no interactive mode, so a "session" here is bookkeeping plus a cache directory, not a live process. Opening the 4.5 GB reference capture costs ~18 s — and roughly 90 s each if four opens run concurrently, so the server does not parallelise them. Artifacts are cached under a fingerprint of path, size, and mtime; repeat queries return in milliseconds, and editing the capture invalidates the cache.
**Except through the bridge, which keeps it open.** The sidecar holds the document, so the same 4.5 GB capture opens once in ~0.3 s and every query afterwards is effectively free. That is why the deep-analysis tools are not cached: there is nothing to amortise. It stays a separate process rather than a Python binding because the API is COM-with-C#-extensions, and because a crash in PIX's engine should not take the server with it.
**The bridge hosts two engines, not one.** A GPU capture goes through the COM document API; a timing capture goes through `Microsoft.PIX.Core`'s managed query engine over the SQLite file. They share only the sidecar process and the handle table, so `pix_open_capture` has to decide which one a file belongs to before it can open it at all — hence the header sniff rather than a trial open, which on a 4.5 GB file would cost 18 seconds to learn nothing.
**Responses are small by default.** Lists are paged, text is chunked, and events are addressed by `global_id`, so an agent can navigate a large capture without flooding its context.
**Errors are data.** A failed tool returns `{"ok": false, "error": {"code", "message", "details"}}` with a message that says what to do next, rather than a transport-level failure.
**Counters are expensive.** Each matched counter costs another replay pass, which is why the default is exact selection rather than a wildcard.
## Development
```console
pip install -e . --group dev # or: uv sync
pytest
ruff check .
mypy
```
`tests/test_server.py` holds the parity check: a table mapping all 24 pixtool commands to the tool that covers each. Add a command to pixtool and nothing covers it, and that test fails.
The tests are hermetic — no PIX install needed. `tools/smoke.py` is the end-to-end run against a real capture:
`tools/smoke_full.py` launches an independent server and exercises the **real MCP
stdio transport**. It checks nonempty results, event/CSV consistency, numeric
counter values, paging, cache reuse, PNG headers/dimensions, expected errors and
native handle release. It writes each result and `report.json`; an unexpected
failure exits nonzero. Choose a fresh output directory for a cold-cache run.
Image contents still need visual inspection; a valid PNG alone cannot prove a
texture is meaningful.
```powershell
python tools/smoke_full.py 'C:\captures\GPU 1.xpix' --device-address '<devkit IP>' --output-dir '_probe/full-xbox'
python tools/smoke_full.py 'C:\captures\GPU 1.wpix' --output-dir '_probe/full-windows'
python tools/smoke_full.py 'C:\captures\timing.wpix' --output-dir '_probe/full-timing'
```
The earlier scripts are useful for narrower probes:
```console
python tools/smoke.py "C:\path\to\capture.wpix"
python tools/smoke_mcp.py "C:\path\to\capture.wpix" # through FastMCP
python tools/smoke_analysis.py "C:\path\to\capture.wpix" # the bridge-backed tools
python tools/probe_timing_bridge.py "C:\path\to\timing.wpix" # the timing engine
python tools/probe_timing_objects.py "C:\path\to\timing.wpix" # objects, metrics, input
python tools/probe_objects_via_mcp.py "C:\path\to\timing.wpix" # the same, as tools
python tools/probe_capture_tables.py "C:\path\to\timing.wpix" # row counts, via sqlite3
```
`tools/probe_*.py` are the scripts used to establish the pixtool behaviours above, kept because they are how you'd re-verify them against a future PIX release.
## License
MIT
TDQS
Scored across 67 tools
Most tools have distinct purposes, but a few pairs overlap, such as pix_list_queues vs pix_discover_queues and pix_list_events vs pix_list_gpu_events. The detailed descriptions help, but the large number of tools makes misselection more likely.
All tools follow a consistent snake_case verb_noun pattern with the pix_ prefix, such as list_resources, get_pipeline_state, and capture_timing. This consistency makes the tool's action and target predictable across the entire set.
With 67 tools, this far exceeds the typical well-scoped MCP server range. While the PIX domain is broad, the count is unwieldy and could benefit from being split into focused servers by area such as capture, timing, or GPU analysis.
The tool set covers the full capture-analysis lifecycle: capture from various targets, open/close/upgrade captures, inspect GPU/CPU timing, events, resources, pipeline state, counters, occupancy, memory, input, and export. No obvious dead ends or missing core operations are apparent.