Skip to main content
Glama
README.md
# TopicForge

<!-- mcp-name: io.github.yaniswav/topicforge -->

[![PyPI version](https://img.shields.io/pypi/v/topicforge.svg)](https://pypi.org/project/topicforge/)
[![CI](https://github.com/yaniswav/TopicForge/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/yaniswav/TopicForge/actions/workflows/ci.yml)
[![Python versions](https://img.shields.io/pypi/pyversions/topicforge.svg)](https://pypi.org/project/topicforge/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/yaniswav/TopicForge/blob/main/LICENSE)
[![Read-only](https://img.shields.io/badge/safety-read--only-2563eb)](https://github.com/yaniswav/TopicForge#security-model)

A read-only MCP (Model Context Protocol) server that lets an AI agent inspect a ROS2 graph, recorded bag files and the DDS layer underneath ROS. The code has no write path: it cannot publish to the bus or command a robot, and there is no permission system to configure.

Every tool also declares itself read-only in the MCP protocol (`readOnlyHint`, `destructiveHint=false`, `idempotentHint`, with an honest `openWorldHint`), and a test fails if any tool lacks it. See [`SECURITY.md`](SECURITY.md#read-only-guarantee-architecture-declaration-proof) for the guarantee and its limits.

It gives the agent eighteen typed tools that return frozen Pydantic schemas (output contract 2: one object per tool, one `note`, units in the field names; see [`docs/CONTRACT.md`](docs/CONTRACT.md)), identical whether the server talks to a real robot or to its built-in mock fixtures. Ask why `nav_planner` gets no scan, and the agent reads the bus, finds the BEST_EFFORT writer facing a RELIABLE reader and names the incompatible policy (see [`examples/02-debug-qos-mismatch.md`](examples/02-debug-qos-mismatch.md)). It is meant for ROS2 developers, robotics ML/CV engineers and teams that cannot accept a write path into a production stack.

For DDS, TopicForge joins a domain as a read-only participant through one open-source binding (Eclipse CycloneDDS from PyPI) and reads the builtin discovery topics that the OMG DDS-RTPS protocol standardizes. Cyclone DDS, Fast DDS and Dust DDS participants have been observed on live buses: the Fast DDS ones on every CI run of the ROS 2 test bench (Humble and Jazzy with `rmw_fastrtps_cpp`) and in the OmniSim validation. RTI Connext, OpenDDS and CoreDX announce themselves through the same standard discovery, but none of them has been observed yet. This covers discovery only: participants, readers, writers and their QoS. See [`docs/dds-interop-matrix.md`](docs/dds-interop-matrix.md).

## Quickstart

No ROS2 is needed; the mock adapter serves deterministic fixtures for a small differential robot (LIDAR + RGB camera). Python 3.10 to 3.13.

```bash
pip install topicforge
TOPICFORGE_MODE=mock python -m topicforge
# Windows PowerShell: $env:TOPICFORGE_MODE="mock"; python -m topicforge
```

The server speaks MCP over stdio and waits for a client, so wire it into one. For Claude Desktop, add to `claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "topicforge": {
      "command": "python",
      "args": ["-m", "topicforge"],
      "env": { "TOPICFORGE_MODE": "auto" }
    }
  }
}
```

Then ask it to list the topics. To try the bag tools on fixtures, set `TOPICFORGE_MODE` to `mock` and ask it to analyze `/tmp/demo.mcap`; in `auto` or `live` a bag path must exist. For Claude Code: `claude mcp add topicforge -- topicforge`. If the client cannot launch the server (WSL2 to Windows, a robot over an SSH tunnel), `topicforge --transport streamable-http --port 8765` serves `http://127.0.0.1:8765/mcp` on loopback (another `--host` needs a bearer token in `TOPICFORGE_HTTP_TOKEN`); see [`docs/CLIENTS.md`](docs/CLIENTS.md#local-http-transport).

**Other MCP clients.** Ready-to-paste configs for Claude Desktop, Cursor, VS Code, Windsurf, Cline, Roo Code, Continue, Zed, JetBrains, Codex, Gemini CLI, Goose and more are in [`docs/CLIENTS.md`](docs/CLIENTS.md). One-click: [Add to Cursor](https://cursor.com/install-mcp?name=topicforge&config=eyJjb21tYW5kIjoidXZ4IiwiYXJncyI6WyItLWZyb20iLCJ0b3BpY2ZvcmdlPT0wLjExLjAiLCJ0b3BpY2ZvcmdlIl0sImVudiI6eyJUT1BJQ0ZPUkdFX01PREUiOiJhdXRvIiwiVE9QSUNGT1JHRV9ERFNfQkFDS0VORCI6ImN5Y2xvbmUiLCJUT1BJQ0ZPUkdFX0REU19ET01BSU5fSUQiOiIwIn19) | [Install in VS Code](https://vscode.dev/redirect/mcp/install?name=topicforge&config=%7B%22command%22%3A%22uvx%22%2C%22args%22%3A%5B%22--from%22%2C%22topicforge%3D%3D0.11.0%22%2C%22topicforge%22%5D%2C%22env%22%3A%7B%22TOPICFORGE_MODE%22%3A%22auto%22%2C%22TOPICFORGE_DDS_BACKEND%22%3A%22cyclone%22%2C%22TOPICFORGE_DDS_DOMAIN_ID%22%3A%220%22%7D%7D).

Setup for a real ROS2 environment (WSL2, Linux, Docker, native Windows) is in [`docs/TESTING.md`](docs/TESTING.md); recurring monitoring prompts and the privacy contract are in [`docs/TUTORIEL.md`](docs/TUTORIEL.md).

## Tools

Every response except `health_check` carries `mode_effective` (`"live"` or `"mock"`), so a caller can tell a real graph from fixtures, and a single `note` that explains anything unusual. `health_check.contract_version` is `2`. The full reference, with every field, is generated in [`docs/TOOLS.md`](docs/TOOLS.md); upgrading from 0.6.x: [`docs/MIGRATION-0.7.md`](docs/MIGRATION-0.7.md).

| Tool                    | Purpose                                                                                          |
| ----------------------- | ------------------------------------------------------------------------------------------------ |
| `health_check`          | Environment and mode introspection. Always succeeds; reports `mode` next to `requested_mode`      |
| `list_topics`           | Discover the ROS2 graph                                                                          |
| `get_topic_info`        | Message type, publisher/subscriber counts and QoS for one topic                                  |
| `list_nodes`            | ROS 2 nodes on the graph: names, namespaces, duplicate names (one `ros2 node list` call)         |
| `get_node_info`         | One node's publishers, subscribers, services, actions, parameters (secrets masked) and `use_sim_time`; a node that does not answer is reported, not an error |
| `inspect_tf`            | The TF tree from `/tf_static` and `/tf`: roots, stale and two-parent frames, disconnected trees, sim-time-safe ages and rates, optional path between two frames |
| `graph_diff`            | What changed since it last worked: snapshots the ROS 2 graph (or a bag) and diffs it with `previous`, a `snap-N` id or a bag: nodes, topics, types, writer QoS, counts, bag rates |
| `diagnose`              | Is anything wrong: one bounded sweep (nodes, topics, QoS pairs, restarts, TF, `/rosout` errors, a parameter read per node) ranked `critical` / `warning` / `info` with evidence; a check that cannot run is listed, never silent. Holds the ROS lock up to `budget_s` |
| `sample_messages`       | Peek recent messages on a ROS2 topic (count capped at 50), or a time window                      |
| `get_image`             | One live camera frame as image content, on the sampling helper                                   |
| `analyze_bag`           | Summarize a `.mcap` / `.db3` recording or `rosbag2_*` directory (via `ros2 bag info`)            |
| `list_participants`     | DDS participants on the domain: vendor, `name` (EntityName QoS, Cyclone) and `hostname`          |
| `detect_qos_mismatches` | Incompatible QoS pairs between DDS readers and writers                                           |
| `peek_dds_samples`      | Raw DDS samples; structured on the three builtin discovery topics, presence-only on user topics   |
| `participant_events`    | Timeline of participant `discovered` / `lost` events                                             |
| `topic_metrics`         | Frequency, sequence-gap and latency schema; data only for builtin discovery topics               |
| `peek_bag_samples`      | Decoded samples from a recorded bag, including ROS 1 `.bag` (`rosbags` ships with the package) |
| `list_endpoints`        | DDS writers and readers with structured QoS, per-topic roll-up that flags orphans (writer with no reader, reader with no writer) |

The server also offers three MCP prompts, `diagnose-dds-bus`, `inspect-ros2-robot` and `diagnose-nav2`, that give a client the read-only call order, and short server `instructions` for clients that do not show prompts (see [`docs/CLIENTS.md`](docs/CLIENTS.md#prompts-and-instructions)).

Walkthroughs against the mock, each with the exact tool calls and payloads, are in [`examples/`](examples/README.md). To run the DDS tools against a real bus with several programs and vendors, see [`examples/dds/README.md`](examples/dds/README.md) (`python examples/dds/run_all.py`).

## Modes

| Mode   | When to use                                      | Backend                    |
| ------ | ------------------------------------------------ | -------------------------- |
| `mock` | Development, demos, CI, screencasts              | Deterministic fixtures     |
| `live` | ROS2 sourced and on PATH, and/or a DDS backend   | `ros2` CLI, DDS participant |
| `auto` | Detect what is available, else mock (default)    | Best available             |

`live` and `auto` degrade instead of failing: if neither `ros2` nor a DDS backend comes up, the server serves the mock fixtures and `health_check` reports `mode: "mock"` next to `requested_mode`. Only `TOPICFORGE_MODE=mock` forces fixtures unconditionally. The live ROS2 adapter shells out to the `ros2` CLI, so `rclpy` does not need to be importable by the server. `sample_messages` may use a short-lived sampling helper instead, under the distro's own Python: see `TOPICFORGE_ROS_SAMPLER` below.

## DDS backends

```bash
pip install topicforge                           # includes Eclipse CycloneDDS where wheels exist (Python 3.10-3.13; Linux x86_64, Windows x64, macOS)
pip install "topicforge[dds]"                    # alias, kept so older commands work; forces a source build elsewhere (needs the Cyclone C library)
TOPICFORGE_DDS_BACKEND=cyclone python -m topicforge
```

`TOPICFORGE_DDS_BACKEND` accepts `mock` (default), `cyclone` and `auto` (`cyclone` if the binding imports, else `mock`). The default `mock` selects no DDS backend: installing the Cyclone binding is not enough, you must also set `TOPICFORGE_DDS_BACKEND=cyclone`. With `TOPICFORGE_MODE=live` and no backend selected, the DDS tools raise `DDS module is not active: ...` with the actual cause (backend not selected, binding not installed, or binding installed but the adapter failed to start), and `health_check` reports `dds_backend: "none"` plus `dds_inactive_note`. An explicit value is honoured with or without `ros2` on PATH, in any mode except `mock`. If the binding is missing or the participant cannot start, the server logs a warning naming the cause and falls back to the ROS2 CLI alone, or to the mock fixtures. When both `ros2` and a DDS backend are up, a composite adapter routes the nine `ros2` CLI tools (`list_topics`, `get_topic_info`, `list_nodes`, `get_node_info`, `inspect_tf`, `graph_diff`, `diagnose`, `sample_messages`, `analyze_bag`) to the CLI and the six DDS tools to the DDS backend; `peek_bag_samples` goes to the ROS half but runs no `ros2` process.

Fast DDS participants (ROS 2 nodes on `rmw_fastrtps_cpp`) need no backend of their own: the Cyclone backend observes them through standard RTPS discovery. Since 0.9.0 `TOPICFORGE_DDS_BACKEND=fast` is refused, like `rti`, `opensplice`, `coredx`, `intercom`, `opendds` and `dust`: startup stops with an error that lists the valid values (see [`docs/TROUBLESHOOTING.md`](docs/TROUBLESHOOTING.md)). Full backend selection, the routing table and the QoS mismatch scenario are in [`docs/DDS_QUICKSTART.md`](docs/DDS_QUICKSTART.md).

## Configuration reference

| Variable                   | Default | Description                                                                                       |
| -------------------------- | ------- | ------------------------------------------------------------------------------------------------- |
| `TOPICFORGE_MODE`          | `auto`  | `mock`, `live` or `auto`                                                                          |
| `TOPICFORGE_LOG_LEVEL`     | `INFO`  | `DEBUG`, `INFO`, `WARNING`, `ERROR`                                                               |
| `TOPICFORGE_ROS2_BIN`      | `ros2`  | Name or path of the ROS2 CLI binary                                                               |
| `TOPICFORGE_ROS_SAMPLER`   | `auto`  | What `sample_messages` runs on: `cli` (`ros2 topic echo`), `rclpy` (the sampling helper, an error if it cannot run) or `auto` (the helper if a check at startup passes, else the CLI). `health_check.sampling_backend` says which, `sampling_backend_note` why not the helper |
| `TOPICFORGE_ROS_PYTHON`    | unset   | Python that runs the sampling helper. Unset: the interpreter in the shebang of `ros2`, then `python3` |
| `TOPICFORGE_TELEMETRY`     | `off`   | Opt-in anonymous telemetry; an unrecognized value aborts startup. See [Telemetry](#telemetry)     |
| `TOPICFORGE_DDS_BACKEND`   | `mock`  | `mock` (no DDS backend), `cyclone`, `auto`; anything else is rejected                              |
| `TOPICFORGE_DDS_DOMAIN_ID` | `0`     | DDS domain observed (0..232). Joined at startup; changing it needs a restart                      |
| `TOPICFORGE_DDS_DECODE`    | `off`   | `off` or `on`: decode ROS 2 user-topic samples on the Cyclone backend. Adds a visible subscriber during each read, and a lazy publisher may wake. See below |
| `TOPICFORGE_MAX_SAMPLE_BYTES` | `1048576` | Size cap for one sampled message (1 KiB..64 MiB); a call returns at most 4 times that. Over-cap messages are dropped with a note |

`TOPICFORGE_DDS_DECODE=on` makes `peek_dds_samples` return decoded samples and `topic_metrics` a real publish rate for ROS 2 topics (`rt/` names, types in the `rosbags` store), from a bounded read of at most 5 s and 10 s. **Each read adds a subscriber that the robot can see, so a publisher that only works when someone listens may wake.** Heavy types (`Image`, `PointCloud2` and similar), custom types and writers of different types are refused with a note and today's empty result; so is any doubt about the definition. It is off by default until it has soaked; see [SECURITY.md](SECURITY.md) and `docs/CONTRACT.md` section 9.28.

Command-line options (not environment variables): `--transport stdio|streamable-http` (default `stdio`), `--port N` (default 8765) and `--host ADDR` (default `127.0.0.1`), the last two only with `streamable-http`. The HTTP transport binds `127.0.0.1` with DNS-rebinding protection; `TOPICFORGE_HTTP_TOKEN` (at least 32 characters) makes every request carry `Authorization: Bearer <token>` and is required for any other `--host` (plain HTTP, so prefer an SSH tunnel); see [docs/CLIENTS.md](docs/CLIENTS.md#local-http-transport).

Samples with comments are in [`.env.example`](.env.example). Any invalid value stops the server with `topicforge: configuration error: ...` and exit code 2, so a typo cannot silently change behaviour.

## Limitations

External validation against a simulated robot's ground truth (OmniSim): [docs/VALIDATION.md](docs/VALIDATION.md).

- DDS validation is partial. The Cyclone adapter has run against real buses with Cyclone, Fast DDS and Dust DDS participants, on Windows and in CI on Ubuntu and Windows (`.github/workflows/demo.yml`, `.github/workflows/ros2-live.yml`). No RTI, OpenDDS, CoreDX or OpenSplice participant has been observed by this project. The multi-vendor claim rests on the RTPS protocol guarantee, not on a recorded cross-vendor run.
- By default user-topic payloads are not decoded (`TOPICFORGE_DDS_DECODE=on` changes this for ROS 2 topics). `peek_dds_samples` on a user topic returns count 0 and a note that the topic is announced on the bus; no traffic is read. `topic_metrics` therefore has data only for the builtin discovery topics and says so in its `status`. It is a discovery-layer probe, not a publish-rate monitor.
- Liveliness at runtime is not observed. A writer that is alive but silent (a hung process whose lease is still renewed) looks healthy, because TopicForge reads discovery, not data. An opt-in data probe is planned. A crash and a clean leave cannot be told apart, and `lost_ns` is an upper bound of the death.
- Cyclone vendor ids: participants that do not follow the RTPS vendor-id convention in their GUID prefix (Dust DDS, and RTI by default) are reported with vendor `unknown`.
- `get_image` and `window_s` on `sample_messages` run on the sampling helper only (`TOPICFORGE_ROS_SAMPLER`). A raw `sensor_msgs/Image` needs the `image` extra, Pillow (`uvx --from "topicforge[image]" topicforge` or `pip install "topicforge[image]"`); a `CompressedImage` passes through without it. `get_image` sends the frame to the model: see `SECURITY.md`.
- Single domain: the server observes the domain it joined at startup; changing it needs a restart.
- DDS Security is not handled. A participant without credentials sees an empty secure bus. `detect_qos_mismatches` checks Partition, type name, Reliability, Durability, Deadline, Liveliness, LatencyBudget, Ownership (kind), DestinationOrder and DataRepresentation (History as a risk); Presentation, XTypes assignability and runtime behavior are not checked, and the result lists them in `policies_unchecked`. It returns a `MismatchScan` envelope: read `reports` for the mismatches.
- `sample_messages` (live) streams `ros2 topic echo` until `count` messages arrive or `timeout_s` (1..40, default 10, for the whole call) runs out, and returns what arrived with a `note` (`N of M messages within T s`, saying whether a publisher exists). QoS is matched to the publishers, so latched topics work. The payload has nested named fields (`payload.header.stamp.sec`); `timestamp_ns` is `header.stamp` (the publisher's clock, sim time on a simulation), the time in the body of `/clock`, `/tf` and `/rosout` messages (`stamp_source` `payload`), or 0 for other headerless types, with `stamp_source` and `received_ns` (wall clock when the CLI printed it). Arrays are cut at 128 elements by default; `max_array_length` (1..65536, or null for no cut) and `arrays_summary_only` change that, and a cut is listed under `_truncated_fields`. `nan` and `inf` come back as strings. `count` above 50 is capped with a note.
- `analyze_bag` parses `ros2 bag info` text for the totals and counts when `ros2` is on PATH, and reads the bag with `rosbags` otherwise or when the CLI fails (`reader` says which: `ros2_cli`, `rosbags` or `fixture`). One bounded scan (8 s) adds per-topic times, rates (`(n - 1) / span`), `latched` and `anomalies` as observations, never causes: `/clock` steps, silences of steady sensor and state topics (a topic that stops early counts) and of `/tf` edges, several topics silent together as one line, and backward header stamps, each with topic, offset and size, relative to each topic's own rhythm. Empty means none found by these checks, not clean: sustained irregular loss goes unseen. When the scan is cut, rates fall back to count / bag duration (`frequency_basis`) and `note` gives the part covered. A bag with `compression_mode: FILE` is not unpacked: its anomalies are skipped, and the rosbags-only path refuses it above 64 MiB. `peek_bag_samples` reads the file itself, through `rosbags`, returns the first `count` messages in recording order (not the last), and needs no `ros2` either; its `timestamp_ns` is the message's `header.stamp` (`stamp_source` `header`), the body time of `Clock`, `TFMessage` and `Log` messages (`payload`) or, without either, the bag record time (`stamp_source` `recorded`), and `recorded_ns` is always the bag record time; bags that embed no message definitions (Humble `.db3`) are decoded with the Humble definitions, or the distro the bag records, and `note` says so. Without `ros2`, the bag tools read the file with `rosbags` (a missing path is an error); only an explicit `TOPICFORGE_MODE=mock` gives fixtures. `health_check` also reports `sim_clock_published` (live only): true when `/clock` has a publisher, a hint that header stamps may be simulation time.
- Calls are serialized per backend (one `ros2` call at a time, one DDS call at a time) and each call has a 45 s wall budget, lock wait included; a call that cannot get its lane fails at once with `busy`. `health_check` and `peek_bag_samples` take no lock.
- No streaming or push subscriptions: tools are strictly request/response.

Not done yet: an opt-in probe to tell a hung writer from a healthy one, wider real-bus validation (RTI, OpenDDS) and DDS Security. Open work is tracked in [issues](https://github.com/yaniswav/TopicForge/issues).

## Telemetry

Opt-in, anonymous and off by default. When off, instrumentation returns the handler unchanged: no event is built, no transport is constructed, no network code runs (pinned by `tests/test_telemetry.py::test_build_app_off_makes_no_transport_calls`).

```bash
TOPICFORGE_TELEMETRY=on python -m topicforge
```

On-values: `on`, `1`, `true`, `yes`, `enabled`. Off-values: unset, `off`, `0`, `false`, `no`, `disabled`. Anything else is a configuration error rather than a silent "off".

When on, each tool call emits one event with exactly six fields:

| Field        | Example         | Notes                                                       |
| ------------ | --------------- | ----------------------------------------------------------- |
| `tool_name`  | `"list_topics"` | One of the eighteen tools, never argument values             |
| `latency_ms` | `12.34`         | Handler wall-clock duration, 2 decimals                     |
| `mode`       | `"mock"`        | Mode of the adapter actually serving: `mock` or `live`      |
| `version`    | `"0.11.0"`      | TopicForge server version                                   |
| `session_id` | `"a1b2c3..."`   | Random UUID per process, never persisted                    |
| `success`    | `true`          | Whether the handler returned or raised                      |

Never sent: topic names, message types or payloads, bag paths or contents, hostnames, usernames, IP addresses, environment variables, error messages. The field set is fenced by `tests/test_telemetry.py::test_payload_contains_only_whitelisted_keys`; adding a field requires updating this section. The transport is a local log line only (the `topicforge.telemetry` logger): there is no HTTP endpoint, nothing is sent over the network, and none is planned. The implementation is in [`src/topicforge/telemetry/`](src/topicforge/telemetry/).

## Security model

TopicForge is designed for local trust: it runs as a subprocess of your MCP client on a machine you control and inspects your own ROS2 graph, DDS domain and bag files. It is not hardened for adversarial inputs.

- `TOPICFORGE_ROS2_BIN` accepts an arbitrary path; treat it the way you treat `PATH`.
- `analyze_bag` and `peek_bag_samples` open whatever path the client passes (no workspace isolation, no symlink restriction).
- All `ros2` invocations use `subprocess.run` with an argument list, never `shell=True`. ROS2 topic names are validated against `^/[A-Za-z0-9_/]+$` first.
- The server loads no third-party code at startup.
- No outbound network calls unless telemetry is turned on.

Before exposing TopicForge to untrusted MCP clients (hosted endpoints, shared environments), add path isolation and revisit the `TOPICFORGE_ROS2_BIN` policy. Vulnerability reports: see [`SECURITY.md`](SECURITY.md).

## Development

```bash
git clone https://github.com/yaniswav/TopicForge.git && cd TopicForge
python -m venv .venv && source .venv/bin/activate     # Windows: .venv\Scripts\Activate.ps1
pip install -e ".[dev]"
python -m ruff check src tests
python -m pytest
```

Tests run against the mock adapter, the live adapter's pure parsers and the binding-free DDS helpers; they never need a running ROS graph. Tests needing the `cyclonedds` binding skip themselves when it is absent, and the `integration` marker (real-bus tests) is deselected by default. The `Makefile` (`make check`) uses POSIX shell syntax; on plain PowerShell run the commands above. See [`CONTRIBUTING.md`](CONTRIBUTING.md).

## Upgrading

TopicForge is pre-1.0; [`CHANGELOG.md`](CHANGELOG.md) lists every change, including the yanked releases and removed extras. 0.7.0 changed the shape of tool results (`contract_version` 2): [`docs/MIGRATION-0.7.md`](docs/MIGRATION-0.7.md) maps every old field to its new one.

## Layout

```
src/topicforge/
  server/      MCP bootstrap, build_app(settings), prompts and instructions
  tools/       thin MCPServer handlers, no backend logic
  services/    input validation, orchestration, adapter factory
  adapters/    ros2_live, ros2_mock, dds_cyclone, common/ (binding-free logic: QoS, CDR,
               discovery buffers, bag reader, message summaries)
  models/      frozen Pydantic schemas, the contract with MCP clients
  config/      settings and mode resolution
  telemetry/   opt-in, off by default
examples/      mock walkthroughs (*.md) and runnable live DDS examples (dds/)
scripts/       real-bus interop checks, ground-truth comparator, agent evaluation
docs/          guides and reference
tests/         pytest suite, mock-only, no ROS2 or DDS required
```

Layers are strictly separated: handlers never call `subprocess`, adapters are the only code that talks to a backend and never import the service layer, and new backends implement the `MiddlewareAdapter` protocol in `adapters/base.py`.

## License

MIT, see [LICENSE](LICENSE). Integration or support work for a specific ROS 2 / DDS setup: ethvignot.yanis@gmail.com.

TDQS

A4.1/5.0

Scored across 18 tools

Disambiguation4/5

The set splits cleanly along layers (ROS 2 graph vs raw DDS vs bag files) and each tool's description explicitly cross-references the sibling to use instead, e.g. list_nodes vs list_participants vs list_endpoints, and sample_messages vs peek_dds_samples vs peek_bag_samples. Residual overlap remains: get_topic_info's QoS and list_endpoints' qos both describe reliability/durability, diagnose partially duplicates health_check plus every list call, and topic_metrics vs sample_messages' `rate` both report frequency. Boundaries are nonetheless statable, so misselection risk is low but not zero.

Naming Consistency4/5

All names are snake_case and most follow verb_noun (list_nodes, get_topic_info, detect_qos_mismatches, peek_bag_samples, analyze_bag). A minority break the verb pattern with noun-only or bare-verb names (graph_diff, participant_events, topic_metrics, health_check, diagnose), but these are still readable and semantically clear. Minor deviation, not a mixed convention problem.

Tool Count4/5

18 tools is at the upper edge but each covers a distinct source/format combination (ROS graph, DDS bus, MCAP/db3/ROS1 bags) plus one aggregate `diagnose`, so few feel redundant. The triplication of sampling tools across sources and the separate list/get pairs for nodes, topics, participants and endpoints inflate the count slightly beyond what the purpose strictly requires.

Completeness4/5

For a deliberately read-only observability server, coverage is strong: discovery for nodes/topics/participants/endpoints, QoS mismatch diagnosis, live and offline sampling, temporal metrics, TF tree, graph diffing, health and an aggregate diagnose. Gaps: services and actions have no first-class listing or inspection tool (only an `include_internal` flag on list_endpoints), and there is no dedicated log query beyond /rosout filtering inside sample_messages or diagnose's log group.

Maintenance

ActivityActive
ResponsivenessNo recent community issues