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

An [MCP](https://modelcontextprotocol.io) server that keeps **one OpenSTA process alive** for the whole
conversation and exposes static timing and power analysis as nine tools. An agent (Claude Code, or any
MCP client) loads a design once, then asks questions; the server translates each question into
OpenSTA Tcl, runs it in the live session, and returns the result with the exact command it ran.

```
Claude Code  ──JSON-RPC over stdio──▶  opensta-mcp (Python)  ──Tcl over stdin/stdout──▶  sta
             ◀──────────────────────                          ◀────────────────────────
```

The left pipe carries request ids; the right pipe is plain text and carries none. Everything
in `session.py` exists because of that asymmetry.

## Tools

| tool | what it does |
|---|---|
| `load_design(liberty, verilog, top, sdc="", spef="")` | reads the design (`liberty` may be a list); returns per-step timings and a digest of warnings |
| `get_summary()` | worst setup/hold slack, TNS/WNS, min period and fmax per clock, DRV violation counts, cell/net counts |
| `report_timing(path_delay, path_count, scope, from_pin, to_pin, fields, format)` | `report_checks` with the common options typed; `scope="reg2reg"` etc. |
| `report_power(top_instances=0, format="text")` | power by group and component, optionally the N highest-power instances; always states whether switching activity came from a VCD/SAIF or from OpenSTA defaults |
| `run_tcl(command)` | any other OpenSTA command, sandboxed and journaled; shows printed output and the Tcl return value; `run_tcl("help report_checks")` shows options |
| `find_commands(pattern="*")` | names of the OpenSTA commands matching a glob; for when the agent does not know a command's name |
| `get_status()` | what sta is doing now; works even while another tool is waiting |
| `get_conditions(state=0)` | the files, constraint changes, activity source and scenes a result was computed under; every result ends with `[conditions #n]`, and `state=n` reads that state back from the journal |
| `restart_session()` | the only way the sta process is restarted; after sta dies every other tool is refused until this is called |

Every analysis result (the report tools, `get_summary`, `load_design`, and `run_tcl` when it runs a
`report_*` command) ends with one line naming its conditions, e.g.
`[conditions #2] liberty: sky130_fd_sc_hd__tt_025C_1v80 | sdc: 6_final.sdc +1 change by run_tcl | spef: 6_final.spef`.
The number advances on `load_design` and on every state-changing `run_tcl`, so two results with
the same number were computed under the same conditions. The count is by the text sent, so a
command OpenSTA accepts but ignores, or a `read_sdc` of an empty file, is counted too. `load_design`
on a running sta reads the SDC on top of the earlier state; the line then says so and earlier
run_tcl changes are listed apart. Call `restart_session` first for a clean state.

Anything not covered by a dedicated tool goes through `run_tcl`. Commands that recur in the
journal are candidates for promotion to a dedicated tool.

## Install

```bash
git clone https://github.com/InsungHeo/opensta-mcp && cd opensta-mcp
python3 -m venv .venv && source .venv/bin/activate
pip install -e .            # adds the `opensta-mcp` and `opensta-mcp-probe` commands
which sta                   # OpenSTA must be on PATH, or set STA_BIN
```

Register with Claude Code (adjust paths):

```bash
claude mcp add opensta \
  --env STA_BIN=/path/to/OpenROAD/bin/sta \
  -- /path/to/opensta-mcp/.venv/bin/opensta-mcp
```

or copy `examples/claude_mcp.json` into your project's `.mcp.json`.

**Claude Code on Windows, OpenSTA in WSL.** A Windows `claude` cannot execute a Linux path, and
`--env` sets a Windows-side variable that does not cross into WSL. Launch the server through
`wsl.exe` and set the variable inside it:

```bash
claude mcp add --scope user opensta -- \
  wsl.exe -d Ubuntu-24.04 --exec env STA_BIN=/home/<you>/OpenROAD-flow-scripts/tools/install/OpenROAD/bin/sta \
  /home/<you>/opensta-mcp/.venv/bin/opensta-mcp
claude mcp list        # should show opensta as connected
```

`--exec` matters: without it the command goes through a shell first, which expands `$VAR` and
`$(...)` before your own settings apply.

Two Windows shell traps seen in practice:

- **PowerShell 5.1 drops `--`** when calling a native program, so `claude` reads `-d` as its own
  option (`unknown option '-d'`). Run the command from Git Bash or `cmd` instead.
- **Git Bash rewrites `/home/...` into a Windows path.** Prefix the command with
  `MSYS_NO_PATHCONV=1`.

## Configuration (environment variables)

| variable | default | meaning |
|---|---|---|
| `STA_BIN` | `sta` | OpenSTA binary |
| `STA_ARGS` | `-no_splash -no_init` | extra arguments |
| `OPENSTA_MCP_HOME` | `~/.opensta-mcp` | where sessions and ownership files live |
| `OPENSTA_MCP_STDERR` | `file` | `file`: sta's stderr goes to `sta.stderr.log`; `merge`: into stdout |
| `OPENSTA_MCP_STALL_S` | `60` | seconds of silence before the server checks whether sta is idle |
| `OPENSTA_MCP_MAX_OUTPUT` | `12000` | characters returned per tool call before truncation (about 150 report lines) |
| `OPENSTA_MCP_LOG` | `INFO` | server log level (stderr + `server.log`) |

## What a session leaves behind

```
~/.opensta-mcp/sessions/20260926_153012_4242/
    journal.jsonl     every command: tool, arguments, Tcl sent, ok/error, seconds, warning count
    replay.tcl        only the state-changing commands; `sta replay.tcl` reproduces the session by hand
    warnings.log      every warning line, grouped by command (OpenSTA prints warnings on stdout)
    sta.stderr.log    the sta process's stderr, for crash traces
    server.log        the server's log
```

`tail -f journal.jsonl` in another terminal shows what the agent is doing in real time.

## Design decisions

1. **One live process, not one per query.** Liberty/SPEF loading dominates; constraints set
   earlier must still apply later.
2. **Scripts travel as base64 data.** The stdin line is always a complete Tcl command
   (`__mcp_run <id> <base64>`), so an unbalanced brace becomes a caught error instead of an
   interpreter waiting for more input.
3. **`catch` decides success.** The Tcl side reports `__MCP_ERR__ <id>` before the error text;
   the server never guesses from output patterns.
4. **A per-request end marker.** `__MCP_DONE__ <id>` with a fresh id per command, matched as a
   whole line, so late output from an abandoned command is recognised and dropped.
5. **One lock in front of the pipe.** The MCP side may send requests concurrently; the sta side
   cannot tell them apart.
6. **No automatic restart.** Death, computing and stalled are distinguished (`/proc/<pid>/stat`)
   and reported with a diagnosis. After sta dies every command is refused until `restart_session`,
   so nothing runs silently on a fresh sta that lacks the design and constraint changes.
7. **Dies with its parent.** `PR_SET_PDEATHSIG`, spawned from the reader thread so the signal
   is tied to the process lifetime. Ownership files let a new server reap orphans left by a
   crashed one, and only those.
8. **Sandbox, inside the interpreter.** `exec exit socket source cd load` are deleted from the Tcl
   interpreter (the session stops sta by closing its stdin); `open` is wrapped to allow
   reading only (OpenSTA's own `read_sdc` and `include` read files through Tcl `open`, so removing
   it breaks them; the first real-OpenSTA run found this); `file` keeps read-only subcommands;
   `rename` and `interp` are deleted last, so nothing can be brought back, not even from a file
   read with `include`; `auto_noexec` stops Tcl from running a program for an unknown word.
   OpenSTA's own file writers (at any command position) and `> file` redirection are allowed only
   under the session directory (only the output argument is checked; library and model inputs may
   live anywhere). The original `open` and `file` remain reachable inside the interpreter under
   internal names, so a script written to find them can still write files: this guards against
   accidents and careless prompts, not against a determined user. The sta process runs with the
   user's permissions.
9. **Every argument is quoted.** Bus bits like `reg_next_pc[31]` would otherwise be read as a
   command substitution; OpenSTA itself is deprecating unquoted bus names (ChangeLog 2026-09-24).
10. **stdout is for JSON-RPC only.** Server logs go to stderr and a file.
11. **Errors carry a fix hint when the cause is Tcl, not OpenSTA.** An unquoted
    `reg_next_pc[31]$_SDFFE_PN0P_/D` fails as `invalid command name "31"`; the error says to use braces.

## Checking the tool before trusting the design

```bash
opensta-mcp-probe --liberty LIB.lib --verilog design.v --top TOP --sdc c.sdc --spef d.spef --json probe.json
```

prints whether this OpenSTA supports `-format json`, whether output is buffered when piped,
which stream warnings use, and how long each load step takes. These answers decide the output
format and the stderr handling for your version.

## Tests

```bash
pip install -e ".[dev]"
pytest                                  # uses tests/fake_sta.py, no OpenSTA needed
OPENSTA_MCP_INTEGRATION=1 STA_BIN=sta pytest   # also runs the real-sta round trip
```

## Status and limits

Built for a talk on agent-driven timing analysis (September 2026).

- **Tested**: OpenSTA 3.1.0 (the `sta` built by OpenROAD-flow-scripts) on Linux / WSL2, Python 3.12, MCP Python SDK 1.x.
- **Not a security boundary against a determined user**: the sandbox stops accidents and careless
  prompts; the sta process itself runs as you and can read what you can read.
- **MCP SDK**: pinned to `mcp<2`. SDK 2.x renamed `FastMCP` to `MCPServer`; migration is planned.
- **OpenSTA versions**: 3.x. Older releases call `-group_path_count` `-group_count`, so `report_timing` fails there (use `run_tcl`).
- **macOS**: should work but is untested. Stall detection reads `/proc`; without it the server
  never declares a stall and reports the process state as unknown.
- **Windows**: run the server in WSL (see Install).
- **OpenSTA bug worked around**: `report_power -highest_power_instances N` fails in OpenSTA 3.1.0
  (and on master as of 2026-09-24) because `power/Power.tcl` calls a proc `highest_power_instances`
  that does not exist. `report_power(top_instances=N)` checks for that proc and, when it is missing,
  calls the underlying `sta::report_power_highest_insts` directly.

## License

MIT

TDQS

A4.2/5.0

Scored across 9 tools

Disambiguation4/5

Most tools have clearly distinct purposes: load_design loads, report_timing/report_power produce specific reports, restart_session resets, etc. However, run_tcl is a general escape hatch that overlaps functionally with report_timing and report_power, and get_status vs get_conditions both report session state, creating some potential for misselection. Descriptions mitigate this well.

Naming Consistency5/5

All tool names follow a consistent snake_case verb_noun pattern (get_summary, load_design, report_timing, run_tcl, find_commands, get_status, get_conditions, restart_session). There are no deviations in casing or style.

Tool Count5/5

Nine tools is a well-scoped set for an STA server. Each tool covers a distinct operation (load, report, inspect, reset, discover) and there is no redundant tool that could be removed without losing functionality.

Completeness5/5

The surface covers the full lifecycle: loading a design, querying conditions/status, producing timing and power reports, discovering commands, running arbitrary Tcl, and restarting. Missing specialized reports (e.g., area, clocks) can be accessed via run_tcl, so there are no dead ends.

Maintenance

ActivityMaintained
ResponsivenessNo issues