waveform-mcp
by schalkiii
README.md
# Waveform MCP — AI-Friendly VCD/FSDB Waveform Debugger
A Model Context Protocol (MCP) server that gives AI agents the ability to read, analyze, and debug VCD/FSDB waveform files from digital circuit simulations.
Parse a waveform once, then query signals, find AXI transactions, render ASCII waveforms, and validate protocol compliance — all through structured JSON-RPC tools.
## Features
* **Stateful session**: Parse once, query many times. Large files cached in memory.
* **VCD + FSDB support**: Native VCD parsing; FSDB via Verdi's `fsdb2vcd` auto-conversion.
* **32 structured tools**: Signal search, snapshot, edge detection, transaction analysis, AXI write/read channel correlation, burst aggregation, bus-level protocol checks, clock analysis, signal activity stats, dual-waveform diff, value diff between times, ASCII rendering, multi-session comparison, signal aliases, and more.
* **Smart signal search**: Ranked relevance matching (exact > base name > suffix > component > substring), with support for bit-select suffixes like `wdata[31:0]`.
* **AXI transaction analysis**: Automatically extract valid-ready transactions from all 5 AXI channels (AW/W/B/AR/R) with captured data values. One-call `analyze_axi_channel` auto-detects signals, aggregates W beats into bursts by `wlast`, and reports throughput/gap/duration statistics. `analyze_axi_write`/`analyze_axi_read` correlate address+data+response channels into complete transfers with latency breakdown.
* **Bus-level protocol checks**: Detect AXI protocol violations including data instability while VALID=1/READY=0, BVALID before WLAST, and zero-width valid pulses.
* **Clock & activity analysis**: Auto-detect clock signals and report period, frequency, duty cycle, jitter, and gating. Find dead signals (never toggle) and rank signals by change activity.
* **Dual-waveform diff**: Compare two loaded waveforms signal-by-signal, report first mismatch time and value for each differing signal. Matches signals by base name across hierarchies.
* **Multi-session support**: Load multiple waveforms simultaneously (e.g. golden vs actual) under different session IDs, switch between them, or pass `session_id` to any tool for cross-waveform comparison.
* **Signal aliases**: Define short aliases for long hierarchical paths (e.g. `tb.dut.u_memory.wdata[31:0]` → `wdata`), usable anywhere a signal name is expected.
* **Flexible time inputs**: All time parameters accept integers (picoseconds) or human-readable strings like `"100ns"`, `"1.5us"`, `"500ps"`.
* **ASCII waveform rendering**: View timing diagrams directly in the conversation without a waveform viewer. Linear-scan rendering with collision-free time scale and right-boundary labeling.
* **Protocol compliance checking**: Detect valid-ready handshake violations.
* **Pickle cache**: Large files reload in seconds instead of minutes.
* **CLI fallback**: Human-friendly command-line interface for manual debugging.
## Architecture
```
src/waveform_mcp/core.py (core library, pure stdlib, no MCP dependency)
├── src/waveform_mcp/server.py (MCP Server — 32 tools over stdio JSON-RPC)
└── src/waveform_mcp/cli.py (CLI — backward-compatible, human-friendly)
```
The core library (`core.py`) contains all parsing and analysis logic. It can be used standalone in Python scripts, or exposed via MCP or CLI. The package uses a standard `src/` layout and is installable via `pip install -e .`.
## Installation
### Requirements
* Python >= 3.10
* `mcp >= 2.0` (for MCP Server; CLI and core library don't need it)
* Verdi with `fsdb2vcd` in PATH (only for FSDB files; VCD files don't need it)
### Install (package mode, optional)
```
pip install -e ".[mcp]" # installs core + MCP SDK
pip install -e . # core only (zero dependencies, for CLI/scripts)
```
### Install MCP dependency (manual mode)
```
pip install "mcp[cli]"
```
### Clone / copy the project
```
# All files are self-contained in this directory
cd D:\workspace\waveform_mcp
```
No build step required. The project is pure Python.
## MCP Server Deployment
### Claude Desktop
Edit `claude_desktop_config.json` (location varies by OS):
**Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
**macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
**Linux:** `~/.config/Claude/claude_desktop_config.json`
```
{
  "mcpServers": {
  "waveform-debugger": {
  "command": "python",
  "args": \["D:\\\workspace\\\waveform\_mcp\\\waveform\_mcp.py"]
  }
  }
}
```
Restart Claude Desktop. The server will appear in the MCP status indicator with 17 tools.
### Cursor
Create or edit `.cursor/mcp.json` in your project root:
```
{
  "mcpServers": {
  "waveform-debugger": {
  "command": "python",
  "args": \["D:\\\workspace\\\waveform\_mcp\\\waveform\_mcp.py"]
  }
  }
}
```
Restart Cursor. Enable the server in Settings > MCP.
### Other MCP-compatible clients
Any MCP 2.x-compatible client (Cline, Continue, custom agents) can connect via stdio:
```
{
  "command": "python",
  "args": \["D:\\\workspace\\\waveform\_mcp\\\waveform\_mcp.py"]
}
```
### Verify the server is working
```
\# List registered tools (should show 17)
python -c "import asyncio, sys; sys.path.insert(0,'.'); from waveform\_mcp import server; print(asyncio.run(server.list\_tools()).\_\_len\_\_())"
\# Run full MCP communication test
python test\_mcp\_server.py
```
## Tool Reference
### Session Management
| Tool | Description |
| ------------------------------------- | ----------------------------------------------------------------- |
| `load_waveform(path, use_cache=true, session_id?)` | Load a VCD/FSDB file and parse into memory. Must be called first. `session_id` for multi-waveform comparison. |
| `get_waveform_info(session_id?)` | Get signal count, module count, max time, timescale, module list. |
| `unload_waveform()` | Unload all sessions and free memory. |
| `list_sessions()` | List all loaded sessions and the active session ID. |
| `switch_session(session_id)` | Switch the active session. |
| `close_session(session_id?)` | Close a specific session and free its memory. |
### Signal Aliases
| Tool | Description |
| ------------------------------------- | ----------------------------------------------------------------- |
| `set_alias(alias, signal_name, session_id?)` | Define a short alias for a long hierarchical signal path. Usable anywhere a signal name is expected. |
| `list_aliases(session_id?)` | List all user-defined aliases. |
| `remove_alias(alias, session_id?)` | Remove a user-defined alias. |
### Signal Browsing & Search
| Tool | Description |
| --------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| `list_modules()` | List all module hierarchy paths. |
| `list_signals(module?, pattern?)` | List signals, optionally filtered by module or name pattern. |
| `search_signals(pattern, match_mode="auto", exclude_constants=false)` | Search signals by name. `auto` mode ranks by relevance. Supports `[31:0]` suffixes. |
**Match modes:**
* `auto` — ranked relevance (recommended)
* `exact` — exact full path match
* `suffix` — base name ends with pattern (supports bit-select suffixes)
* `contains` — case-insensitive substring
* `regex` — regular expression
### Time Queries
| Tool | Description |
| --------------------------------------------------------------- | ------------------------------------------------------------------------- |
| `snapshot(time, signals, session_id?)` | Get signal values at a specific time (bin/hex/dec). `time` accepts int (ps) or "100ns"/"1.5us". |
| `watch_signals(signals, trigger?, time_window?, max_events=10, trigger_on="first", session_id?)` | Track values over time. `trigger_on="any"` reports when ANY signal changes; each event includes `triggered_by`. |
| `next_edge(signal, from_time, edge="rising", session_id?)` | Find next rising/falling edge after a time. |
| `prev_edge(signal, from_time, edge="rising", session_id?)` | Find previous rising/falling edge before a time. |
| `find_edges(signal, edge="both", max_n=100, session_id?)` | Find all edges of a signal. |
### Transaction Analysis
| Tool | Description |
| ------------------------------------------------------------------------ | -------------------------------------------------------------------- |
| `find_transactions(valid_signal, data_signals, ready_signal?, max_n=10, session_id?)` | Find valid-ready transactions and capture data values at each start. Registered-ready handshake time is correct (end = ready assertion, not valid fall). |
| `analyze_axi_channel(channel_prefix, max_n=100, session_id?)` | One-call AXI channel analysis: auto-detect signals, extract transactions, aggregate W beats into bursts by `wlast`, report throughput/gap/duration stats. Channel: aw/w/b/ar/r. |
| `get_transaction_stats(valid_signal, data_signals, ready_signal?, max_n=100, bytes_field?, session_id?)` | Extract transactions and compute cross-transaction stats: count, duration min/avg/max, gap min/avg/max, total bytes, throughput Mbps. |
| `compare_transaction_fields(transactions, expected_field, actual_field)` | Compare two fields across all transactions, find mismatches. |
| `analyze_axi_write(max_n=100, session_id?)` | Correlate AW+W+B channels into complete write transfers with latency breakdown (AW->W, W-burst, WLAST->B, total). Time-proximity matching handles unequal channel counts. |
| `analyze_axi_read(max_n=100, session_id?)` | Correlate AR+R channels into complete read transfers with latency breakdown (AR->R-first, R-burst, total). |
### Protocol, Clock & Activity Analysis
| Tool | Description |
| -------------------------------------------------------- | ---------------------------------------------------- |
| `check_axi_protocol(channel="write", session_id?)` | AXI bus-level protocol checks: data stability while VALID=1/READY=0, BVALID before WLAST, zero-width valid pulses. |
| `analyze_clock(clock_signal?, session_id?)` | Auto-detect clock and report period, frequency, duty cycle, jitter, and gating periods. |
| `signal_activity_stats(module?, top_n=20, session_id?)` | Rank signals by change count, find dead signals (never toggle), report average changes per signal. |
### Value Diff & Comparison
| Tool | Description |
| -------------------------------------------------------- | ---------------------------------------------------- |
| `value_diff_between_times(t1, t2, signals?, session_id?)` | Find all signals whose value changed between two time points. Useful for "what changed after reset?". |
| `compare_waveforms(session_a, session_b, signals?, time_window?)` | Compare two loaded waveforms signal-by-signal, report first mismatch time and value for each differing signal. |
### Visualization
| Tool | Description |
| ---------------------------------------------------------------- | -------------------------------------------- |
| `render_ascii_waveform(signals, time_start, time_end, width=80, session_id?)` | Render ASCII timing diagram with collision-free time scale and right-boundary label. Linear-scan for performance. |
### Validation & Checking
| Tool | Description |
| -------------------------------------------------------- | ---------------------------------------------------- |
| `check_signal_value(signal, expected_value, at_time, session_id?)` | Check if signal equals expected value (bin/hex/dec). |
| `find_signal_mismatch(signal_a, signal_b, time_window?, session_id?)` | Find times when two signals differ. |
| `check_valid_ready_protocol(valid_signal, ready_signal, session_id?)` | Detect valid-ready handshake violations. |
## Example Workflow (Agent Debugging AXI2MEM)
Here's how an agent would use the tools to debug a waveform:
```
1\. load\_waveform(path="/path/to/axi2mem\_tb.vcd")
  → 671 signals, 66 modules, 475us max time
2\. search\_signals(pattern="awvalid", exclude\_constants=true)
  → \["axi2mem\_tb.awvalid", ...]
3\. find\_transactions(
  valid\_signal="awvalid",
  data\_signals=\["awaddr", "awlen", "awsize", "awburst"],
  ready\_signal="awready",
  max\_n=5
  )
  → 5 write address transactions with addresses and lengths
4\. find\_transactions(
  valid\_signal="wvalid",
  data\_signals=\["wdata", "wstrb", "wlast"],
  ready\_signal="wready",
  max\_n=5
  )
  → 5 write data transactions:
  \[0] 115ns wdata=0xDEADBEEF wstrb=0xF wlast=1
  \[1] 385ns wdata=0xA0000000 wstrb=0xF wlast=0
  ...
5\. render\_ascii\_waveform(
  signals=\["clk", "awvalid", "awready", "wvalid", "wready"],
  time\_start=90000, time\_end=110000, width=80
  )
  → ASCII timing diagram showing handshake timing
6\. check\_valid\_ready\_protocol(valid\_signal="wvalid", ready\_signal="wready")
  → 0 violations (clean protocol)
```
## CLI Usage
The CLI (`src/waveform_mcp/cli.py`) provides a human-friendly interface for manual debugging. It is backward-compatible with the original script.
### List signals
```
python debug\_waveform.py --vcd waveform.vcd --list-signals
python debug\_waveform.py --vcd waveform.vcd --list-signals --pattern valid
```
### Snapshot at a time
```
python debug\_waveform.py --vcd waveform.vcd --watch result mode --time 95000
python debug\_waveform.py --vcd waveform.vcd --watch result mode --time 95000 --json
```
### Time-series with trigger
```
python debug\_waveform.py --vcd waveform.vcd --watch result mode --trigger valid -n 20
```
### Time window trace
```
python debug\_waveform.py --vcd waveform.vcd --watch valid result --time 95000-97500
```
### Transaction analysis (new)
```
python debug\_waveform.py --vcd waveform.vcd \\
  \--transactions valid\_out \\
  \--data result mode overflow \\
  \--ready ready\_in \\
  -n 5
```
### ASCII waveform (new)
```
python debug\_waveform.py --vcd waveform.vcd \\
  \--ascii clk valid\_out ready\_in result \\
  \--time 90000-110000 \\
  \--width 100
```
### FSDB support
```
python debug\_waveform.py --vcd waveform.fsdb --watch result --time 95000
```
Requires `fsdb2vcd` from Verdi in PATH.
### Cache (new)
```
python debug\_waveform.py --vcd huge.vcd --cache --watch result --time 95000
```
First load parses and saves cache; subsequent loads are \~5x faster.
## Python API Usage
Use the core library directly in scripts:
```
from waveform\_core import WaveformSession
\# Load waveform
session = WaveformSession("/path/to/waveform.vcd", use\_cache=True)
\# Search signals
valid\_signals = session.search\_signals("valid", exclude\_constants=True)
\# Snapshot
snap = session.snapshot(95000, \["result", "mode"])
print(snap\["signals"]\["result"]\["hex"])
\# Find AXI transactions
txs = session.find\_transactions(
  "awvalid", \["awaddr", "awlen"],
  ready\_signal="awready", max\_n=10
)
for tx in txs:
  print(f" addr={tx\['data']\['awaddr']\['hex']} len={tx\['data']\['awlen']\['hex']}")
\# ASCII waveform
wave = session.render\_ascii(\["clk", "valid", "ready"], 90000, 110000, width=80)
print(wave)
\# Protocol check
violations = session.check\_valid\_ready\_protocol("valid", "ready")
session.unload()
```
## FSDB Support
FSDB (Fast Signal Database) is Synopsys Verdi's proprietary format. Since it's not open, this project uses Verdi's `fsdb2vcd` command-line tool for conversion:
1. When you load a `.fsdb` file, the server automatically calls `fsdb2vcd` to convert it to a temporary VCD.
2. The converted VCD is then parsed normally.
3. The temporary file is cleaned up on unload.
**Requirements:**
* Verdi installed with `fsdb2vcd` in PATH, or
* Set `VERDI_HOME` environment variable, or
* Pass `fsdb2vcd_path` explicitly in Python API
**Limitations:**
* Conversion adds overhead (proportional to file size).
* FSDB-specific features (like signal hierarchy browsing in Verdi) are not available.
* For very large FSDB files, consider converting to VCD manually first.
## Testing
### Core library tests (synthetic VCD)
```
python test\_waveform\_core.py
```
12 tests covering: load/parse, search, snapshot, transactions, edges, ASCII rendering, protocol check, cache, watch signals, mismatch detection, module listing.
### MCP communication tests
```
python test\_mcp\_server.py
```
8 tests covering the full stdio JSON-RPC lifecycle: initialize, tools/list, load\_waveform, search, snapshot, transactions, ASCII rendering, unload.
### End-to-end agent workflow test
```
python test\_e2e\_mcp.py # auto-detects real VCD, falls back to synthetic
python test\_e2e\_mcp.py /path/to/your/waveform.vcd
```
15-step simulation of a full agent debugging session over real stdio JSON-RPC: initialize → tools/list → load → search (7 patterns) → list\_modules → snapshot → find\_transactions (all AXI channels) → render\_ascii → find\_edges/next\_edge → protocol\_check → watch\_signals (trigger) → check\_signal\_value → unload. Uses real AXI2MEM VCD when available.
### Real VCD tests
```
python test\_real\_vcd.py /path/to/your/waveform.vcd
```
10 tests against a real waveform: load performance, module/signal inventory, search patterns, snapshot at multiple times, edge detection, watch with trigger, AXI transaction analysis (5 channels), ASCII rendering, protocol check, cache performance.
### Generate a test VCD
```
python generate\_test\_vcd.py
```
Creates `test_waveform.vcd` with a simple valid-ready pipeline for quick testing.
## File Structure
```
waveform\_mcp/
├── AGENTS.md # Agent rules file (project context, run commands, conventions)
├── README.md # This file
├── WAVEFORM\_MCP\_DESIGN.md # Detailed design document (architecture, API, roadmap)
├── waveform\_core.py # Core library: parsing, querying, analysis (no MCP dep)
├── waveform\_mcp.py # MCP Server: 17 tools over stdio JSON-RPC
├── debug\_waveform.py # CLI: human-friendly command-line interface
├── generate\_test\_vcd.py # Test VCD generator
├── test\_waveform\_core.py # Core library test suite (12 tests)
├── test\_mcp\_server.py # MCP communication test suite (8 tests)
├── test\_e2e\_mcp.py # End-to-end agent workflow test (15 steps, real VCD support)
├── test\_real\_vcd.py # Real VCD test suite (10 tests)
└── test\_waveform.vcd # Generated test VCD (small, synthetic)
```
## Performance
Measured on a 22MB VCD (671 signals, 475us simulation):
| Operation | Time |
| --------------------------- | --------- |
| Initial parse | 2.0s |
| Cache load (2nd time) | 0.75s |
| Snapshot query | 0.1-0.6ms |
| Transaction analysis (5 tx) | 0.2ms |
| ASCII render (80 chars) | 0.3ms |
| Protocol check | 0.3ms |
For files >100MB, use `--cache` or `use_cache=True` to avoid re-parsing.
## Troubleshooting
### "No module named 'mcp'"
Install the MCP SDK:
```
pip install "mcp\[cli]"
```
### "fsdb2vcd not found"
* Ensure Verdi is installed and `fsdb2vcd` is in PATH.
* Or set the `VERDI_HOME` environment variable.
* Or convert FSDB to VCD manually: `fsdb2vcd -i input.fsdb -o output.vcd`
### MCP server not showing up in client
* Check the path in the config is correct (use absolute paths).
* Check Python is in PATH (use full path to python.exe if needed).
* Run `python src/waveform_mcp/server.py` directly — it should start and wait for input (no errors).
* Check client logs for connection errors.
### Signal search returns too many results
* Use `exclude_constants=true` to filter out parameters and constants.
* Use more specific patterns (e.g., `awvalid` instead of `valid`).
* Use `match_mode="suffix"` for exact base-name suffix matching.
### ASCII waveform looks wrong
* Ensure the time window is correct (in picoseconds).
* For multi-bit signals, values are shown as hex labels at change points.
* Increase `width` for more detail.
## Design Document
See [docs/DESIGN.md](docs/DESIGN.md) for the full design document, including:
* Detailed architecture and design principles
* Complete API specification
* Tool-by-tool parameter reference
* FSDB support design (two approaches)
* Deployment configuration for all clients
* Extension roadmap (v1.1, v1.2, v2.0)
* Verification checklist
## License
Internal use.This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues