---
title: "MCP tools"
description: "The four tools of the monedula-sim-link MCP server: build_playground_url, open_scenario, validate_actions and cluster_to_url, with inputs and outputs."
url: https://monedula.dev/flock/docs/sim-link/reference/mcp-tools/
---
`monedula-sim-link mcp` serves four tools over the Model Context Protocol on stdio, and an MCP client shows each tool's input schema to the agent. [Use the MCP server](/flock/docs/sim-link/how-to/use-the-mcp-server/) shows how to register the server.

Every tool validates its input and returns a link only when it decodes back to the identical action list; a validation error names the offending field, separator or config path. [Self-validation before printing](/flock/docs/sim-link/concepts/self-validation/#the-mcp-tools) lists the checks.

| Tool | Use it to |
| --- | --- |
| [`build_playground_url`](#build_playground_url) | Build a free-play session from a description of a cluster and a list of timed actions. |
| [`open_scenario`](#open_scenario) | Get the deep link of a curated scenario. |
| [`validate_actions`](#validate_actions) | Check what the playground would actually load from an actions string or a link. |
| [`cluster_to_url`](#cluster_to_url) | Visualize a live cluster, snapshotted read-only. |

## build_playground_url

A free-play session from a declarative description. Every input is optional, and an empty call opens the default cluster.

| Input | Meaning |
| --- | --- |
| `cluster` | The topology: `single-dc` (the default), `active-passive`, `active-active`, `stretched-2-5` or `stretched-3`. |
| `shape` | The starting cluster, described below. Omit it for the topology's default. Not supported for `stretched-2-5`. |
| `actions` | An ordered list of timed actions to replay on the starting cluster. It may be empty, to just open the cluster. |

The `shape` object has these fields:

| Field | Meaning |
| --- | --- |
| `brokers` | The total broker count of a single-zone topology (`single-dc`). |
| `brokersPerZone` | The broker count per zone, for multi-zone topologies, in topology order: `[primary, dr]` for `active-passive`, `[west, east]` for `active-active`, `[dc-a, dc-b, dc-c]` for `stretched-3`. |
| `topics` | The topics to create. A topic named `orders` shapes the primary topic, and every other topic becomes an extra topic, at most 6. Omit it to keep the default single `orders` topic. |
| `rackAware` | `true` turns on rack-aware placement (`single-dc`). |

Each entry of `topics` has `name`, `partitions` and `replicationFactor`, and optionally `minInSyncReplicas` and `cleanupPolicy`:

| Field | Meaning |
| --- | --- |
| `name` | Letters, digits, `.`, `_` and `-`, at most 40 characters. An `add_group` can subscribe to a name with a `.`, but the actions with a `topic` field, such as `produce` and `reassign_partition`, cannot reference it, because their grammar reserves the dot, so prefer `_` or `-`. |
| `partitions` | The partition count, at least 1, and at most the limit of the topology: 24 for `single-dc`, 6 for `active-passive` and `active-active`, and for `stretched-3` the largest count from 1 to 6 that keeps the busiest broker within 3 partitions at the chosen replication factor. 9 brokers at RF 3 allow 6 partitions and reject 7, and 9 brokers at RF 4 allow 3 ([the mapping reference](/flock/docs/sim-link/reference/mapping/#limits-and---clamp)). |
| `replicationFactor` | At least 1, and at most the broker count of the hosting zone set. |
| `minInSyncReplicas` | Optional `min.insync.replicas`, from 1 to the replication factor. Unset means the simulator's default: 1 for an extra topic, which is Kafka's own default, and `max(1, replicationFactor-1)` for `orders`. |
| `cleanupPolicy` | Optional: `delete`, `compact` or `compact,delete`. Unset means `delete`. |

Leaving `minInSyncReplicas` or `cleanupPolicy` out is safe: the tool writes the defaults into an extra topic's segment of the `size` parameter. The segment for `orders` carries only a value that differs from its default, so `minInSyncReplicas: 2` at replication factor 3, or `cleanupPolicy: delete`, adds nothing to its `size`, which stays `6x4x3` for 6 brokers and 4 partitions. Before 0.11.0 an extra topic with no `minInSyncReplicas` was written with an empty field, and the simulator dropped the whole topic from the session.


Diskless topics, from the KIP-1163 draft, add three inputs:

- `cluster` also takes `diskless-3az`: one cluster whose three availability zones are racks, with rack-aware placement and a diskless `clicks` topic next to `orders`. Its default is 6 brokers and 3 partitions with replication factor 3; `shape.brokers` sets at most 9 brokers, and a topic has at most 6 partitions.
- A `topics` entry may set `disklessEnable: true`, except with a `compact` or `compact,delete` cleanup policy or on `active-passive` and `active-active`.
- `shape.disklessTiming` overrides the diskless timing of the session with `commitIntervalMs`, the `diskless.append.commit.interval.ms`, and `uploadMs`, the object-storage upload latency. Both are optional, in milliseconds from 100 to 5000. The override needs a diskless topic, from `disklessEnable` or the `diskless-3az` preset; without one, the call fails.

The shape describes no producers or consumer groups, so it cannot set the `client.rack` that the simulator's size format can give each initial producer and consumer group.


Every action has a `kind` and an `at`, the simulation time in milliseconds at which it fires, 0 or more. Actions are replayed sorted by `at`, and spacing them about 1000 ms apart lets the playground animate them one at a time. The other fields depend on the kind:

| Kind | Required fields | Optional fields |
| --- | --- | --- |
| `produce` | `producerId`, `topic` | `key`, and either `value` or `tombstone: true`. The two are mutually exclusive. |
| `kill_broker` | `brokerId` | `graceful`: `true` for a graceful shutdown, otherwise a hard kill. |
| `restart_broker` | `brokerId` | |
| `add_broker` | `brokerId` | `rack`: pin the broker's placement to a rack. |
| `remove_broker` | `brokerId` | |
| `add_producer` | `producerId` | `topic`: pin the producer to a topic. |
| `remove_producer` | `producerId` | |
| `add_group` | `groupId`, `topics` (at least one; the first is the primary) | `autoOffsetReset`: `earliest`, `latest` or `none`. Omitted, a new group starts at the high watermark. |
| `remove_group` | `groupId` | Removes a whole consumer group with its members. |
| `add_consumer` | `groupId`, `memberId` | |
| `add_share_group` | `groupId`, `topic` | `members`: explicit member ids. |
| `config_change` | `path`, and either `value` or `clear: true` | `clear` is allowed only for the paths the URL format marks as clearable. |
| `reassign_partition` | `topic`, `partition`, `replicas` (the full target list of broker ids) | |
| `change_replication_factor` | `topic`, `targetReplicationFactor` | |
| `set_replica_speed` | `brokerId`, `slow` | `slow: true` throttles the broker's follower replication, `false` restores full speed. |
| `tier_offload` | `topic`, `partition`, `toOffset` | Offloads the local log up to that offset, exclusive, to remote storage. |

Broker ids are `broker-1` to `broker-N`. A default free-play cluster starts with the producer `p1`, the consumer group `c1` and the primary topic `orders`. A `config_change` `path` is one the URL format whitelists, such as `producers.p1.config.acks` or `topics.orders.retentionMs`.

The tool does not write the optional settings field of `add_group`. An `add_group` with a `settings` object fails with an error that names the field: `settings` only appears in the output of [`validate_actions`](#validate_actions), which decodes it.

Example input:

```json
{
  "shape": {
    "brokers": 6,
    "topics": [
      { "name": "orders", "partitions": 4, "replicationFactor": 3 },
      { "name": "payments", "partitions": 6, "replicationFactor": 3,
        "minInSyncReplicas": 2, "cleanupPolicy": "delete" }
    ]
  },
  "actions": [
    { "kind": "add_group", "at": 1000, "groupId": "analytics", "topics": ["orders"] },
    { "kind": "produce", "at": 2000, "producerId": "p1", "topic": "orders", "key": "O1" },
    { "kind": "config_change", "at": 3000, "path": "producers.p1.config.acks", "value": "all" },
    { "kind": "kill_broker", "at": 8500, "brokerId": "broker-2" },
    { "kind": "restart_broker", "at": 22000, "brokerId": "broker-2" }
  ]
}
```

It returns the link and the fields below:

```text
https://monedula.dev/kafka-simulator/playground?scenario=free&actions=...&size=6x4x3xpayments~6~3~2~d~~~~
```

| Output | Meaning |
| --- | --- |
| `url` | The full playground link. The actions are percent-encoded, and compressed automatically past the plain threshold. |
| `cluster`, `size` | The `cluster` and `size` parameters the link carries, if any. |
| `actionCount` | The number of encoded actions. |
| `plainLength` | The length of the plain action string. The compressed form starts past 1500 characters. |
| `compressed` | `true` when the link carries the compressed `~1` form. |
| `decodedActions` | The actions decoded back out of the generated link. Compare them with your input. |
| `notes` | Non-fatal adjustments or hints. |

## open_scenario

The deep link of a curated scenario, paused on its scripted walkthrough.

| Input | Meaning |
| --- | --- |
| `scenarioId` | The scenario id in dotted form, such as `00.1.3`. |

```json
{ "scenarioId": "00.1.3" }
```

```text
https://monedula.dev/kafka-simulator/playground?scenario=00.1.3
```

The output has `url`, `scenarioId` and an optional `note`.

## validate_actions

What the playground would actually load from an actions string. The playground drops entries it cannot parse without saying so, and this tool reports them.

| Input | Meaning |
| --- | --- |
| `actions` | The raw actions value: the plain grammar (`K:broker-2@8500,...`), the compressed `~1` form, a percent-encoded value, or a full playground link, from which the `actions` parameter is extracted. |

```json
{ "actions": "K:broker-2@8500,NOPE,AG:analytics:orders@1000" }
```

The result has 2 surviving entries, sorted by `at`, and one dropped entry: `NOPE`, with the reason `missing @<at>`.

| Output | Meaning |
| --- | --- |
| `entries` | The decoded actions the playground would load, sorted by `at`, in the action shape of `build_playground_url`. An `add_group` that carries the settings field also has a `settings` object, described below. |
| `keptCount` | The number of actions that survive, after the cap. |
| `droppedCount`, `dropped` | The entries the playground would silently drop, each with its `entry` text and a `reason`. |
| `truncatedCount` | Decodable actions beyond the cap that the playground would discard. The earliest 2000 win. |
| `compressed`, `plainLength` | Whether the value used the compressed form, and the length of the plain string. |
| `compressionThreshold` | 1500: plain strings longer than this are emitted compressed. |
| `maxUrlActions` | 2000: the action cap. |
| `canonicalEncoding` | The surviving actions re-encoded canonically, sorted and plain. Absent when no action survives. Also absent when the actions decode but the tool's generator rules refuse to write them back, such as a `produce` to a dotted region topic like `west.orders`; a note then says why. |
| `notes` | Non-fatal hints. |

### Group settings

An `add_group` can carry the group's settings in an optional fourth field, which the [playground URL format](/flock/docs/kafka-simulator/reference/url-format/#24-the-add_group-settings-field) specifies. The playground writes it when `kafka-consumer-groups --reset-offsets` rebuilds a configured group, and when it pins a new group on an active-active cluster to its region. `validate_actions` decodes the field and echoes it as a `settings` object on the entry. The field is strict, like the playground's decoder: an empty field, an unknown or repeated setting, or a value outside its domain drops the whole `add_group`, which then appears in `dropped`.

```json
{ "actions": "AG:g1:west.orders~east.orders:e:region=west~assignor=range~principal=User:CN=west~_OU=apps@9000" }
```

The entry it returns:

```json
{
  "kind": "add_group",
  "at": 9000,
  "groupId": "g1",
  "topics": ["west.orders", "east.orders"],
  "autoOffsetReset": "earliest",
  "settings": { "region": "west", "assignor": "range", "principal": "User:CN=west,OU=apps" }
}
```

The `settings` object has a field only for each setting the group carries, with its value unescaped, so the `~_` in the link is a `,` in `principal`. An `add_group` without the settings field has no `settings` object.

| Field | Meaning |
| --- | --- |
| `region` | The region the group is pinned to. It reads only that region's copies of its topics. |
| `assignor` | `partition.assignment.strategy`: `range`, `roundrobin`, `sticky` or `cooperative-sticky`. |
| `isolationLevel` | `isolation.level`: `read_committed` or `read_uncommitted`. |
| `autoCommit` | `enable.auto.commit`, `true` or `false`. |
| `pollMs` | The poll interval in milliseconds, any integer. 0 or less stops the poll loop. |
| `maxPollRecords` | `max.poll.records`, at least 1. |
| `maxPollIntervalMs` | `max.poll.interval.ms`, at least 1. |
| `rack` | The group's `client.rack`. |
| `principal` | The principal the group authenticates as. |

`canonicalEncoding` writes the settings back in the order of this table, whatever order the link used. The `settings` object is output only: [`build_playground_url`](#build_playground_url) refuses it as input and never writes the field.

## cluster_to_url

The live-cluster counterpart to `build_playground_url`. It connects read-only to a real Kafka cluster, through the same path as [`connect`](/flock/docs/sim-link/reference/cli/connect/), and returns the link that visualizes it. It sends the same read requests, plus `DescribeProducers` only when `producers` is `true`: [the read-only guarantee](/flock/docs/sim-link/concepts/read-only-guarantee/) lists them.

| Input | Meaning |
| --- | --- |
| `brokers` | The bootstrap brokers as `host:port`, at least one. Required. |
| `topics` | Exact topic names to include. It fails if one is missing, and it is the only way to include internal topics. Listed topics are always kept when the selection is over the limit of 6 extra topics. |
| `topicsRegex` | Additionally include non-internal topics that match this Go regular expression. |
| `tls` | Connect over TLS. Implied when `caCert`, `clientCert` or `clientKey` is set. |
| `caCert`, `clientCert`, `clientKey` | PEM file paths, resolved on the machine where the MCP server runs, not on the client's. `clientCert` and `clientKey` go together. |
| `saslMechanism`, `username`, `password` | SASL: `plain`, `scram-sha-256` or `scram-sha-512`, which requires `username`. Prefer the `MONEDULA_SIM_LINK_PASSWORD` environment variable on the server's machine to `password`; the variable is the fallback when `password` is empty. |
| `timeoutSeconds` | The overall snapshot timeout in seconds. The default is 15. |
| `maxRecordsPerPartition` | The cap on synthetic trailing records per partition. The default is 8. |
| `clamp` | Trim a cluster over 10 brokers or 24 partitions per topic, or a `topics` list of more than 6 topics besides `orders`, deterministically, with warnings. Without it, such a selection fails with the list of violations. More topics or consumer groups than the simulator shows never fail: the most eventful are kept, with a warning. |
| `producers` | Also read the idempotent and transactional producers with `DescribeProducers` (Kafka 2.8 or newer), and add up to 4 of them to the link: the ones whose last write to a shown topic is in the last 15 minutes, ranked by the number of partitions they wrote to, each pinned to the topic it wrote to most and named `pid-<producerId>`. Off by default, because the request needs `READ` on every selected topic; with it off, no `DescribeProducers` request is sent. A producer that is neither idempotent nor transactional never appears, and one with an open transaction is shown as an idempotent producer. A refused or unsupported read is a warning, never a failure. Unlike the CLI, the tool has no input for the 15-minute window. |

Omit `topics` and `topicsRegex` to include every non-internal topic.

```json
{ "brokers": ["broker1:9092", "broker2:9092"], "topics": ["orders", "payments"] }
```

| Output | Meaning |
| --- | --- |
| `url`, `cluster`, `size`, `actionCount`, `plainLength`, `compressed` | As for `build_playground_url`. The `size` parameter carries each topic's real `min.insync.replicas`, `cleanup.policy`, `retention.ms` and `retention.bytes`. |
| `decodedActions` | The actions decoded back out of the link, including `add_group` and `add_consumer` for every mapped consumer group and its members, up to 4 members per group, and `add_producer` for every producer when `producers` is `true`. It starts with `remove_producer` for `p1` and `remove_group` for `c1`, the template producer and group of free play. |
| `brokerNames`, `topicNames`, `groupNames` | The real-to-simulator tables, as lists of `real` and `simulator` pairs. A group with no committed offset on a shown topic is absent from `groupNames`, and so is a group outside the 4 kept. |
| `warnings` | Unreadable offsets, approximations, the topics and groups kept from a larger selection, any clamps applied and, with `producers`, an open transaction, a producer read that was refused or unsupported, and a window with no producer in it. |
| `notes` | Non-fatal hints. |

A selection that matches nothing is an error with an explanation.

## Related

- [Handle large clusters](/flock/docs/sim-link/how-to/handle-large-clusters/) describes the free-play limits, which topics and groups are kept, and what `clamp` trims.
- [The mapping reference](/flock/docs/sim-link/reference/mapping/) explains the actions that `cluster_to_url` emits.
- The [`mcp` command reference](/flock/docs/sim-link/reference/cli/mcp/) lists the flag of the server.