Your Agent Is Now a First-Class Tumult Operator

2026-07-04

Since 1.0, Tumult has had an MCP server. It worked. It also treated the agent on the other end like a tourist: 19 tools, all returning prose, no way to tell a harmless read from a fault injection, and an experiment runner that threw its journal away the moment the call returned.

2.1.0 rebuilds that surface into something spec-faithful. 24 tools. Annotations on every one. Structured output with advertised schemas on 16. Workspace files served as real MCP resources. And — the part that changes what an agent can actually do — the run→analyze→recommend loop now closes entirely over MCP.

Let’s take those one at a time.

The loop is closed

Here’s the embarrassing part of the old behavior. tumult_run_experiment executed your experiment, rendered the journal into the response… and that was it. Nothing written to disk. Nothing ingested. The analytics store — the thing that powers recommend, coverage, and trend — never heard about the run.

before 2.1.0:

  agent ──► run_experiment ──► journal text ──► (gone)

  recommend / coverage / trend ──► store ──► "you've never tested anything"
                                              (the store never saw the runs)

The tool was a dead-end executor. An agent could run chaos all afternoon and the intelligence tools would keep recommending the experiments it had just finished.

Now tumult_run_experiment does what tumult run has always done: it persists the journal (journal_path, default journal.toon) and auto-ingests it into the analytics store. Skip with no_ingest, redirect with store_path — CLI parity, parameter for parameter.

2.1.0:

  agent ──► run_experiment ──┬──► journal.toon written
                             └──► ingested into analytics store
                                        │
              ┌─────────────────────────┼─────────────────────┐
              ▼                         ▼                     ▼
        tumult_recommend         tumult_coverage        tumult_trend
        "test kafka next"        "tumult-net: NONE"     "score improving"
              │
              ▼
        agent picks the gap ──► run_experiment ──► (loop)

Run, learn, decide, run again. That’s chaos engineering. Now it’s chaos engineering an agent can do alone in a room.

The result even tells you how the ingestion went — ingested, duplicate, skipped, or failed: <reason>. Ingestion trouble is a warning in the result, never a failed run.

Annotations: reads are free, chaos is gated

MCP tool annotations exist so a client can make policy decisions before calling anything. Every one of the 24 tools now declares its readOnlyHint / destructiveHint / idempotentHint / openWorldHint.

The split is stark, and that’s the point:

                     24 tools
                        │
        ┌───────────────┼──────────────────┐
        ▼               ▼                  ▼
  18 read-only     2 destructive      4 writers
  idempotent       + open-world       (non-destructive)
                                       
  validate          run_experiment     create_experiment
  read_journal      gameday_run        gameday_create
  analyze                              report
  compliance        these inject       recommend
  trend             REAL faults        (open-world with
  coverage          into REAL          agent= set)
  ...               systems

  client policy:    client policy:     client policy:
  auto-approve ✓    ASK THE HUMAN ⚠    approve writes ✎

A well-behaved MCP client reads these hints and does the obvious thing: let the agent grep through journals, compliance verdicts, and coverage reports without ceremony — and put a human approval gate in front of the two tools that pause your Redis or kill your database connections.

One annotation worth calling out: tumult_recommend is marked openWorldHint: true, but not because Tumult phones home. When you pass agent: claude-code, the tool spawns your local agent CLI, and that may call its model API. The annotation tells the truth about the blast radius.

Structured output: schemas, not prose

The old tools returned text. Nicely formatted text! Which your agent then… parsed. With regexes. In 2026.

before:                              after:

┌─────────────────────┐    ┌──────────────────────────────┐
│ "Experiment passed!  │    │ content: [ text (unchanged) ] │
│  Duration: 314ms.    │    │ structuredContent: {          │
│  3 steps executed,   │    │   journal: {...},             │
│  journal looks OK."  │    │   journal_path: "...",        │
└─────────┬───────────┘    │   ingestion: "ingested"       │
          │                └──────────────┬───────────────┘
          ▼                               │
  agent regexes the prose      validated against the
  and hopes for the best       outputSchema advertised
                               in tools/list ✓

16 of the 24 tools now return structuredContent and advertise a matching outputSchema. Here’s a real one — the shape tumult_run_experiment declares, filled in by the Redis experiment from the README:

{
  "journal": {
    "experiment_title": "Redis resilience — verify recovery after disruption",
    "experiment_id": "d2f8...",
    "status": "completed",
    "started_at_ns": 1782043200000000000,
    "ended_at_ns": 1782043200314000000,
    "duration_ms": 314,
    "method_results": [
      {
        "name": "pause-redis",
        "activity_type": "action",
        "status": "succeeded",
        "duration_ms": 102,
        "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
        "span_id": "00f067aa0ba902b7"
      }
    ],
    "rollback_results": []
  },
  "journal_path": "/workspace/journal.toon",
  "ingestion": "ingested"
}

The status field is an enum in the schema (completed / deviated / aborted / failed / interrupted), the activity statuses are enums, the trace IDs are right there for your observability stack. No prose archaeology.

The same strictness applies on the way in: enum-ish parameters (format, rollback_strategy, framework, metric, load_tool) reject unknown values with the list of valid ones. And every inline text payload is capped at 512 KiB with an explicit truncation notice — your context window will thank us.

Resources: the workspace has URIs now

Tool calls aren’t the only way to get at files anymore. The workspace is served under a proper URI scheme:

tumult://
   │
   ├── journal/{file} ──────── journals, read as JSON {summary, journal}
   │                           (application/json)
   ├── experiment/{file} ───── experiment definitions, raw TOON
   │                           (application/toon)
   └── gameday/{file} ──────── campaign files, raw TOON
                               (application/toon)

  and tools hand out links into it:

  run_experiment ────► resource_link: tumult://journal/journal.toon
  gameday_create ────► resource_link: tumult://gameday/drill.gameday.toon
  report ────────────► resource_link: tumult://journal/report.xml's path
  list_journals ─────► one resource_link per journal (first 50)

So the flow an MCP client sees: call a tool, get the text, get the structured content, and get a link to the artifact the tool just produced. Follow the link with resources/read whenever you want the file — no second tool call, no path guessing. resources/list paginates with opaque cursors (pages of 100), and the three list tools grew limit / offset / total for the same reason: nobody wants 4,000 journal paths in one response.

Filenames only, by the way — path separators and traversal attempts in a resource URI are rejected with the same containment checks the tools use. And if you’ve set TUMULT_MCP_TOKEN, resource requests pass the same bearer gate as tool calls.

The full map

Five of the 24 are new in 2.1.0 (report, compliance, trend, agents, gameday_create), and together the surface now covers the whole workflow:

 discover            validate           run                analyze
 ────────            ────────           ───                ───────
 discover            validate           run_experiment ⚠   read_journal
 list_experiments    create_experiment                     list_journals
 agents                                                    analyze
                                                           analyze_store
                                                           store_stats
                                                           query_traces

 report              compliance         gameday            recommend
 ──────              ──────────         ───────            ─────────
 report              compliance         gameday_create     recommend
 trend                                  gameday_run ⚠      coverage
                                        gameday_analyze
 agentic: list_scenarios,               gameday_list       ⚠ = destructive,
          smoke, run_experiment                                gate it

Everything an operator does at the terminal, an agent can now do over MCP — with the same shared code underneath. tumult_compliance runs the same tumult_core::compliance scoring as tumult compliance. tumult_recommend runs the same tumult-intelligence engine as the CLI, agent enhancement and validation gate included. tumult_gameday_run even fixed a real bug on the way in: it used to silently skip the campaign’s declared load; it now drives the same k6 executor as the CLI.

One implementation, two front doors.

Try it

tumult-mcp --transport http --port 3100

Point any MCP client at it, list the tools, and watch it read the annotations before it touches anything. Then let it run an experiment and ask for a recommendation — the answer will already know about the run.


The MCP server negotiates protocol revision 2025-11-25 and ships in 2.1.0. The MCP Guide has the full data model — annotations, schemas, resources, pagination, and auth.


Tumult is open source under the Apache-2.0 license.