# Drive simulators from your coding agent

Run rystic mcp so a coding agent starts, seeds, clocks, inspects and diffs simulators, rolls them back to snapshots, and saves and checks runs as test cases, through MCP tools. One-command install for Claude Code, Cursor, Codex, VS Code and Windsurf.

`rystic mcp` is a local MCP server. Through it your coding agent starts a [simulator](https://www.rystic.ai/docs/concepts/glossary.md#simulator), loads a [scenario](https://www.rystic.ai/docs/concepts/glossary.md#scenario), [seeds](https://www.rystic.ai/docs/concepts/glossary.md#seed) records, moves the [clock](https://www.rystic.ai/docs/concepts/glossary.md#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](https://www.rystic.ai/docs/installation.md) 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](https://www.rystic.ai/docs/control-plane.md) 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](https://www.rystic.ai/docs/installation.md). When it is not on the `PATH`, use the command below.

[Add to Cursor]: cursor://anysphere.cursor-deeplink/mcp/install?name=rystic&config=eyJjb21tYW5kIjoicnlzdGljIiwiYXJncyI6WyJtY3AiXX0=
[Add to VS Code]: vscode:mcp/install?%7B%22name%22%3A%22rystic%22%2C%22type%22%3A%22stdio%22%2C%22command%22%3A%22rystic%22%2C%22args%22%3A%5B%22mcp%22%5D%7D
[Add to VS Code Insiders]: vscode-insiders:mcp/install?%7B%22name%22%3A%22rystic%22%2C%22type%22%3A%22stdio%22%2C%22command%22%3A%22rystic%22%2C%22args%22%3A%5B%22mcp%22%5D%7D

One command writes the server entry into your client's config:

```bash
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).

```text
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](#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](#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](#scope-the-server), 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:

```text
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.

```bash
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](#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

```json
{
  "mcpServers": {
    "rystic": {
      "type": "stdio",
      "command": "rystic",
      "args": [
        "mcp"
      ]
    }
  }
}
```

### Cursor

```json
{
  "mcpServers": {
    "rystic": {
      "type": "stdio",
      "command": "rystic",
      "args": [
        "mcp"
      ]
    }
  }
}
```

### Codex

```toml
[mcp_servers.rystic]
command = "rystic"
args = ["mcp"]
```

### VS Code

```json
{
  "servers": {
    "rystic": {
      "type": "stdio",
      "command": "rystic",
      "args": [
        "mcp"
      ]
    }
  }
}
```

### Windsurf

```json
{
  "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:

```text
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](#resources) describe only what it serves, and a [prompt](#prompts) 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:

1. `list_twins` for the running instances, or `start_twin`, which returns the URL.
2. `load_scenario`, or `seed`, to set the starting state.
3. Your own code acts against that URL. The simulator answers as the real API does; you do not call it through MCP.
4. `inspect_state`, `list_calls`, `list_sent` and `diff_state` to read what happened.
5. `save_test_case` to keep the run, or `check_test_case` to 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](#resources) 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](#pages-filters-and-links) | 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](#pages-filters-and-links) | the API calls served since the last reset, oldest first, each naming its caller | read-only |
| `list_sent` | `instance`, `effect`, `type`, [paging](#pages-filters-and-links) | what the simulator sent since the last reset: webhooks, emails and other outbound effects, oldest first, from the [egress log](https://www.rystic.ai/docs/concepts/glossary.md#egress-log) | read-only |
| `diff_state` | `instance`, `register`, [paging](#pages-filters-and-links) | 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](#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](#check-a-later-run) | none |
| `snapshot_twin` | `instance`, `name` | saves the instance's state under `name`; omit `name` to list its snapshots. See [Snapshots](#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_twin` or a first `diff_state`. Records seeded through `seed` after the baseline count as setup, not as the run's. An instance with no baseline is refused with `no_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:

- `twin` completes from the running simulators first, then the installed ones and those your license names, without a network call.
- `scenario` completes from a running instance of the simulator named in `twin`. 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."

```text
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.

<!-- BEGIN generated: rystic mcp -errors -->

| 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 <twin>/<name>, one of the refs the message lists; a server started with --twin takes --instance <name> instead. |
| 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. |

<!-- END generated: rystic mcp -errors -->

## 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](https://www.rystic.ai/docs/agent-quick-start.md#12-machine-readable-surfaces) shows the one-line install for the docs MCP.

---

Source: https://www.rystic.ai/docs/mcp-server · Markdown: https://www.rystic.ai/docs/mcp-server.md
