Guides
Drive simulators from your coding agent
rystic mcp is a local MCP server. Through it your coding agent starts a simulator, loads a scenario, seeds records, moves the clock, reads state and the calls and webhooks the simulator saw, diffs state against where it started, rolls back to a snapshot, saves the run as a test case and grades a later run against it, without writing a curl line.
Requires a current rystic CLI. If rystic help mcp reports an unknown command, run rystic self-update. New to Rystic? Install the CLI and sign in first.
The server speaks MCP over stdio: the client launches rystic mcp as a subprocess. Each tool is a thin wrapper over a CLI command or a control-plane route, so it answers what that command or route answers on your plan. A simulator it starts keeps running after the client exits; stop_twin or rystic stop ends it. The tool names say “twin”, meaning a simulator, and take the product names rystic list prints, such as stripe-twin.
Install
In Cursor or VS Code, one click adds the server: Add to Cursor, Add to VS Code or Add to VS Code Insiders. The editor opens and asks before it adds the entry. Like --print, the entry’s command is plain rystic, so the CLI must be on the editor’s PATH and signed in. When it is not on the PATH, use the command below.
One command writes the server entry into your client’s config:
rystic mcp install --client claude-code
# created /p/.mcp.json (claude-code, project scope)
# next: restart Claude Code in this directory and approve the project's rystic server when it asks
The entry’s command is the absolute path of the rystic binary you ran, so the client finds it even when rystic is not on its PATH. An existing config file is merged: other servers and keys stay. A file that is not valid JSON, comments and trailing commas included, is refused and left unmodified. A new user-scope file is created readable only by you (mode 0600).
rystic mcp install --client <claude-code|cursor|codex|vscode|windsurf> [--scope project|user] [--name rystic] [--command <path>] [--force] [--dry-run] [--print] [--json]
[--twin <simulator>] [--instance <name>] [--read-only] [--tools a,b,c]
| Flag | Effect |
|---|---|
--client | claude-code, cursor, codex, vscode or windsurf |
--scope project|user | where the entry goes; default project for Claude Code, Cursor, Codex and VS Code, user for Windsurf. The paths are in the table under Configure by hand |
--name | the server’s name in the config, default rystic; another name adds a second entry beside an existing one |
--command <path> | the binary the client launches, instead of the absolute path of the running rystic |
--force | replace a different entry already under that name |
--dry-run | show the file it would write; write nothing |
--print | print the client’s copy-paste block, the same as the blocks under Configure by hand; write nothing. Its command is plain rystic, so rystic must be on the client’s PATH. Takes no other flag but --client, --json and the scope flags |
--json | the result as JSON |
--twin, --instance, --read-only, --tools | the scope flags, checked as rystic mcp checks them and written into the entry’s args, --twin as the simulator’s full name: "args": ["mcp", "--twin", "stripe-twin", "--read-only"] |
Running it again with nothing to change, or with a different entry already there:
unchanged /p/.mcp.json (claude-code, project scope): "rystic" is already installed
rystic: /p/.mcp.json: already has a different "rystic" server; pass --force to replace it, or --name to add another (not modified)
It exits 2 on a bad flag, 9 on a conflicting entry without --force, and 1 when the existing file cannot be read or is not valid.
The last line of a write names the next step:
| Client and scope | Next step |
|---|---|
| Claude Code, project | restart Claude Code in this directory and approve the project’s rystic server when it asks |
| Claude Code, user | restart Claude Code; /mcp lists the server |
| Cursor | restart Cursor, or turn the server on under Settings > MCP |
| Codex, project | restart Codex in this directory; Codex reads .codex/config.toml only in a trusted project |
| Codex, user | restart Codex |
| VS Code | reload VS Code, then start the server from the MCP: List Servers command |
| Windsurf | restart Windsurf, or refresh the MCP servers in Cascade |
VS Code’s user scope is not written by rystic. code --add-mcp takes one entry with a name, not the whole file: the rystic entry from rystic mcp install --client vscode --print with "name": "rystic" added.
code --add-mcp '{"name":"rystic","type":"stdio","command":"rystic","args":["mcp"]}'
Or paste that entry, without name, under "servers" in the file MCP: Open User Configuration opens.
Configure by hand
Each block is a complete config file holding the rystic entry. Merge the entry into a file you already have.
| Client | Project scope | User scope |
|---|---|---|
| Claude Code | .mcp.json | ~/.claude.json |
| Cursor | .cursor/mcp.json | ~/.cursor/mcp.json |
| Codex | .codex/config.toml (trusted projects only) | ~/.codex/config.toml |
| VS Code | .vscode/mcp.json | code --add-mcp, or the file MCP: Open User Configuration opens (Install has the line) |
| Windsurf | none | ~/.config/devin/mcp_config.json; $XDG_CONFIG_HOME/devin/mcp_config.json when that is set, %AppData%\devin\mcp_config.json on Windows |
Claude Code
{
"mcpServers": {
"rystic": {
"type": "stdio",
"command": "rystic",
"args": [
"mcp"
]
}
}
}
Cursor
{
"mcpServers": {
"rystic": {
"type": "stdio",
"command": "rystic",
"args": [
"mcp"
]
}
}
}
Codex
[mcp_servers.rystic]
command = "rystic"
args = ["mcp"]
VS Code
{
"servers": {
"rystic": {
"type": "stdio",
"command": "rystic",
"args": [
"mcp"
]
}
}
}
Windsurf
{
"mcpServers": {
"rystic": {
"command": "rystic",
"args": [
"mcp"
]
}
}
}
If the client cannot find rystic, set command to its absolute path (command -v rystic prints it).
Scope the server
By default rystic mcp serves every tool on every simulator running on this machine. Four flags narrow it:
rystic mcp [--twin <simulator>] [--instance <name>] [--read-only] [--tools a,b,c]
| Flag | Effect |
|---|---|
--twin <simulator> | serves one simulator, named as rystic run takes it. Its tools take no instance or twin argument, and start_twin takes no name: each acts on that simulator’s one running instance, and list_twins lists only its instances. start_twin starts the instance, and refuses with name_conflict while one runs. A call naming another simulator or instance fails with out_of_scope |
--instance <name> | only with --twin: the instance to act on when several run, and the name start_twin gives the one it starts |
--read-only | serves only the read-only tools: list_twins, list_scenarios, inspect_state, list_calls, list_sent and diff_state |
--tools a,b,c | serves only the named tools. With --read-only, only read-only tools may be named |
A bad scope flag exits 2 and names the valid choices. A scoped server’s instructions and its guide describe only what it serves, and a prompt that needs a tool it hides is hidden too. rystic mcp install takes the same flags and writes them into the client’s entry.
Call order
At startup the server hands the client instructions with the order of calls:
list_twinsfor the running instances, orstart_twin, which returns the URL.load_scenario, orseed, to set the starting state.- Your own code acts against that URL. The simulator answers as the real API does; you do not call it through MCP.
inspect_state,list_calls,list_sentanddiff_stateto read what happened.save_test_caseto keep the run, orcheck_test_caseto grade it against a saved case.
start_twin, load_scenario, reset and restore_twin set the baseline diff_state compares against; set_clock and advance_clock control time; snapshot_twin and restore_twin let an agent try one path, roll back and try another. The guide resource walks each step.
Tools
Every tool except list_twins and start_twin takes instance: the instance’s name, its simulator’s name when that simulator runs one instance, or <twin>/<name>. save_test_case and check_test_case take instances, one per simulator, in its place for a case across simulators. list_twins names them.
Each tool declares MCP annotations. Read-only tools change no state. Destructive tools replace or drop state; idempotent ones are safe to repeat with the same arguments. Every tool is closed-world except start_twin, which may download a simulator release and checks your license.
| Tool | Arguments | What it does | Annotations |
|---|---|---|---|
list_twins | none | the instances running on this machine: name, simulator, version, URL, runner and status from a live check | read-only |
start_twin | twin (required), scenario, name, port, version, runner (native or docker), project, clock (RFC3339) | starts a simulator in the background, as rystic run <twin> -d does, and returns its instance name and URL. A simulator that is not installed is pulled first, which can take a minute. A call that sends a progressToken gets progress notifications while it downloads, and canceling the call stops the download, or stops the simulator if it has already started. Takes a free port unless port is given | open-world |
stop_twin | instance | stops the instance and removes its record and diff baseline, as rystic stop does. Its snapshots stay | destructive, idempotent |
list_scenarios | instance | the named starting scenarios the simulator ships, each with a summary | read-only |
load_scenario | instance, name | replaces state with the named scenario, keeping the simulator’s built-in records | destructive, idempotent |
seed | instance, body | merges records into state. body is {register: {key: {field: value}}}; rystic describe <twin> -json shows each register with an example record | destructive |
set_clock | instance, time (RFC3339) | pins the clock; events due by then fire. Returns the clock the simulator then reads | destructive, idempotent |
advance_clock | instance, seconds or duration (exactly one) | moves the clock by whole seconds, forward or, negative, back: 90s, 15m, 48h. Events fire as the clock passes them | destructive |
reset | instance | returns the instance to the simulator’s built-in records, the state it boots in with no scenario, dropping scenario and seeded records, and clears the call and sent logs | destructive, idempotent |
inspect_state | instance, register, key, view, paging | the state, resource then row key then row, a page at a time under state; view asks for one of the simulator’s named views instead, answered as {view, result} | read-only |
list_calls | instance, method, path_prefix, status, actor, paging | the API calls served since the last reset, oldest first, each naming its caller | read-only |
list_sent | instance, effect, type, paging | what the simulator sent since the last reset: webhooks, emails and other outbound effects, oldest first, from the egress log | read-only |
diff_state | instance, register, paging | rows created, changed and deleted per resource since the baseline; each changed field carries its JSON Pointer path, before and after | read-only |
save_test_case | name (required), instance or instances, prompt, out_dir | saves the run since the baseline as a test case and drafts its checks; see Save a run as a test case | none |
check_test_case | case (required), instance or instances, out_dir | grades the run since the baseline against a saved case, with no model; see Check a later run | none |
snapshot_twin | instance, name | saves the instance’s state under name; omit name to list its snapshots. See Snapshots | none |
restore_twin | instance, name (required) | resets the instance and lands the named snapshot | destructive, idempotent |
diff_state compares against the state the instance had when start_twin started it, load_scenario loaded a scenario, reset reset it or restore_twin restored a snapshot. seed leaves that baseline in place, so seeded rows read as created. For an instance started outside the server, with rystic run say, the first diff_state stores the current state as the baseline and answers "baseline": "first_seen" with an empty diff.
Pages, filters and links
inspect_state, list_calls, list_sent and diff_state answer one page at a time: at most limit entries (default 50, at most 500) and about 20 KB. Pass a page’s next_cursor back as cursor for the next one. A cursor belongs to the baseline it was read against: after a reset, load_scenario or new baseline it is refused, and reading starts again without one.
Every page carries truncated. It is true when more pages remain or an entry was cut, and then hint names the exact call that gets the rest.
format sets how much of each entry a page carries:
| Tool | concise (default) | detailed |
|---|---|---|
inspect_state | string values over 256 bytes cut; cut_rows lists the rows they were cut from | whole rows |
list_calls | each call’s seq, at, method, path, status and actor | the simulator’s whole record |
list_sent | each event’s seq, effect, type and owner | the payload too |
diff_state | created and deleted rows with string values over 256 bytes cut; changed rows as their fields | each changed row’s state too |
In either format an entry over 12 KB is cut and marked.
Filters narrow a page; matched counts the entries they match across every page:
| Tool | Filters |
|---|---|
inspect_state | register, and key with it for one row |
list_calls | method, path_prefix, status (a code such as 404, or a class such as "4xx"), actor |
list_sent | effect, type |
diff_state | register |
list_calls and list_sent also report total, every entry since the last reset. In list_sent, delivery sums up webhook delivery with its newest 20 attempts in results, and results_total counts every attempt the simulator reports. tail, which starts the page at the last N entries, still works but is deprecated in favor of cursor.
A state larger than one page also comes with state_uri and a resource link to rystic://instances/{twin}/{name}/state, the whole state uncut. A named view that is a file, or larger than a page, comes back as its size and the URL to fetch it from.
Snapshots
snapshot_twin with a name saves the instance’s state, clock, id counters and pending scheduled events and, when your plan includes network shaping, the network profile. A snapshot of the same name is replaced, and the result’s overwrote says so. Names take letters, digits, ., _ and -, at most 64. Without a name it lists the instance’s snapshots.
restore_twin resets the instance, then lands the snapshot, so ids minted after the restore continue where the snapshot left off. The restored state becomes diff_state’s baseline. A snapshot taken on another version of the simulator is refused with snapshot_version_mismatch.
The CLI keeps snapshots on this machine, per simulator and instance name. They survive stop_twin and a restart of the same instance on the same version.
A snapshot does not hold, so a restore does not bring back:
- the idempotency cache, so a replayed request makes a new object;
- the request-id counter;
- the calls and sent logs, which start empty;
- the webhook delivery queue;
- credential bindings;
- upload sessions;
- the rate limiter;
- the clock’s wall offset;
- scheduled events in full: only the pending events of the scenario’s own schedule come back, less any that uses a value a fired event bound, and a schedule loaded on its own replays from its start;
- built-in records deleted before the snapshot, which the reset brings back;
- whether a scenario installed the network profile: once restored, it stays through later resets.
snapshot_twin refuses an instance with a pending event that acts as a named caller (snapshot_unsupported) and a state too large to restore (snapshot_too_large). When the simulator refuses a snapshot after the reset, restore_twin fails with restore_failed and leaves the instance reset. On an older simulator release, every restore_twin result carries a warning: ids minted after the restore can collide with restored records, and per-account records can come back duplicated.
Save a run as a test case
save_test_case turns the run an agent just made into a regression test. It writes a grid cell directory, the same shape rystic grid writes: cell.json and, per simulator, twins/<grid name>/before.json, after.json, calls.json and egress.json, numbers exact. The grid name is the simulator’s name without -twin (linear for linear-twin), as rystic grade and the eval grid name it. Then it drafts the eval’s deterministic checks with rystic grade --draft into the cell’s eval.yaml, so rystic grade <cell_dir> passes on this run and fails on a later run that differs.
| Argument | Meaning |
|---|---|
name | the case’s name: the eval name in cell.json and the temp directory’s prefix |
instance or instances | one instance, or several, one per simulator, for a case across simulators |
prompt | the task the agent was given, the draft’s prompt. Omitted, the draft carries a placeholder and notes says to replace it |
out_dir | an absolute path that does not exist yet or is an empty directory. Omitted, a new temp directory that is kept |
It returns {name, cell_dir, eval, draft, twins, notes}; each twins entry carries instance, twin, scenario, baseline, baseline_taken_at, seeded and counts (created, changed, deleted).
- Before is the instance’s baseline, from
start_twin,load_scenario,reset,restore_twinor a firstdiff_state. Records seeded throughseedafter the baseline count as setup, not as the run’s. An instance with no baseline is refused withno_baseline. - After is the state now, with the calls and sent log since the last reset.
- Rows a scenario’s own timeline seeds during the run count as the run’s changes.
- The draft expects each row the run created or changed, forbids any other creation, change, caller or method, and allows each deletion the run made. It is a starting point: trim the checks to what the task requires before keeping them.
Check a later run
check_test_case grades a new run against a saved case, as rystic grade does, with no model. After save_test_case, reset or load_scenario the instance, run the agent again, then call check_test_case with case set to the cell_dir or eval path save_test_case returned, or any eval file, as an absolute path. It captures the run since the baseline into a new cell, under out_dir or a new temp directory, grades it and writes its verdict.json.
| Field | Meaning |
|---|---|
result | pass, fail, error, or incomplete when every graded check passed but one was skipped |
reason | why the case got that result |
cell_dir | the new run’s cell, with its verdict.json |
checks | per check: its id, result (pass, fail, error or skipped), detail and, for a failure, the rows behind it |
notes | anything to know about the run, such as a baseline scenario that differs from the case’s |
A case directory’s checks/ YAML files are graded too. A script check is skipped, not run. The case’s world must name exactly the instances’ simulators, by full name (linear-twin) or grid name (linear).
Prompts
The server offers two prompts, which a client lists as prompts or slash commands:
| Prompt | Arguments |
|---|---|
reproduce_webhook_bug | twin (required), scenario, description of what the webhook does wrong |
write_test_for_flow | twin (required), scenario, flow (required), the flow in a sentence or two |
Each walks the agent through start_twin, load_scenario or seed, the act step, list_sent, list_calls and diff_state, then save_test_case. twin takes a short name, stripe for stripe-twin. A simulator that is not running, installed or in your plan is refused with unknown_twin, and a scenario a running instance does not ship with unknown_scenario. Under --twin, the prompts take no twin argument.
Clients that support completion fill the arguments in:
twincompletes from the running simulators first, then the installed ones and those your license names, without a network call.scenariocompletes from a running instance of the simulator named intwin. With none running, it offers nothing.
Resources
| URI | Type | Holds |
|---|---|---|
rystic://guide | text/markdown | the call order step by step, the forms of the instance argument, every error code with its fix, and a worked example. A scoped server’s guide says what it serves |
rystic://instances/{twin}/{name}/state | application/json | a running instance’s whole state, uncut: resource, then row key, then row |
rystic://instances/{twin}/{name}/baseline | application/json | the state diff_state compares the instance with, in the same shape, uncut: a deleted row and a changed field’s before value are whole here |
{twin} is the full simulator name, such as stripe-twin, and {name} the instance’s name. Both template arguments complete from the running instances.
Example session
The agent’s prompt: “Test that our checkout code charges the customer once and sends one receipt webhook.”
start_twin {"twin": "stripe-twin", "scenario": "design_studio", "name": "stripe"}
→ {"instance": "stripe", "ref": "stripe-twin/stripe", "url": "http://localhost:53124", "port": 53124, "runner": "native", "baseline": "stored", …}
# the agent points its Stripe client at http://localhost:53124 and runs the checkout test
list_calls {"instance": "stripe", "method": "POST"}
→ {"calls": [the POST /v1/charges the test made, with its caller], "matched": 1, "truncated": false, …}
list_sent {"instance": "stripe"}
→ one charge.succeeded event
diff_state {"instance": "stripe"}
→ {"baseline": "start_twin", "counts": {"created": 1, "changed": 0, "deleted": 0}, "resources": […], "truncated": false, …}
advance_clock {"instance": "stripe", "duration": "48h"}
→ {"instance": "stripe", "clock": "…"}
save_test_case {"instance": "stripe", "name": "checkout-charges-once", "prompt": "Charge the customer once and send one receipt webhook."}
→ {"name": "checkout-charges-once", "cell_dir": "…/checkout-charges-once-…", "draft": "…", "twins": [{"instance": "stripe", "twin": "stripe-twin", "scenario": "design_studio", "counts": {"created": 1, "changed": 0, "deleted": 0}, …}], …}
# later, after a code change: load the scenario again, rerun the checkout test, grade the new run
load_scenario {"instance": "stripe", "name": "design_studio"}
check_test_case {"instance": "stripe", "case": "…/checkout-charges-once-…"}
→ {"result": "pass", "cell_dir": "…", "checks": [{"id": "nothing_destructive", "result": "pass", …}, …], …}
stop_twin {"instance": "stripe"}
Results and errors
Every tool declares an outputSchema describing its result shapes and the error object, and every result carries structuredContent that matches it, error results included. A successful result’s text content carries the same JSON, for a client that reads only text.
A failed call is an error result (isError: true), never a protocol error. Its structured content is {"error": {"code", "message", "cause", "fix", "retryable", …}}, and its text carries the same fields as lines. Extra fields appear where the code needs them, such as retry_after_ms, upgrade_url, twin_errors or snapshots. A malformed call is a bad_argument naming the argument: a wrong type, an unknown or missing argument, arguments that are not an object, a string over 64 KiB, or nesting over 64 levels.
A stdin line that is not one JSON-RPC message gets a JSON-RPC error, and the session goes on. That covers invalid JSON, a line nested over 10,000 levels, a line over 32 MiB, and any batch. Batches are not supported, so send each message on its own line. The error’s code is -32700 (parse error) or -32600 (invalid request). It carries the request’s id when the line names one and null otherwise, and each request in a batch gets its own error. The server logs each refused line to stderr. These are protocol errors, separate from the tool errors in the table below.
retryable says whether the same call, unchanged, may succeed later, and retry_after_ms, when present, how long to wait first. The table gives each code’s default. An error after a write that may have taken effect, such as a seed or advance_clock whose answer was lost, is not retryable, and its fix says how to check before repeating it. Tools that are safe to repeat (reset, load_scenario, set_clock) keep the default. rystic mcp -errors prints this table.
| code | retryable | retry_after_ms | cause | fix |
|---|---|---|---|---|
| bad_argument | no | - | An argument is missing, unknown, of the wrong type, too large or out of range, or the arguments are not a JSON object. | Correct the argument the message names; tools/list carries each tool’s input schema. |
| twin_not_running | no | - | No running instance has that name, or the instance did not answer. | Call list_twins for the running instances; start one with start_twin, or rystic run <twin> -d where this server serves no start_twin. |
| instance_ambiguous | no | - | The name matches more than one running instance. | Pass the instance as |
| feature_not_in_plan | no | - | The twin refused the call because your plan does not include the feature it needs. | Upgrade at the error’s upgrade_url, or use a tool your plan includes; the error names the feature. |
| port_in_use | no | - | Another instance or process holds the port start_twin asked for. | Omit port, so the twin takes a free one, or name another port. |
| name_conflict | no | - | An instance of this twin with that name is already running. | Pass another name, or stop_twin the running instance first; under —twin, which pins the name, stop_twin it first. |
| unknown_scenario | no | - | The twin has no scenario by that name. | Call list_scenarios for the names this twin ships. |
| bad_seed_body | no | - | The twin refused the seed body: not a non-empty object of {register: {key: {field: value}}}, or a register, key or field it does not accept. | Fix what the error’s twin_errors name; rystic describe <twin> -json shows each register with an example record. |
| license_or_lease | no | - | The license or API key is missing, expired, refused, or does not cover this twin. | Run rystic login (or rystic license activate <file>), then retry; rystic doctor names what is wrong. |
| unknown_twin | no | - | No release of that twin exists, at that version or for this platform. | Run rystic list for the twins your plan covers; omit version for the newest release. |
| download_failed | yes | - | The downloaded release failed its checksum or would not unpack, so nothing was installed. | Call start_twin again, which downloads afresh; if it fails again, rystic feedback with the message. |
| twin_unhealthy | yes | 1000 | The twin ran but did not serve, or answered with a server error. | Retry once after retry_after_ms; if it fails again, read the log lines the error carries, or rystic logs <name>, then stop_twin and start_twin again. |
| twin_refused | no | - | The twin refused the call with a status no other code covers. | Read the twin’s message in the error; rystic docs <twin> describes its control routes. |
| out_of_scope | no | - | The call names a twin or instance other than the one this server serves (rystic mcp —twin, —instance). | Call the tool with no twin or instance argument, so it acts on the served instance; for another twin, run another rystic mcp. |
| no_baseline | no | - | The instance has no stored state to compare the run against: it was started outside this server and never reset, loaded or diffed through it. | Call reset, load_scenario or diff_state on the instance before the run, then save_test_case or check_test_case after it. |
| unknown_snapshot | no | - | The instance has no snapshot by that name; snapshots belong to one instance name of one twin. | Pick one of the names the error’s snapshots lists, or call snapshot_twin without a name to list them; snapshot_twin takes a new one. |
| snapshot_version_mismatch | no | - | The snapshot was taken on another version of the twin than the instance runs now, so its records may not fit this one. | Take a new snapshot_twin on this version, or start_twin the instance at the version the message names and restore there. |
| snapshot_unsupported | no | - | The instance has a pending event that acts as a named caller, and a restore cannot bring back credential bindings, so a snapshot would not hold it. | Let the event fire or cancel it, then call snapshot_twin again. |
| snapshot_too_large | no | - | The snapshot’s world document is over the size the twin’s seed route accepts, so a restore could not land it. | Reduce the instance’s state (reset, or delete records) and snapshot again; this snapshot cannot be restored. |
| restore_failed | no | - | The instance was reset, but the twin refused the snapshot’s world, so it now holds a reset state. | Read the message and twin_errors; take the snapshot again on this twin, or rystic feedback with the message if it persists. |
| cannot_write | no | - | The cell’s files could not be written: out_dir holds files already, is a file, or is not writable. | Pass an out_dir that does not exist yet or is an empty writable dir, or omit it for a new temp dir. |
| draft_failed | no | - | rystic grade --draft could not draft an eval from the saved cell. | Read the message; cell_dir keeps the files, and rystic grade --draft <cell_dir> reproduces it. If it persists, rystic feedback with the message. |
| case_not_found | no | - | The case check_test_case was given does not exist, or the dir holds no eval.yaml. | Pass the cell_dir or eval path save_test_case returned, or an eval file, as an absolute path. |
| bad_case | no | - | The case’s eval does not parse or is not a valid eval, a checks/ file beside it is invalid, or every check it holds is a script. | Fix what the message names, or save_test_case a run again for a fresh case. |
| case_mismatch | no | - | The case’s world names other twins than the instances given run. | Pass one instance of each twin the case’s world names: instance for one twin, instances for several. |
| grade_failed | no | - | rystic grade could not grade the cell of the run check_test_case captured. | Read the message; cell_dir keeps the files, and rystic grade <cell_dir> reproduces it. If it persists, rystic feedback with the message. |
| runtime_unavailable | no | - | The docker runner needs a container runtime and none answered. | Start Docker or Podman, or pass runner: native. |
| network | yes | 2000 | An endpoint Rystic needed (the release or registry) did not answer. | Check the connection and retry after retry_after_ms. |
| internal | yes | - | Something failed that no other code covers. | Retry; if it persists, rystic feedback with the message. |
The docs MCP is a different server
rystic mcp | Docs MCP | |
|---|---|---|
| Where it runs | on your machine, launched by the client over stdio | hosted at https://www.rystic.ai/mcp, over HTTP |
| What it does | starts and drives simulators | searches and fetches these docs |
| Tools | the 17 above | search_rystic_docs, fetch_rystic_doc |
| Needs | the rystic CLI and an API key or license | nothing |
Add both: the docs MCP answers how a simulator behaves, rystic mcp runs one. Agent quick start → Machine-readable surfaces shows the one-line install for the docs MCP.