This document is the authoritative reference for the MCP tools exposed by the TypeScript server.
It is generated from the source of truth: typescript/src/server.ts,
typescript/src/config/command_whitelist.ts,
typescript/src/core/models.ts, and the golden fixtures under
typescript/__tests__/golden/fixtures/.
The golden fixtures are the machine-readable wire contract. Regenerate them with:
make golden- Wire Format Conventions
- Error Conventions
- Session Tools
- Report Image Tools
- ORFS Metrics Tools
- Flow Run Tools
- Session Lifecycle Notes
All tool responses carry a single MCP content item of type "text" whose text field is a JSON
string. All JSON keys are snake_case — the server converts camelCase models at the boundary
before serialising. isError is never set on any tool response; errors are signalled inside the
JSON payload itself.
Every result type has a nullable error field:
"error": null // success
"error": "<string>" // failure — human-readable or error-code string
All tools return a JSON object. On failure the error field is non-null and the rest of the
payload is either zero-valued or null. Example (blocked command):
{
"output": "",
"session_id": "sess-0001",
"timestamp": "2026-01-01T00:00:00",
"execution_time": 0,
"command_count": 0,
"buffer_size": 0,
"error": "CommandBlocked: 'exit'",
"message": "Command blocked: 'exit' is not on the OpenROAD allowlist.\nFull command: 'exit'"
}The message field is present only on blocked-command responses; it contains the human-readable
explanation alongside the machine-readable error code.
list_report_images and read_report_image use structured error codes in the error field:
| Code | Meaning |
|---|---|
ValidationError |
Bad platform, design, path traversal attempt, or empty segment |
RunNotFound |
The run directory does not exist |
InvalidImageName |
image_name does not end in .webp |
ImageNotFound |
The named file is not present in the run directory |
NotAFile |
The path exists but is not a regular file |
FileTooLarge |
Image exceeds the 50 MB on-disk limit |
UnexpectedError |
Any other failure |
Execute a read-only OpenROAD command. The whitelist is default-deny: only READONLY_PATTERNS
(report_*, get_*, check_*, sta, help, version, and a set of safe Tcl built-ins) are
accepted. Everything else is rejected with CommandBlocked. See SECURITY.md for
full whitelist details.
| Parameter | Type | Required | Default |
|---|---|---|---|
command |
string | yes | — |
session_id |
string | no | (auto-generates new session) |
timeout_ms |
integer | no | 30000 (OPENROAD_COMMAND_TIMEOUT) |
Annotations:
readOnlyHint: truedestructiveHint: falseidempotentHint: falseopenWorldHint: false
Response shape (interactive_exec_result_success.json):
{
"output": "OpenROAD v2.0",
"session_id": "sess-0001",
"timestamp": "2026-01-01T00:00:00",
"execution_time": 1.5,
"command_count": 3,
"buffer_size": 131072,
"truncated": false,
"bytes_discarded": 0,
"total_bytes": 13,
"error": null
}| Field | Meaning |
|---|---|
output |
Everything OpenROAD wrote for this command, ANSI-stripped |
session_id |
The session used (auto-created if none was passed) |
execution_time |
Seconds spent on this command |
command_count |
Commands executed in this session so far |
buffer_size |
The session buffer's capacity in characters |
truncated |
true when output exceeded the buffer and its head was discarded |
bytes_discarded |
Characters dropped from the start of the output |
total_bytes |
Raw characters the command produced, before ANSI cleaning |
Truncation warning. Output larger than the session buffer loses its beginning, not its end. When
truncatedistruethe result is a partial answer: the retained text may start mid-line, and any error or warning printed before the cut is not inoutputand is not reflected inerror. A truncated result also carries a[TRUNCATED: ...]banner at the head ofoutput. Narrow the command and re-run rather than analysing what came back.
Session accumulation warning. Omitting
session_idcreates a session and leaves it running. Capture the returnedsession_idand reuse it, or you will accumulate sessions untilOPENROAD_MAX_SESSIONS(default 50) is reached and new commands fail.
Execute a state-modifying OpenROAD command. The whitelist is default-allow: only
BLOCKED_COMMANDS (quit, socket, load, glob, fconfigure, chan, vwait, rename,
after, subst) are rejected. Read-only commands such as report_wns are also accepted here —
use interactive_openroad_query when you want to keep state changes visible and auditable.
| Parameter | Type | Required | Default |
|---|---|---|---|
command |
string | yes | — |
session_id |
string | no | (auto-generates new session) |
timeout_ms |
integer | no | 30000 (OPENROAD_COMMAND_TIMEOUT) |
Annotations:
readOnlyHint: falsedestructiveHint: trueidempotentHint: falseopenWorldHint: false
The response shape is identical to interactive_openroad_query. Long-running flow commands
routinely exceed 30 s — pass a larger timeout_ms for placement, CTS, and routing.
A session is headless, but Qt renders offscreen, so save_image works — the GUI-only commands
(gui::show_worst_path, gui::set_display_controls) just need a GUI event loop to exist first.
gui::show <script> false provides one: it boots the GUI, runs the script, and returns. This is
how ORFS writes its own report images (flow/scripts/final_report.tcl).
gui::show {gui::set_display_controls "Timing Path/*" visible true
gui::show_worst_path
save_image -width 1920 /tmp/worst_path.png} false-width is independent of die size; ORFS instead computes -resolution from the block height,
which is why its images come out ~1000 px regardless of design.
Set QT_QPA_PLATFORM=offscreen in the session's env when no display is present (ORFS does this
automatically in variables.mk). A bare gui::show, or one left interactive, is rejected —
it blocks the Tcl event loop waiting on a human, leaving the session alive but permanently
unresponsive. The error message carries the working form.
Before rendering anything, check whether the run already has the image: ORFS emits
final_worst_path.webp, final_congestion.webp and others into reports/, and
read_report_image returns them directly with no session at all.
Create a session explicitly so you control its identifier, command, environment, and working
directory. Use this before a long flow to capture the session_id without running a first command.
| Parameter | Type | Required | Default |
|---|---|---|---|
session_id |
string | no | random 8-char hex ID |
command |
string[] | no | ["openroad", "-no_init"] |
env |
object | no | inherits server env |
cwd |
string | no | inherits server cwd |
Annotations:
readOnlyHint: falsedestructiveHint: falseidempotentHint: falseopenWorldHint: false
Response shape (interactive_session_info_success.json):
{
"session_id": "sess-0001",
"created_at": "2026-01-01T00:00:00",
"is_alive": true,
"command_count": 5,
"buffer_size": 4096,
"uptime_seconds": 12.5,
"state": "active",
"error": null
}state is one of "creating", "active", "terminated", or "error".
Terminate a session. Sends SIGTERM; if force is true sends SIGKILL immediately.
| Parameter | Type | Required | Default |
|---|---|---|---|
session_id |
string | yes | — |
force |
boolean | no | false |
Annotations:
readOnlyHint: falsedestructiveHint: trueidempotentHint: trueopenWorldHint: false
Response shape (session_termination.json):
{
"session_id": "sess-0001",
"terminated": true,
"was_alive": true,
"force": false,
"error": null
}List all sessions known to the server, both active and recently terminated.
No parameters.
Annotations:
readOnlyHint: truedestructiveHint: falseidempotentHint: trueopenWorldHint: false
Response shape (interactive_session_list.json):
{
"sessions": [
{
"session_id": "sess-0001",
"created_at": "2026-01-01T00:00:00",
"is_alive": true,
"command_count": 5,
"buffer_size": 4096,
"uptime_seconds": 12.5,
"state": "active",
"error": null
},
{
"session_id": "sess-0002",
"created_at": "2026-01-01T00:00:00",
"is_alive": false,
"command_count": 0,
"buffer_size": 0,
"uptime_seconds": null,
"state": null,
"error": "Session failed to start"
}
],
"total_count": 2,
"active_count": 1,
"error": null
}Return detailed metrics for a single session: buffer utilisation, memory, CPU time, idle seconds, and history length.
| Parameter | Type | Required | Default |
|---|---|---|---|
session_id |
string | yes | — |
Annotations:
readOnlyHint: truedestructiveHint: falseidempotentHint: trueopenWorldHint: false
Response shape (session_inspection.json):
{
"session_id": "sess-0001",
"metrics": {
"session_id": "sess-0001",
"state": "active",
"is_alive": true,
"created_at": "2026-01-01T00:00:00",
"last_activity": "2026-01-01T00:00:00",
"uptime_seconds": 12.5,
"idle_seconds": 3.25,
"commands": {
"total_executed": 4,
"current_count": 4,
"history_length": 4
},
"performance": {
"total_cpu_time": 1.5,
"peak_memory_mb": 128,
"current_memory_mb": 96
},
"buffer": {
"current_size": 2048,
"max_size": 131072,
"utilization_percent": 1.5625
},
"timeout": {
"configured_seconds": 300,
"is_timed_out": false
}
},
"error": null
}Retrieve the command history for a session, with optional limit and search filter.
| Parameter | Type | Required | Default |
|---|---|---|---|
session_id |
string | yes | — |
limit |
integer | no | returns all (up to 1000) |
search |
string | no | no filter |
Annotations:
readOnlyHint: truedestructiveHint: falseidempotentHint: trueopenWorldHint: false
Response shape (session_history.json):
{
"session_id": "sess-0001",
"history": [
{
"command": "place_design",
"timestamp": "2026-01-01T00:00:00",
"command_number": 1,
"execution_start": 1767225600,
"execution_time": 0.75,
"output_length": 42
}
],
"total_commands": 1,
"limit": 10,
"search": "place",
"error": null
}Return aggregate metrics across all sessions: total/active/terminated counts, combined CPU time, and per-session summaries.
No parameters.
Annotations:
readOnlyHint: truedestructiveHint: falseidempotentHint: trueopenWorldHint: false
Response shape (session_metrics.json):
{
"metrics": {
"manager": {
"total_sessions": 2,
"active_sessions": 1,
"terminated_sessions": 1,
"max_sessions": 10,
"utilization_percent": 10
},
"aggregate": {
"total_commands": 4,
"total_cpu_time": 1.5,
"total_memory_mb": 96,
"avg_memory_per_session": 96
},
"sessions": [ "..." ]
},
"error": null
}Search the output of commands already run in a session, without re-running them or re-sending a large result.
| Parameter | Type | Required | Default |
|---|---|---|---|
session_id |
string | yes | — |
pattern |
string | yes | — |
max_matches |
integer | no | 200 |
context_lines |
integer | no | 0 |
ignore_case |
boolean | no | true |
command_number |
integer | no | — (all retained commands) |
{
"session_id": "a1b2c3d4",
"pattern": "wns",
"matches": [
{ "command_number": 1, "command": "report_checks", "line_number": 42, "line": "wns max -0.485" }
],
"total_matches": 1,
"truncated": false,
"pattern_kind": "regex",
"searched_commands": 1,
"searched_lines": 900,
"retained_chars": 131166,
"evicted_commands": 0,
"message": null,
"error": null
}| Field | Meaning |
|---|---|
total_matches |
Matches found, which exceeds matches.length when capped by max_matches |
truncated |
true when matches were dropped — the list is not the whole answer |
pattern_kind |
How the pattern was applied; see below |
searched_commands / searched_lines |
How much was actually searched |
retained_chars |
Characters of output currently held for this session |
evicted_commands |
Commands whose output has aged out of the retention budget |
Pattern handling. pattern is a regular expression. pattern_kind reports what actually
happened:
regex— compiled and applied as a regex.substring— the pattern did not compile, so it was applied literally. Pasting a raw name is not treated as a user error.substring-fallback— the pattern compiled and matched nothing, and it contains regex metacharacters, so it was retried literally and that found matches. A pasted instance name likedpath.a_reg[0]is valid regex meaning "…a_regfollowed by0", which silently matches nothing — worse than a syntax error, because you would be told there are no matches when it is the pattern that is wrong.
Only recent output is searchable.
runCommanddrains the session's circular buffer, so the live buffer holds nothing once a command has returned. Output is therefore retained separately, bounded byOPENROAD_OUTPUT_HISTORY_CHARS(default 256 KB) andOPENROAD_OUTPUT_HISTORY_COMMANDS(default 50), oldest evicted first.evicted_commandstells you when something has aged out. A single result larger than the whole budget keeps its tail.
List .webp report images produced by an ORFS run, grouped by pipeline stage. Requires
ORFS_FLOW_PATH to point at your ORFS flow/ directory.
| Parameter | Type | Required | Default |
|---|---|---|---|
platform |
string | yes | — |
design |
string | yes | — |
run_slug |
string | yes | — |
stage |
string | no | "all" |
platform and design are validated against subdirectories discovered under
{ORFS_FLOW_PATH}/platforms/ and {ORFS_FLOW_PATH}/designs/{platform}/. run_slug may not
contain path separators, .., or glob characters.
Annotations:
readOnlyHint: truedestructiveHint: falseidempotentHint: trueopenWorldHint: false
Response shape (list_images.json):
{
"run_path": "/reports/nangate45/gcd/base",
"total_images": 2,
"images_by_stage": {
"floorplan": [
{
"filename": "floorplan.webp",
"path": "/reports/nangate45/gcd/base/floorplan.webp",
"size_bytes": 15000,
"modified_time": "2026-01-01T00:00:00",
"type": "unknown"
}
]
},
"message": null,
"error": null
}type is "unknown" for filenames that are not in the known mapping (see
SECURITY.md for the full list).
When no images are found: run_path, total_images, and images_by_stage are null and
message is "No images found".
Read a single report image and return it as a real MCP image content block a vision model can see, plus a text block carrying the metadata. The base64 payload is not repeated in the text block.
Images are downscaled only as far as their byte budget actually requires: the longest edge is
capped at OPENROAD_IMAGE_MAX_DIMENSION (default 1568 px, the resolution vision models downsample
to anyway), then a size/quality ladder steps down only while the encoding still exceeds
OPENROAD_IMAGE_MAX_BASE64_KB (default 1024 KB of base64), never below
OPENROAD_IMAGE_MIN_DIMENSION (default 512 px). Most report images now pass through untouched.
The 50 MB on-disk limit is enforced before any resizing.
Quality is shed before resolution. Every quality rung (85, 70, 55) is tried at the current
size before the image is made smaller, and the first encoding that fits the budget is returned.
A congestion or routing map carries its signal in fine wire and via detail, which downsampling
destroys outright, while WebP quality loss degrades it gracefully — so a full-resolution q70
image is returned in preference to a downscaled q85 one. width/height in the response are
the dimensions actually returned; compare them against original_width/original_height to see
whether resizing was reached at all.
| Parameter | Type | Required | Default |
|---|---|---|---|
platform |
string | yes | — |
design |
string | yes | — |
run_slug |
string | yes | — |
image_name |
string | yes | — |
max_size_kb |
integer | no | OPENROAD_IMAGE_MAX_BASE64_KB |
max_size_kb overrides the base64 budget for a single call — lower it for a quick thumbnail,
raise it when a heatmap needs more detail.
image_name must end in .webp and may not contain path separators or traversal sequences.
Annotations:
readOnlyHint: truedestructiveHint: falseidempotentHint: trueopenWorldHint: false
Response shape. The tool returns two content blocks:
[ { "type": "image", "data": "<base64>", "mimeType": "image/webp" },
{ "type": "text", "text": "<the JSON below, without image_data>" } ]
The underlying JSON payload (read_image.json, as returned by the library API which still
includes image_data):
{
"image_data": "<base64>",
"metadata": {
"filename": "cts_clk.webp",
"format": "webp",
"size_bytes": 15000,
"width": 1024,
"height": 768,
"modified_time": "2026-01-01T00:00:00",
"stage": "cts",
"type": "unknown",
"compression_applied": true,
"original_size_bytes": 48000,
"original_width": 2048,
"original_height": 1536,
"compression_ratio": 3.2
},
"message": null,
"error": null
}compression_ratio is original_size_bytes / size_bytes. compression_applied is true only
when the image actually had to be re-encoded; format is sniffed from the bytes rather than
guessed from the filename, so the doubled .webp.png form some ORFS builds emit is reported
correctly.
Error response (read_image_error.json):
{
"image_data": null,
"metadata": null,
"message": "Image not found: foo.webp",
"error": "ImageNotFound"
}Read a design's per-stage metrics, evaluate its rules-base.json gate thresholds against them, and
surface the tagged errors and warnings from each stage log — in one call. This replaces the
find + cat + jq + grep sequence over the flow tree that accounted for roughly 39% of all
shell usage in the capability study.
| Parameter | Type | Required | Default |
|---|---|---|---|
design |
string | yes | — |
stage |
string | no | all |
platform |
string | no | inferred from design |
variant |
string | no | base |
platform is inferred when the design name is unique across platforms; if it is not, the error
names the candidates. design and variant are validated as single path segments.
Stage resolution. stage is matched, in order, against:
- an exact file stem —
4_1_cts - an ORFS metric namespace —
placeopt,detailedplace,globalplace,globalroute,detailedroute,finish. These matter becauserules-base.jsonkeys metrics by namespace (globalroute__timing__setup__ws) while the file on disk is5_1_grt.json. - an everyday stage group —
synth,floorplan,place,cts,route,finish, which expand to every step in that group (place→ all five3_*files) - a plain substring of the stem
all(the default) — every stage
Stages are discovered from the logs directory rather than a hardcoded table, so a renumbered ORFS
release still resolves. An unmatched stage returns StageNotFound listing the stems present.
Repeated metrics are arrays. ORFS appends a block of metrics per sub-run, so a stage file legitimately contains the same key more than once — the real
4_1_cts.jsonhas 63 key occurrences across 55 distinct keys, recordingcts__utilization__before__dplas both76.7787and82.1146. A plainJSON.parsekeeps only the last and reports nothing. This tool returns every value, in file order, and names those keys inrepeated_metricsso you can tell an array from a scalar without inspecting each value. Gates on such a metric are judged on the last value (what ORFS's own checkers see) and marked"ambiguous": true.
Response shape:
{
"platform": "nangate45",
"design": "gcd",
"variant": "base",
"stage": "cts",
"logs_path": "logs/nangate45/gcd/base",
"available_stages": ["1_synth", "2_1_floorplan", "...", "6_report"],
"stages": [
{
"stage": "4_1_cts",
"metrics_path": "logs/nangate45/gcd/base/4_1_cts.json",
"metrics": {
"cts__utilization__before__dpl": [76.7787, 82.1146],
"cts__timing__setup__ws": -0.113089,
"cts__design__violations": 0
},
"repeated_metrics": ["cts__utilization__before__dpl"],
"log": {
"path": "logs/nangate45/gcd/base/4_1_cts.log",
"errors": ["[ERROR ORD-2018] Pin is not ITerm or BTerm or modITerm."],
"warnings": [],
"error_count": 1,
"warning_count": 0,
"truncated": false
},
"error": null
}
],
"gates": [
{
"metric": "cts__timing__setup__ws",
"stage": "4_1_cts",
"value": -0.113089,
"threshold": -0.0529,
"compare": ">=",
"level": "error",
"status": "fail"
}
],
"unmatched_gates": [
{ "metric": "finish__timing__setup__ws", "threshold": -0.0559, "compare": ">=", "level": "error" }
],
"gate_summary": {
"total": 1, "pass": 0, "fail": 1, "unknown": 0,
"failing_errors": 1, "failing_warnings": 0, "unmatched": 1
},
"rules_path": "designs/nangate45/gcd/rules-base.json",
"message": null,
"error": null
}| Field | Meaning |
|---|---|
stages[].metrics |
The stage's metrics; a value is an array iff its key is in repeated_metrics |
stages[].metrics_path |
null when the stage produced a log but no metrics file (a crashed stage) |
stages[].error |
MalformedMetrics: ... when the JSON is half-written; the stage is still returned |
stages[].log.truncated |
true when more diagnostics existed than the 50-per-category listed |
gates[].status |
pass, fail, or unknown when the operator cannot apply to the value |
gates[].level |
From the rule; a rule with no level defaults to error, as ORFS does |
unmatched_gates |
Rules whose metric was not in the stages read — use stage: "all" to cover them |
All paths are returned relative to the ORFS flow root, never as absolute host paths.
Error codes: ValidationError (bad design/variant/platform, or path traversal), RunNotFound
(no such variant; the message lists those present), StageNotFound (the message lists the stems
present).
Run an ORFS stage through the server instead of shelling out to make. Output streams to a file —
never through the session's 128 KB circular buffer, which would keep only the tail of a route log.
What "inspectable" means here. A
make-spawned OpenROAD is a separate process this server does not own, so its Tcl interpreter cannot be queried mid-run.get_orfs_jobgives structured job progress — stage, iteration, live violation count, CPU and memory — which is what a caller would otherwise hand-roll fromtail,grep,psandstat. It is not live access to the running interpreter.
| Parameter | Type | Required | Default |
|---|---|---|---|
design |
string | yes | — |
stage |
string | yes | — |
overrides |
object | no | {} |
platform |
string | no | inferred from design |
variant |
string | no | base |
wait_seconds |
integer | no | — (return immediately) |
jobs |
integer | no | — (make default) |
timeout_seconds |
integer | no | OPENROAD_FLOW_RUN_TIMEOUT (6 h) |
dry_run |
boolean | no | false |
Returns a job_id immediately so a multi-hour route does not block the call. Pass wait_seconds
to get the finished result inline when the stage is short. dry_run: true runs make -n, showing
which stages the target would chain without running any of them.
overrides become make command-line assignments, which take precedence over both the
environment and the Makefile:
{ "design": "gcd", "stage": "cts", "overrides": { "CTS_CLUSTER_SIZE": "20" } }Override names are returned verbatim, not converted to snake_case.
Allowed stage values: synth, floorplan, place, cts, grt, route, finish, all,
metadata, and clean_synth / clean_floorplan / clean_place / clean_cts / clean_route /
clean_finish / clean_metadata / clean_all. Anything else is rejected — see
SECURITY.md for why the allowlist exists and which override names are refused.
Annotations: readOnlyHint: false, destructiveHint: true — it writes into the flow tree, and
the clean_* targets delete results.
| Parameter | Type | Required | Default |
|---|---|---|---|
job_id |
string | no | — (list every run) |
recent_lines |
integer | no | 50 |
{
"job_id": "a3f2c1d0",
"status": "running",
"design": "ibex",
"stage": "route",
"command": "make route DESIGN_CONFIG=./designs/sky130hd/ibex/config.mk FLOW_VARIANT=base",
"elapsed_seconds": 1840,
"progress": {
"current_stage": "5_2_route",
"iteration": 1,
"percent": 80,
"violations": 19604,
"iteration_violations": 20,
"cpu_seconds": 1802,
"memory_mb": 4210.5,
"peak_memory_mb": 4300.0
},
"recent_lines": [" Completing 80% with 19604 violations."],
"log_path": "/tmp/openroad-mcp-runs/a3f2c1d0.log",
"log_bytes": 48210934,
"log_truncated": true,
"stages": [],
"gates": [],
"error": null
}status is running, succeeded, failed, cancelled or timed_out, with exit_code always
present once finished. On success the result also carries stages, gates and gate_summary
in the same shape read_orfs_metrics returns — including repeated metrics as
arrays.
current_stage comes from the newest *.tmp.log in the run's logs directory: ORFS writes each
stage to <stem>.tmp.log and renames it to <stem>.log on success, so this tracks the flow without
depending on stage numbering. cpu_seconds and memory_mb come from OpenROAD's own DRT-0267
line, not from sampling the make pid — the process consuming the machine is a grandchild.
log_truncated reports that the log holds more than the lines returned; log_bytes is the true
total.
| Parameter | Type | Required |
|---|---|---|
job_id |
string | yes |
Signals the run's entire process group, then escalates to SIGKILL after a grace period.
Killing only make would strand the openroad it spawned. Running jobs are also torn down on
server shutdown.
Error codes across these tools: ValidationError (bad design/variant/stage/override),
FlowJobLimit (concurrency cap reached), FlowJobNotFound, FlowPathNotFound.
| Limit | Default | Environment Variable |
|---|---|---|
| Max concurrent sessions | 50 | OPENROAD_MAX_SESSIONS |
| Output buffer per session | 128 KiB | OPENROAD_DEFAULT_BUFFER_SIZE |
| Command history per session | 1000 entries | (constant) |
| Default command timeout | 30 s | OPENROAD_COMMAND_TIMEOUT |
| Input queue depth | 128 commands | OPENROAD_SESSION_QUEUE_SIZE |
| Report-image payload | 1024 KB base64 | OPENROAD_IMAGE_MAX_BASE64_KB |
| Report-image longest edge | 1568 px | OPENROAD_IMAGE_MAX_DIMENSION |
| Report-image resize floor | 512 px | OPENROAD_IMAGE_MIN_DIMENSION |
| Searchable output per session | 256 KB | OPENROAD_OUTPUT_HISTORY_CHARS |
| Searchable commands per session | 50 | OPENROAD_OUTPUT_HISTORY_COMMANDS |
| Concurrent flow runs | 2 | OPENROAD_MAX_FLOW_JOBS |
| Flow run timeout | 6 h | OPENROAD_FLOW_RUN_TIMEOUT |
| Flow run log directory | <tmpdir>/openroad-mcp-runs |
OPENROAD_RUN_LOG_DIR |
Idle sessions are not automatically reaped. The cleanupIdleSessions function exists in the
manager but no scheduler calls it in production. Sessions persist until manually terminated, the
process exits, or the underlying OpenROAD process dies. Plan your session lifecycle explicitly or
you will hit OPENROAD_MAX_SESSIONS.
OPENROAD_SESSION_IDLE_TIMEOUT (default 300 s) is read by inspect_interactive_session and
reported in the timeout block; it does not trigger any automatic cleanup.