Skip to main content
Glama
README.md
# wpa-mcp

> An **MCP (Model Context Protocol) server** that turns Windows **WPR `.etl`** traces into structured, LLM-friendly performance insights — using **WPAExporter** + **xperf** under the hood, and optionally emitting **flamegraph-ready folded stacks**.

`wpa-mcp` bridges two worlds:

- **Windows Performance Analyzer (WPA)** — the gold standard for analyzing ETW / WPR traces, but GUI-heavy and hard to automate.
- **LLMs (Claude, Copilot, GPT, …)** — great at reasoning across evidence, but blind to `.etl` files.

This server exposes a small set of MCP tools so an LLM can:

1. **Validate** a trace (does it actually contain the events needed for analysis?)
2. **Export** the right WPA tables to CSV via predefined profiles
3. **Summarize** the CSVs into a compact JSON (Top N processes, hot stacks, ready-thread latency, DPC/ISR offenders, UI jank)
4. **Render** a Brendan-Gregg-style **folded stack** file for flamegraphs — or for the LLM to read directly

---

## Table of contents

- [Architecture](#architecture)
- [Prerequisites](#prerequisites)
- [Install](#install)
- [Capture a trace](#capture-a-trace)
- [MCP tools](#mcp-tools)
- [Built-in WPA profiles](#built-in-wpa-profiles)
- [Analysis examples](#analysis-examples)
  - [Example 1: Runaway CPU](#example-1-runaway-cpu)
  - [Example 2: UI hang / "not responding"](#example-2-ui-hang--not-responding)
  - [Example 3: Audio/mouse glitch caused by a driver](#example-3-audiomouse-glitch-caused-by-a-driver)
  - [Example 4: Feeding folded stacks to the LLM](#example-4-feeding-folded-stacks-to-the-llm)
- [Client configuration](#client-configuration)
- [Release process](#release-process)
- [Troubleshooting](#troubleshooting)
- [FAQ](#faq)

---

## Architecture

```
+------------------+       stdio (MCP)        +--------------------+
|  LLM / MCP host  |  <-------------------->  |   wpa-mcp server   |
| (Claude, VSCode) |                          |  (this repo)       |
+------------------+                          +----------+---------+
                                                         |
                                             subprocess  |
                                                         v
                                   +---------------------+---------------------+
                                   |  xperf.exe          |  wpaexporter.exe    |
                                   |  (validate / stats) |  (+ .wpaProfile)    |
                                   +---------------------+---------------------+
                                                         |
                                                         v
                                              CSV tables (per profile)
                                                         |
                                                         v
                                       summarizer -> JSON  /  flamegraph -> .folded
```

Everything that the LLM sees is **structured JSON** or **compact folded-stack text** — never raw gigabyte CSVs.

---

## Prerequisites

- **Windows 10/11** (required; the analysis tools are Windows-only)
- **Windows Performance Toolkit (WPT)** installed (ships with Windows ADK / Windows SDK)
  - `wpaexporter.exe`
  - `xperf.exe`
- **Python 3.10+**

If WPT is installed to a non-default path, set:

```powershell
setx WPAEXPORTER_PATH \"C:\Program Files (x86)\Windows Kits\10\Windows Performance Toolkit\wpaexporter.exe\"
setx XPERF_PATH       \"C:\Program Files (x86)\Windows Kits\10\Windows Performance Toolkit\xperf.exe\"
```

---

## Install

### Via pipx (recommended — works once published to PyPI)

```powershell
pipx install wpa-mcp
wpa-mcp    # starts the MCP stdio server
```

### From source

```powershell
git clone https://github.com/Jialong-zhong/wpr-xperf-mcp-server.git
cd wpr-xperf-mcp-server
pip install -e .
wpa-mcp
```

---

## Capture a trace

The server's analyses are only as good as the providers you captured. Recommended capture for the four problem classes this server targets:

```powershell
# Run as Administrator
wpr -start CPU ^
    -start GeneralProfile ^
    -start DesktopComposition ^
    -start Registry ^
    -filemode

# ... reproduce the issue ...

wpr -stop C:\traces\case01.etl \"repro notes here\"
```

| WPR profile            | What it adds that wpa-mcp uses                            |
|------------------------|-----------------------------------------------------------|
| `CPU`                  | Sampled CPU, CSwitch, ReadyThread, StackWalk              |
| `GeneralProfile`       | Processes, images, DPC/ISR                                |
| `DesktopComposition`   | DWM frame timing, Window-in-focus (UI hang evidence)      |
| `Registry`             | Registry activity (optional; useful for startup/UI hangs) |

> If you skip `CPU`, the most valuable analyses (hot stacks, scheduling latency) won't work — `validate_trace` will tell you so.

---

## MCP tools

| Tool | Purpose | Typical caller |
|------|---------|----------------|
| `validate_trace(etl_path)` | Run `xperf -a stats` and report which providers / stacks exist | LLM, always first |
| `export_tables(etl_path, profile)` | Run **one** WPA profile via `wpaexporter` and return CSV paths | Advanced / targeted |
| `analyze_etl(etl_path, focus)` | Validate → export (by focus) → summarize. Returns one structured JSON | LLM, default entry point |
| `analyze_audio_glitch(etl_path)` | MXA-style audio glitch analysis: long DPC/ISR buckets, dispatch-level suspects, and pipeline risk | LLM, audio crackle/skip triage |
| `analyze_audio_glitch_ex4(etl_path)` | Executable MXA Exercise 4 method: long DPC/ISR, dispatch blocking, I/O starvation, decoder realtime checks | LLM, root-cause oriented audio triage |
| `render_flamegraph(out_dir)` | Aggregate `CPU Usage (Sampled)` stacks into Brendan-Gregg folded format | After `analyze_etl` with CPU focus |

### `analyze_etl` input schema

```json
{
  \"etl_path\": \"C:\\traces\\case01.etl\",
  \"focus\": \"cpu | latency | ui | dpc_isr | all\",
  \"out_dir\": \"optional override\",
  \"top_n\": 20
}
```

### `analyze_etl` output shape (abbreviated)

```json
{
  \"etl\": \"C:\\traces\\case01.etl\",
  \"focus\": \"all\",
  \"validation\": {
    \"duration_sec\": 42.7,
    \"has_cpu_sampling\": true,
    \"has_cswitch\": true,
    \"has_readythread\": true,
    \"has_stacks\": true,
    \"has_dpc_isr\": true,
    \"has_dwm\": true,
    \"warnings\": []
  },
  \"exports\": [\"...\\cpu\\CPU Usage (Sampled)_...csv\", \"...\"],
  \"summary\": {
    \"cpu_top_processes\": [{\"process\": \"chrome.exe\", \"weight_ms\": 8421.3}],
    \"cpu_top_modules\":   [{\"module\":  \"ntdll.dll\",   \"weight_ms\": 2310.0}],
    \"cpu_hot_stacks\":    [{\"stack\":   \"ntdll!... ; app!hot_fn\", \"weight_ms\": 1240.0}],
    \"ready_latency_top\": [{\"process\": \"explorer.exe\", \"tid\": 1234, \"p95_ms\": 187.0}],
    \"dpc_isr_top\":       [{\"driver\":  \"ndis.sys\",     \"total_ms\": 95.2, \"max_us\": 820}],
    \"ui_focus_top\":      [{\"process\": \"myapp.exe\",    \"focus_ms\": 5400.0}],
    \"dwm_slow_frames\":   {\"count\": 38, \"p95_ms\": 41.7, \"max_ms\": 128.0}
  }
}
```

### `analyze_audio_glitch` input schema

```json
{
  \"etl_path\": \"C:\\traces\\AudioGlitches_ThreadsAtDispatchLevel.etl\",
  \"top_n\": 20,
  \"dispatch_threshold_us\": 1000,
  \"exclude_idle\": true
}
```

### `analyze_audio_glitch` output shape (abbreviated)

```json
{
  \"etl\": \"C:\\traces\\AudioGlitches_ThreadsAtDispatchLevel.etl\",
  \"validation\": { \"duration_sec\": 69.5, \"has_dpc_isr\": true },
  \"analysis\": {
    \"mxa_criteria\": { \"long_dpc_or_isr_threshold_us\": 1000 },
    \"audio_glitch_assessment\": {
      \"risk\": \"medium\",
      \"long_running_dpc\": { \"total_count\": 43808, \"over_threshold_count\": 0 },
      \"long_running_isr\": { \"total_count\": 23838, \"over_threshold_count\": 0 },
      \"audio_related_drivers_in_dpc_top\": [{ \"driver\": \"portcls.sys\", \"total_ms\": 116.7 }],
      \"audio_pipeline_processes\": [{ \"process\": \"audiodg.exe\", \"weight_ms\": 244.7 }]
    },
    \"base_summary\": { \"dpc_isr_top\": [], \"cpu_business_processes\": [] }
  }
}
```

### `analyze_audio_glitch_ex4` output shape (abbreviated)

```json
{
  "etl": "C:\\traces\\AudioGlitches_ThreadsAtDispatchLevel.etl",
  "validation": { "duration_sec": 69.5, "has_dpc_isr": true },
  "analysis": {
    "method": {
      "name": "mxa_exercise4_audio_glitch_method",
      "version": "1.0"
    },
    "checks": [
      { "id": "long_dpc_isr", "status": "pass|warn|fail" },
      { "id": "dispatch_level_blocking", "status": "pass|warn" },
      { "id": "io_delivery_pressure", "status": "pass|warn" },
      { "id": "decoder_realtime_capacity", "status": "pass|warn" }
    ],
    "glitch_likelihood": "low|medium|high",
    "most_likely_causes": ["..."],
    "actions": ["..."]
  }
}
```

---

## Built-in WPA profiles

Each profile is a `.wpaProfile` XML that tells `wpaexporter` which WPA tables + columns to dump.

| Focus key | File | Tables exported |
|-----------|------|-----------------|
| `cpu`     | `wpa/profiles/cpu_hotpath.wpaProfile`         | CPU Usage (Sampled) |
| `latency` | `wpa/profiles/scheduling_latency.wpaProfile`  | CPU Usage (Precise), Ready Thread |
| `ui`      | `wpa/profiles/ui_hang.wpaProfile`             | Window In Focus, DWM Frame Details |
| `dpc_isr` | `wpa/profiles/dpc_isr.wpaProfile`             | DPC/ISR Duration |

Column sets are deliberately minimal to keep CSVs small and summarizer-friendly.

---

## Analysis examples

These are end-to-end, copy-pasteable walkthroughs. Each shows the **user prompt**, the **tool calls the LLM should make**, the **JSON shape you can expect**, and the **conclusions a well-prompted LLM should draw**.

### Example 1: Runaway CPU

**User:** *\"`C:\traces\cpu_spike.etl` — some process is pinning my CPU at 100%. Find it and tell me which function.\"*

**LLM tool calls:**

```jsonc
// 1) validate
validate_trace({ \"etl_path\": \"C:\\traces\\cpu_spike.etl\" })

// 2) analyze focused on CPU
analyze_etl({ \"etl_path\": \"C:\\traces\\cpu_spike.etl\", \"focus\": \"cpu\", \"top_n\": 10 })

// 3) (optional) generate folded stacks for the hottest paths
render_flamegraph({
  \"out_dir\": \"C:\\traces\\cpu_spike_wpa_out\\cpu\",
  \"top_n\": 200,
  \"min_weight_ms\": 5
})
```

**Typical `summary` returned:**

```json
{
  \"cpu_top_processes\": [
    { \"process\": \"myapp.exe\",  \"weight_ms\": 38210.0 },
    { \"process\": \"chrome.exe\", \"weight_ms\":  2100.0 }
  ],
  \"cpu_top_modules\": [
    { \"module\": \"myapp.exe\",  \"weight_ms\": 31020.0 },
    { \"module\": \"ucrtbase.dll\", \"weight_ms\": 4100.0 }
  ],
  \"cpu_hot_stacks\": [
    { \"stack\": \"ntdll!RtlUserThreadStart;kernel32!BaseThreadInitThunk;myapp!worker_loop;myapp!compress_chunk\",
      \"weight_ms\": 27800.0 }
  ]
}
```

**What the LLM should conclude:**

- Trace duration 42 s, `myapp.exe` consumed ~38 s of CPU ≈ **~90% of wall time on a single core equivalent.**
- Nearly all weight is in `myapp!compress_chunk` via `worker_loop` → this is a **hot loop in the app itself**, not the runtime or OS.
- Recommend: inspect `compress_chunk`, consider SIMD / chunking, and capture a second trace with `-start CPU -start FileIO` to see if the loop is also I/O-bound.

---

### Example 2: UI hang / \"not responding\"

**User:** *\"When the user clicks **Save**, the window freezes for ~2 seconds. Trace at `C:\traces\save_hang.etl`.\"*

**LLM tool calls:**

```jsonc
validate_trace({ \"etl_path\": \"C:\\traces\\save_hang.etl\" })
analyze_etl({ \"etl_path\": \"C:\\traces\\save_hang.etl\", \"focus\": \"ui\" })
analyze_etl({ \"etl_path\": \"C:\\traces\\save_hang.etl\", \"focus\": \"latency\" })
```

**Expected signals:**

```json
{
  \"ui_focus_top\": [{ \"process\": \"myapp.exe\", \"focus_ms\": 5400 }],
  \"dwm_slow_frames\": { \"count\": 42, \"p95_ms\": 58.0, \"max_ms\": 1980.0 },
  \"ready_latency_top\": [
    { \"process\": \"myapp.exe\", \"tid\": 7321, \"p95_ms\": 14.0 }
  ]
}
```

**Reasoning rubric the LLM should apply:**

- `dwm_slow_frames.max_ms ≈ 1980` while the app is in focus → **the hang is real and UI-thread-bound.**
- `ready_latency_top` for the UI thread is low (≈14 ms p95) → the thread **wasn't starved for CPU**; it was **busy on its own work** (otherwise we'd see hundreds of ms of ready-wait).
- Therefore the UI thread is doing synchronous work during Save. Likely culprits: synchronous disk/network I/O, lock contention, or a heavy compute path on the UI thread.
- Next step: re-capture with `-start CPU -start FileIO -start DiskIO` and focus on the UI thread's stacks with `render_flamegraph` to see **exactly what** it's doing during those 2 seconds.

> Contrast: if `ready_latency_top` had been, say, `p95_ms: 900` — then the UI thread **was ready but not scheduled**. That's a scheduling/contention story, not a "heavy work on UI thread" story.

---

### Example 3: Audio/mouse glitch caused by a driver

**User:** *\"Audio crackles and the mouse skips for a split second every few minutes. `C:\traces\glitch.etl`.\"*

**LLM tool calls:**

```jsonc
validate_trace({ \"etl_path\": \"C:\\traces\\glitch.etl\" })
analyze_audio_glitch_ex4({ \"etl_path\": \"C:\\traces\\glitch.etl\", \"dispatch_threshold_us\": 1000 })
```

**Expected signal:**

```json
{
  \"glitch_likelihood\": \"high\",
  \"checks\": [
    { \"id\": \"long_dpc_isr\", \"status\": \"fail\" },
    { \"id\": \"dispatch_level_blocking\", \"status\": \"warn\" },
    { \"id\": \"io_delivery_pressure\", \"status\": \"warn\" },
    { \"id\": \"decoder_realtime_capacity\", \"status\": \"pass\" }
  ],
  \"top_dpc_drivers\": [
    { \"driver\": \"Netwtw10.sys\", \"total_ms\": 312.4, \"count\": 1820 },
    { \"driver\": \"ndis.sys\", \"total_ms\": 95.1, \"count\": 4300 }
  ]
}
```

**What the LLM should conclude:**

- `Netwtw10.sys` (Intel Wi-Fi driver) has a **single DPC over 4 ms** — that's well above the ~1 ms \"don't cause audio glitches\" rule of thumb.
- Correlation with symptom: Wi-Fi DPC storms typically line up with mouse/audio skips because DPCs run at elevated IRQL and block the audio/HID stack.
- Recommend: update the Wi-Fi driver; if the problem persists, disable power-saving for the Wi-Fi adapter and re-capture.

> Quality rules wpa-mcp's prompting guide bakes in: any driver with `max_us > 1000` is suspicious, `>= 500` worth mentioning.

### Example 3B: Exercise 4 workflow (Chinese quick-start)

**用户:**“`C:\\traces\\audio_noise.etl` 分析是否有音频噪点,并给出最可能原因和修复优先级。”

**推荐调用顺序:**

```jsonc
validate_trace({ \"etl_path\": \"C:\\traces\\audio_noise.etl\" })
analyze_audio_glitch_ex4({
  \"etl_path\": \"C:\\traces\\audio_noise.etl\",
  \"dispatch_threshold_us\": 1000,
  \"top_n\": 20,
  \"exclude_idle\": true
})
```

**建议输出模板:**

```text
结论:confirmed | suspected | not_observed
噪点风险:low | medium | high

证据:
1) long_dpc_isr:<status>,关键数值:<dpc/isr over-threshold count>
2) dispatch_level_blocking:<status>,关键模块:<top .sys suspects>
3) io_delivery_pressure:<status>,关键数值:<disk/network weight>
4) decoder_realtime_capacity:<status>,关键数值:<decoder+audiodg weight>

最可能原因(按优先级):
1) <cause #1>
2) <cause #2>
3) <cause #3>

建议动作:
1) 立即动作:<driver update/rollback, power policy>
2) 复现验证:<same scenario recapture>
3) 深挖采集:<CPU + DPC/ISR + CSwitch + ReadyThread + StackWalk>
```

**判读提示:**

- `long_dpc_isr=fail` 基本可判为强噪点证据。
- `glitch_likelihood=medium/high` 且前 2 项存在 `warn/fail` 时,应优先处理驱动与调度干扰。
- 若 `has_stacks=false` 或 `has_readythread=false`,结论需降级为“疑似”,并建议补采集。

---

### Example 4: Feeding folded stacks to the LLM

After `analyze_etl` with `focus="cpu"`, you can ask the LLM to drill deeper:

```jsonc
render_flamegraph({
  \"out_dir\": \"C:\\traces\\cpu_spike_wpa_out\\cpu\",
  \"output_path\": \"C:\\traces\\cpu_spike.folded\",
  \"top_n\": 300,
  \"min_weight_ms\": 2
})
```

Returns:

```json
{
  \"folded_file\": \"C:\\traces\\cpu_spike.folded\",
  \"source_csv\": \"C:\\traces\\cpu_spike_wpa_out\\cpu\\CPU Usage (Sampled)_....csv\",
  \"line_count\": 287,
  \"total_weight_ms\": 39120.0,
  \"preview\": \"ntdll!RtlUserThreadStart;kernel32!BaseThreadInitThunk;myapp!worker_loop;myapp!compress_chunk 27800\\nntdll!... ; myapp!parse_header 410\\n...\"
}
```

You can now either:

- **Render an SVG** flamegraph (requires Perl + Brendan Gregg's script):

  ```bash
  flamegraph.pl C:\traces\cpu_spike.folded > C:\traces\cpu_spike.svg
  ```

- **Or just let the LLM read the `preview`** — the folded format is already *much* easier for an LLM than raw CSV.

---

## Client configuration

### Claude Desktop — `%APPDATA%\\Claude\\claude_desktop_config.json`

```json
{
  \"mcpServers\": {
    \"wpa\": {
      \"command\": \"wpa-mcp\",
      \"env\": {
        \"WPAEXPORTER_PATH\": \"C:/Program Files (x86)/Windows Kits/10/Windows Performance Toolkit/wpaexporter.exe\",
        \"XPERF_PATH\":       \"C:/Program Files (x86)/Windows Kits/10/Windows Performance Toolkit/xperf.exe\"
      }
    }
  }
}
```

### VS Code (GitHub Copilot Chat / MCP) — `.vscode/mcp.json`

Already included in this repo. It points at `server.py` in the workspace.

### Custom MCP host

Any MCP client that speaks stdio works. Launch `wpa-mcp` (or `python server.py`) as a child process and send `tools/list` + `tools/call` over stdio.

---

## Release process

This repo publishes to PyPI via **GitHub Actions + PyPI trusted publishing (OIDC)** — no secrets required.

One-time PyPI setup:

1. Claim the `wpa-mcp` project on PyPI.
2. Add a **Trusted Publisher**:
   - Owner: `Jialong-zhong`
   - Repository: `wpr-xperf-mcp-server`
   - Workflow: `publish.yml`
   - Environment: `pypi`

Then, to ship a new version:

```powershell
# bump version in pyproject.toml, commit, then:
git tag v0.2.0
git push origin v0.2.0
```

The `Publish to PyPI` workflow (on tag `v*`) will build the sdist + wheel and publish automatically.

---

## Troubleshooting

| Symptom | Likely cause | Fix |
|---------|--------------|-----|
| `wpaexporter not found` | WPT not installed or path wrong | Install Windows Performance Toolkit; set `WPAEXPORTER_PATH` |
| `xperf stats failed` | ETL corrupted or not a WPR trace | Re-capture; ensure you ran `wpr -stop <file>` successfully |
| `columns missing` in summarizer | Your WPA version renamed columns | Open the corresponding `.wpaProfile` and adjust `<Column Name=...>` to match your WPA |
| `has_stacks: false` in validation | `-start CPU` not used during capture, or no admin | Re-capture with `-start CPU` as Administrator |
| Empty `dwm_slow_frames` | `DesktopComposition` profile wasn't enabled | Re-capture with `-start DesktopComposition` |
| `ready_latency_top` all near zero during a hang | The thread isn't ready-waiting → it's doing work | Run `render_flamegraph` on CPU exports to see what work |
| New tool added in `server.py` but not visible in client | MCP host cached old tool manifest | Restart MCP host/client session and run a tools/list check |

### MCP tool visibility quick check

After adding a new MCP tool (for example `analyze_audio_glitch_ex4`), verify in this order:

1. Restart the MCP client session (or restart VS Code window / host app).
2. Confirm the server command points to the updated `server.py`.
3. Trigger a `tools/list` from the client and verify the tool name appears.
4. Run one dry call with a known ETL path to confirm runtime import succeeds.

If the tool still does not appear, close all host processes that keep persistent MCP connections, then reconnect.

You can run a local preflight check before reconnecting the client:

```powershell
python scripts\check_mcp_tools.py
```

With ETL dry-run validation:

```powershell
python scripts\check_mcp_tools.py --etl-path C:\DumpFiles\wpr_example.etl
```

Strict gating mode (fail if `validate_trace` emits warnings):

```powershell
python scripts\check_mcp_tools.py --etl-path C:\DumpFiles\wpr_example.etl --fail-on-validation-warning
```

Exit codes:

- `0`: pass
- `2`: required MCP tools missing
- `3`: ETL path not found
- `4`: strict mode enabled and validation warnings found
- `5`: validation interrupted
- `6`: validation failed due to runtime/tooling error

---

## FAQ

**Q: Does this need WPA GUI installed?**
No. Only `wpaexporter.exe` and `xperf.exe` (both from the Windows Performance Toolkit) are called. WPA GUI never launches.

**Q: Can I use this on Linux/macOS?**
The MCP server itself is pure Python. But `wpaexporter` / `xperf` only exist on Windows, so analysis must run on Windows. A common setup is: capture on Windows, copy ETL to a Windows analysis box, run `wpa-mcp` there.

**Q: Why not parse ETL directly in Python?**
ETL parsing is deep. Microsoft already ships an excellent, correct parser (`wpaexporter`) that understands every kernel + provider schema. Reusing it is cheaper and more accurate than reimplementing.

**Q: Can I add my own WPA profile?**
Yes. Drop a `.wpaProfile` into `wpa/profiles/`, add a key to `PROFILE_MAP` in `server.py`, and (optionally) a summarizer in `wpa/summarizer.py`.

**Q: Does the LLM see the full CSV?**
No — by design. The LLM sees compact summary JSON plus (optionally) folded-stack text. Raw CSVs stay on disk and are referenced by path.

---

## License

MIT. See [LICENSE](./LICENSE).