Skip to main content
Glama
README.md
# naja-scope

<!-- mcp-name: io.github.keplertech/naja-scope -->

[![PyPI version](https://img.shields.io/pypi/v/naja-scope.svg)](https://pypi.org/project/naja-scope/)
[![Python versions](https://img.shields.io/pypi/pyversions/naja-scope.svg)](https://pypi.org/project/naja-scope/)
[![CI](https://github.com/keplertech/naja-scope/actions/workflows/ci.yml/badge.svg)](https://github.com/keplertech/naja-scope/actions/workflows/ci.yml)
[![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE)
[![Glama quality](https://glama.ai/mcp/servers/najaeda/naja-scope/badges/score.svg)](https://glama.ai/mcp/servers/najaeda/naja-scope)

**Let your AI assistant explore SystemVerilog and VHDL designs — without pasting source code into the chat.**

naja-scope is an [MCP](https://modelcontextprotocol.io) server that gives AI
agents (Claude, and any MCP-compatible assistant) a precise, structured view of
your elaborated SystemVerilog or VHDL design. Instead of dumping thousands of lines of
RTL into the model's context, the agent asks targeted questions — *what drives
this signal? what's inside this module? where does this net come from?* — and
gets back small, exact answers with file-and-line references.

Built on the [najaeda](https://github.com/najaeda/naja) netlist engine.

> **On a 17-question [CVA6](https://github.com/openhwgroup/cva6) benchmark,
> the same Claude Code agent scored 17/17 with naja-scope versus 10/17 with
> grep/read source tools — in 77 turns instead of 123, processing about 5×
> less input.** [How it was measured ↓](#does-it-actually-help)

```sh
pip install naja-scope
claude mcp add naja-scope -- naja-scope-mcp
```

![naja-scope demo: a real Claude Code session tracing drivers and a fan-in cone on a UART design, using only naja-scope tools](docs/demo/naja-scope-demo.gif)

---

## Why

Large designs don't fit in a chat window. Pasting RTL is slow, expensive, and
the model still can't reliably trace connectivity across hierarchy. naja-scope
turns your design into something an agent can *navigate*:

- 🔎 **Trace connectivity** — find what drives or loads any signal, across
  module boundaries.
- 🌲 **Walk the hierarchy** — explore modules, instances, and ports on demand.
- 🎯 **Jump to source** — every answer comes with `file:line` ranges, so the
  agent can quote the exact RTL that matters.
- 🧩 **Logic cones** — trace fan-in / fan-out combinational cones up to the
  register boundary.
- 💡 **Recover design intent** — enum state names, struct/union fields, and
  parameter formulas that normally vanish when a design is elaborated.

Requires **najaeda 0.7.33 or newer**, including the CVA6 RV32 elaboration
fix. See the
[0.1.21 release notes](https://github.com/keplertech/naja-scope/blob/main/docs/releases/0.1.21.md)
for test results and snapshot migration requirements.

Works on **RTL and gate-level netlists** alike: load elaborated SystemVerilog
or VHDL ([beta](#vhdl-beta)), or load a post-synthesis structural Verilog netlist together with its Liberty
standard-cell library and navigate the gates the same way (see
[Gate-level designs](#gate-level-designs)).

All responses are token-bounded: lists paginate, large results truncate with
clear markers. Your context stays small; your answers stay accurate.

---

## Does it actually help?

naja-scope helps most when the answer exists in the elaborated design rather
than in any single source file. In an initial 17-question run on the
`cv32a6_imac_sv32` configuration of
[CVA6](https://github.com/openhwgroup/cva6), the same Claude Code agent was
tested with naja-scope and with source-search tools alone.

| Agent setup | Provider and models | Initial automated score | Turns | Input processed | Output tokens |
|---|---|---:|---:|---:|---:|
| **Agent + naja-scope** | Anthropic Claude Code; `claude-sonnet-4-6` with `claude-haiku-4-5-20251001` helper | **17 / 17** | **77** | **1,058,556** | **19,520** |
| Agent + grep/read source | Anthropic Claude Code; `claude-sonnet-4-6` with `claude-haiku-4-5-20251001` helper | 10 / 17 | 123 | 5,461,719 | 55,962 |

The difference is clearest on structural questions that source search cannot
answer directly:

| CVA6 question | Agent + naja-scope | Agent + grep/read source |
|---|---|---|
| Flattened register groups under `ex_stage_i` | **92**, in 4 turns | No answer at the turn limit |
| Flattened register groups under `commit_stage_i` | **0**, in 3 turns | No answer at the turn limit |
| Elaborated `hpdcache_mux` variants | **20**, in 3 turns | No answer at the turn limit |

Source search remains the right tool for local textual questions. naja-scope
adds the elaborated hierarchy, connectivity, lowered primitives, and generated
or uniquified structures that are otherwise difficult to reconstruct.

See the [benchmark methodology and multi-model runner](benchmarks/README.md)
and [historical result record](benchmarks/historical-cva6-20260628.json) for
configuration, scoring, token accounting, and reproducibility details.

---

## Install

```sh
pip install naja-scope        # pulls najaeda and the MCP runtime from PyPI
naja-scope-mcp                # stdio MCP server
```

---

## Connect it to Claude Code

```sh
claude mcp add naja-scope -- naja-scope-mcp
```

Or add it to any MCP client's config:

```json
{
  "mcpServers": {
    "naja-scope": {
      "command": "naja-scope-mcp"
    }
  }
}
```

Then just ask your assistant to load a design and start exploring:

> *"Load my UART design from `rtl/uart.sv` with top `uart_top`, then show me
> everything that drives `tx_o`."*

The agent loads the design once and answers follow-up questions instantly — no
re-reading source, no giant pastes.

---

## Connect it to ChatGPT

ChatGPT connects to MCP servers over an **HTTP endpoint** (custom connectors /
Developer mode), so run naja-scope as an HTTP server instead of stdio:

```sh
naja-scope-mcp --transport streamable-http --host 127.0.0.1 --port 8000
```

This serves MCP at `http://<host>:8000/mcp`. Because ChatGPT reaches the server
over the network, expose that URL where ChatGPT can see it — e.g. a public
tunnel for a local run:

```sh
# example: a tunnel to your local server (ngrok, cloudflared, …)
ngrok http 8000        # -> https://<something>.ngrok.app  →  add /mcp
```

Then in ChatGPT, open **Settings → Connectors** (enable Developer mode if
needed), **add a custom connector**, and paste the server URL
(`https://<your-host>/mcp`). Once connected, ask it to load a design and explore
exactly as above. (ChatGPT's connector UI evolves; the constant is: it needs an
HTTPS MCP URL, which `--transport streamable-http` provides.)

> ⚠️ The HTTP server has no built-in auth — only expose it over a trusted tunnel,
> and prefer short-lived tunnels for local experiments.

---

## Gate-level designs

Already synthesized? Load the structural Verilog netlist together with the
Liberty library that defines its standard cells, and navigate the gates the same
way as RTL:

> *"Load the Liberty library `pdk/stdcells.lib`, then the gate netlist
> `build/top.v`, and tell me what cells `top` is built from and what drives
> `data_out`."*

Hierarchy, per-cell counts (`get_module_card`), drivers/loads, and logic cones
all work on the netlist; cones stop at the sequential cells. `get_source`
shows a cell's or net's line in the netlist. For Yosys output, load with
`yosys_source_attributes=True`: the `(* src = ... *)` attributes become
`origins` that `get_source(path, origin=0)` reads back in the RTL. A runnable
example lives in [`examples/`](examples/) (`stdcells.lib` + `counter2.v` +
`gate_level.py`).

---

## Storage summaries

`get_module_card` reports direct-child register bits and inferred RAM/ROM
summaries: width, depth, capacity in bits, read/write port counts, and
initialization status. Native memory interfaces also report `address_bits`
and `reset_mode` (`none`, `async_low`, `async_high`, `sync_low`, or
`sync_high`). It lists up to 12 memories, with `memories_truncated`
when more exist; initialized memories include a 64-character `init_preview`
and `init_truncated`. Address zero occupies the least-significant word.

`get_stats` aggregates these counts below the selected instance using
`flat_register_bits`, `flat_memory_instances`, `flat_rom_instances`,
`flat_ram_instances`, and `flat_memory_bits`. Register bits count modeled
flip-flop/latch states, including multibit Liberty banks; RAM capacity is
separate. Sequential cells without a state model are counted in
`register_bits_unknown_instances` (`flat_` in statistics), so known bit
counts remain explicit lower bounds when that count is nonzero. Memories
are recognized from native naja primitives; opaque memory macros are not
assigned an inferred capacity.

## SystemVerilog time units

For mixed RTL where only some modules declare a time unit, pass
`default_timescale="1ns/1ps"` to `load_systemverilog`. It supplies a fallback
for modules without a time unit and preserves explicit declarations. The
setting survives snapshots and `load_intent` re-elaboration.

## VHDL (beta)

VHDL loading is available in beta. Call
`load_vhdl(file=["pkg.vhd", "child.vhd", "top.vhd"], top="my_entity")` to
explore its elaborated hierarchy and connectivity; the files may be listed in
any order. A single path per call also works (load packages first);
package-only files may return `top: null` until the top file is loaded. The frontend supports a restricted two-state RTL subset,
and supported constructs may change. `get_source` reads VHDL entity, port,
signal, instance and process origins, including after snapshot reloads.

Designs that name their library, as in `library neorv32;`, need
`library="neorv32"` on the load.

Pass `intent=True` to `load_vhdl`, or call `load_intent` afterwards, to query
`get_intent` for enum literals and their encodings, including FSM states held
in record fields, record fields, generics, and symbolic ranges such as
`WIDTH-1 downto 0`. Package members use `pkg::NAME`, and an instance's parameter
list leaves them out. Query the signal rather than its inferred flop, and note
that record fields report widths, not bit positions. Validated on the
[NEORV32](https://github.com/stnolting/neorv32) SoC.

---

## What you can ask

Once a design is loaded, your assistant can:

- **Resolve** any signal or instance by hierarchical path (with glob and
  did-you-mean suggestions).
- **Find** objects design-wide by pattern.
- **Show the hierarchy** of any module.
- **Get drivers / loads** of a net — the real endpoints, across hierarchy;
  literal drivers preserve four-state `0` / `1` / `X` / `Z` values.
- **Trace logic cones** (fan-in / fan-out) and see the register frontier.
- **Get source** — the exact source lines behind any object.
- **Get a module card** — ports, counts, clock/reset at a glance.
- **Recover design intent** — state-machine names, struct fields, parameter
  expressions lost during elaboration.

A runnable end-to-end walkthrough lives in [`examples/`](examples/), including
versions that run against [CVA6](https://github.com/openhwgroup/cva6) (a
production RISC-V core, cloned on demand — see
[`examples/cva6_demo.sh`](examples/cva6_demo.sh)) and
[CORE-V-MCU](https://github.com/openhwgroup/core-v-mcu) (a full multi-vendor
RISC-V SoC — see [`examples/core_v_mcu_demo.sh`](examples/core_v_mcu_demo.sh)).

---

## The Python escape hatch (off by default)

naja-scope also has a `query_python` tool that runs Python directly against the
loaded design, for queries the typed tools above cannot express. **It is not
registered unless you opt in:**

```bash
NAJA_SCOPE_ENABLE_PYTHON=1 naja-scope-mcp
```

It is unsandboxed `eval`/`exec` inside the server process — read-only by
convention, not enforced — so anything that can reach the server can run
arbitrary Python as the server's user. That matters most under `--transport
streamable-http`, where the server listens on a socket. Leave it off unless you
need it and trust every client that can reach the endpoint.

---

## Requirements

- Python 3.10+
- Works anywhere `najaeda` runs (Linux, macOS, Windows)

---

## Development

```sh
# from a checkout
python3 -m venv .venv
.venv/bin/pip install -e .
.venv/bin/python -m pytest -q
```

The full test suite runs against a plain `pip install` of `najaeda` — no native
build required. The CVA6 cross-hierarchy cone regression
(`tests/test_zzz_cone_cva6.py`) is slow and skips automatically unless a CVA6
snapshot is present.

CI tests every supported Python version on Linux x86_64, plus native platform
lanes for Linux x86_64/aarch64, macOS x86_64/arm64, and Windows x86_64. The
macOS x86_64 lane builds `najaeda` from its source distribution because PyPI
does not currently provide an Intel macOS wheel.

---

## Support & contact

- 🐛 **Found a bug or have a feature request?**
  [Open an issue on GitHub →](https://github.com/keplertech/naja-scope/issues)
- 📫 **Get in touch:** [contact@keplertech.io](mailto:contact@keplertech.io)

---

## License

Apache-2.0. See [LICENSE](LICENSE).
</content>
</invoke>

## Optional browser schematic

Requires **naja-schematic 0.2.0 or later** (the release containing the
embeddable viewer API):

```bash
pip install "naja-scope[schematic]"
```

Restart the MCP server after installation. Load a design as usual, then use:

- `open_schematic(path="top.u_cpu")`: returns a local browser URL and focuses
  the instance. Omit `path` for the top. Anonymous `#id` instances are supported.
- `annotate_schematic(items=[{"path": "top.u_cpu", "message": "Check reset",
  "severity": "warning"}])`: replaces the overlay. Use `kind="term"` with a
  pin/port path to annotate that terminal; annotations apply to the whole port,
  not individual bus bits. Pass `items=[]` to clear. At most 200 annotations,
  each with at most 2000 message characters.
- `get_schematic_selection()`: returns the last clicked instance as a regular
  naja-scope path, usable with source, hierarchy and module-card queries.

The viewer shares the existing raw naja universe; it does not elaborate again.
The server binds to loopback and returns a token-bearing URL. Open that URL on
**the machine running the MCP server**; it is not a remote or embedded MCP UI.
Treat the URL as access to the loaded design. Design loads/resets refresh open
viewers and clear selection, focus and annotations. The browser server stops
with naja-scope. Existing tools remain usable without the schematic extra.

TDQS

A4.2/5.0

Scored across 23 tools

Disambiguation5/5

Each tool has a sharply distinct purpose, and descriptions explicitly cross-reference siblings (get_drivers vs get_loads vs trace_cone; get_hierarchy vs find vs get_stats vs get_module_card; get_intent vs get_source). Even the potentially overlapping load_intent is disambiguated from the intent=True load flag by explaining it is only for loads that did not retain intent.

Naming Consistency4/5

Strong verb_noun convention throughout (get_*, load_*, save_snapshot, reset_universe, open_schematic, annotate_schematic, trace_cone). A few bare verbs/nouns (find, resolve, status) deviate mildly, but they are unambiguous and readable.

Tool Count4/5

23 tools is on the heavy side, but this is a genuinely broad EDA surface (five loaders, snapshot lifecycle, connectivity queries, schematic viewer). Each tool earns its place, though the load_* family is the one cluster that could be tightened.

Completeness4/5

Covers the full lifecycle: multiple frontends (SV/VHDL/Verilog/Liberty/primitives), session status/reset, snapshot save/load, intent layer, hierarchy/search/resolve, connectivity, stats, source, and schematic interaction. Minor gaps only, such as no export/write-out or per-design unload beyond a full reset, but these are outside the stated read-only analysis scope.

Maintenance

ActivityActive
ResponsivenessNo issues