Skip to main content
Glama

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

+------------------+       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:

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

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

From source

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:

# 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

{
  \"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)

{
  \"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

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

analyze_audio_glitch output shape (abbreviated)

{
  \"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)

{
  "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:

// 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:

{
  \"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:

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:

{
  \"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:

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

Expected signal:

{
  \"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 分析是否有音频噪点,并给出最可能原因和修复优先级。”

推荐调用顺序:

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
})

建议输出模板:

结论: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=falsehas_readythread=false,结论需降级为“疑似”,并建议补采集。


Example 4: Feeding folded stacks to the LLM

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

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:

{
  \"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):

    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

{
  \"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:

# 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:

python scripts\check_mcp_tools.py

With ETL dry-run validation:

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

Strict gating mode (fail if validate_trace emits warnings):

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.

A
license - permissive license
-
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Jialong-zhong/wpr-xperf-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server