TopicForge

io.github.yaniswavPublished by GitHub account yaniswavv0.9.0Updated Nov 3, 2026

Read-only MCP server for ROS2 and DDS: topics, participants, QoS mismatches, bags.

Overview

TopicForge's local MCP server; no sign-in required; STDIO; 17 tools.

No AI overview yet: these facts come from the registry entry and the server itself.

Installation

In SourceWeft

  1. Open TopicForge in the dashboard and add it to a workspace.
  2. Enable the server for the chats that should use its tools.

Desktop only via STDIO. STDIO servers start a local process, so they need the SourceWeft desktop host.

Other MCP clients

Follow the launch instructions in the repository.

README

TopicForge

[PyPI version] [CI] [Python versions] [License: MIT] [Read-only]

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 for the guarantee and its limits.

It gives the agent seventeen 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), 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). 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.

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 topicforgeTOPICFORGE_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 or to analyze /tmp/demo.mcap. 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.

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. One-click: Add to Cursor | Install in VS Code.

Setup for a real ROS2 environment (WSL2, Linux, Docker, native Windows) is in docs/TESTING.md; recurring monitoring prompts and the privacy contract are in 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; upgrading from 0.6.x: docs/MIGRATION-0.7.md.

ToolPurpose
health_checkEnvironment and mode introspection. Always succeeds; reports mode next to requested_mode
list_topicsDiscover the ROS2 graph
get_topic_infoMessage type, publisher/subscriber counts and QoS for one topic
list_nodesROS 2 nodes on the graph: names, namespaces, duplicate names (one ros2 node list call)
get_node_infoOne 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_tfThe 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_diffWhat 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
diagnoseIs 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_messagesPeek recent messages on a ROS2 topic (count capped at 50)
analyze_bagSummarize a .mcap / .db3 recording or rosbag2_* directory (via ros2 bag info)
list_participantsDDS participants on the domain: vendor, name (EntityName QoS, Cyclone) and hostname
detect_qos_mismatchesIncompatible QoS pairs between DDS readers and writers
peek_dds_samplesRaw DDS samples; structured on the three builtin discovery topics, presence-only on user topics
participant_eventsTimeline of participant discovered / lost events
topic_metricsFrequency, sequence-gap and latency schema; data only for builtin discovery topics
peek_bag_samplesDecoded samples from a recorded bag, including ROS 1 .bag (rosbags ships with the package)
list_endpointsDDS 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).

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

Modes

ModeWhen to useBackend
mockDevelopment, demos, CI, screencastsDeterministic fixtures
liveROS2 sourced and on PATH, and/or a DDS backendros2 CLI, DDS participant
autoDetect 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.

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). Full backend selection, the routing table and the QoS mismatch scenario are in docs/DDS_QUICKSTART.md.

Configuration reference

VariableDefaultDescription
TOPICFORGE_MODEautomock, live or auto
TOPICFORGE_LOG_LEVELINFODEBUG, INFO, WARNING, ERROR
TOPICFORGE_ROS2_BINros2Name or path of the ROS2 CLI binary
TOPICFORGE_TELEMETRYoffOpt-in anonymous telemetry; an unrecognized value aborts startup. See Telemetry
TOPICFORGE_DDS_BACKENDmockmock (no DDS backend), cyclone, auto; anything else is rejected
TOPICFORGE_DDS_DOMAIN_ID0DDS domain observed (0..232). Joined at startup; changing it needs a restart
TOPICFORGE_MAX_SAMPLE_BYTES1048576Size 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

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.

Samples with comments are in .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.

  • 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.
  • User-topic payloads are not decoded. 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.
  • 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 (live) parses ros2 bag info text for the totals and counts; anomaly detection is mock-only. Per-topic times, rates ((n - 1) / span) and latched are added when the bag can be read locally (.db3 with the standard library, .mcap with rosbags), else rates fall back to count / bag duration (frequency_basis). peek_bag_samples reads the file itself, through rosbags, returns the first count messages in recording order (not the last), and is served only by the ROS2 CLI adapter or the mock; 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, bag tools return fixtures: check health_check for mode: "mock" before trusting bag output. 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.

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:

FieldExampleNotes
tool_name"list_topics"One of the seventeen tools, never argument values
latency_ms12.34Handler wall-clock duration, 2 decimals
mode"mock"Mode of the adapter actually serving: mock or live
version"0.9.0"TopicForge server version
session_id"a1b2c3..."Random UUID per process, never persisted
successtrueWhether 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 default transport is a structured log line; there is no HTTP endpoint yet. The implementation is in 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.

Development

bash
git clone https://github.com/yaniswav/TopicForge.git && cd TopicForgepython -m venv .venv && source .venv/bin/activate     # Windows: .venv\Scripts\Activate.ps1pip install -e ".[dev]"python -m ruff check src testspython -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.

Upgrading

TopicForge is pre-1.0; 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 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)  models/      frozen Pydantic schemas, the contract with MCP clients  config/      settings and mode resolution  telemetry/   opt-in, off by defaultexamples/      mock walkthroughs (*.md) and runnable live DDS examples (dds/)scripts/       real-bus interop checks, ground-truth comparator, agent evaluationdocs/          guides and referencetests/         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 new backends implement the MiddlewareAdapter protocol in adapters/base.py.

License

MIT, see LICENSE. Integration or support work for a specific ROS 2 / DDS setup: [email protected].

Source: README.md at commit 9de14ce

Tools

17

health_check

Health check

Report TopicForge environment state as a `HealthReport`: `mode`, `ros_backend`, `dds_backend`, `ros_tools_available`, `ros2_distro`, `rmw_implementation`, `dds_domain_id` and `observed_domain_note` (only the domain joined at startup is observed), server version and sample cap; for a live DDS backend also `observer_started_ns` / `now_ns` (nothing earlier was watched) and the discovery tracker status `tracker_*` (errors, evictions or a stale pass mean gaps). `live` with `ros_backend` `none` is a DDS-only setup: use `list_endpoints`. With `dds_backend` `none`, `dds_inactive_note` says why. `payload_decoding` is `disabled` (DDS user-topic payloads are not decoded); `dds_security` is `not_supported`. **Always succeeds**: call it first when something looks wrong. **Duration**: never waits for a lock and never runs the `ros2` CLI; it answers at once.

Takes no parameters.

list_topics

List ROS 2 topics

ROS 2 graph only; on a DDS-only setup use `list_endpoints`. List the topics on the ROS 2 graph (or the mock graph) as a `TopicListing`: each has `name`, `message_type`, `publisher_count` and `subscriber_count`; `mode_effective` tells a real graph from fixtures. QoS is not listed: use `get_topic_info`. **Empty `topics`** when the graph has none or live discovery times out. **Raises an MCP error** without a `ros2` CLI. **Duration**: ROS lock, one call at a time, ends within 45 s; `busy` if held.

Takes no parameters.

get_topic_info

Get topic info

ROS 2 graph only; on a DDS-only setup use `list_endpoints`. Describe one ROS 2 `topic` (fully qualified, e.g. `/cmd_vel`) as a `TopicInfo`: counts and, in live mode, `publisher_qos` / `subscription_qos` (`reliability`, `durability` where `transient_local` marks a latched topic such as `/tf_static`, `endpoint_count`; a `null` side says why in its `_note`) and `publisher_nodes` / `subscriber_nodes`. **Raises an MCP error** for a malformed or unknown topic, or without a `ros2` CLI. **Duration**: ROS lock, one call at a time, ends within 45 s; `busy` if held.

Parameterstopicrequired

list_nodes

List ROS 2 nodes

ROS 2 graph only; on a DDS-only setup use `list_participants`. List the nodes on the ROS 2 graph (or the mock graph) from one `ros2 node list` call as a `NodeListing`: `name`, `namespace`, `full_name`, `duplicate_count`, `lifecycle` (managed node; state not read, `lifecycle_note` if unknown), `by_namespace` counts. `duplicates` lists full names used by several nodes (a per-node tool then answers for one of them, unidentified). Nodes starting with an underscore are hidden and a node on another `ROS_DOMAIN_ID` is invisible. **Empty `nodes`** when the graph has none. Use `get_node_info` for one node's interfaces. **Raises an MCP error** without a `ros2` CLI. Cheap (two CLI calls). **Duration**: ROS lock, one call at a time, ends within 45 s; `busy` if held.

Takes no parameters.

get_node_info

Get node info

ROS 2 graph only; on a DDS-only setup use `list_participants`. Describe one ROS 2 node as a `NodeInfo`: the topics it publishes and subscribes to, the services and actions it serves or calls (each `{name, type}`), its parameters and `use_sim_time`. Interfaces come from the graph (`ros2 node info`), which never waits for the node. Parameters come from a read (`ros2 param dump`) bounded by `timeout_s`: **a node that does not answer in time is a finding, not an error**: `parameters` is `null`, `parameters_note` says its executor is probably blocked, and the interfaces are still returned. A value whose name contains password, secret, token, api_key or credential is replaced by `<masked>` and a long value is cut (`truncated`, `original_size`), both said in `parameters_note`. With `params_file`, `param_diff` lists declared values that differ on the node, are `declared_only` or `masked`. With several nodes of that name `duplicate_count` is above 1 and `note` warns. **Raises an MCP error** when the node is not on the graph (the message lists close matches), the name is malformed, or without a `ros2` CLI. **Read-only**: the only request TopicForge ever sends is a parameter read (list/get); it sends no other request to a node. **Duration**: `ros2` CLI behind the ROS lock, one call at a time; up to three CLI calls (node list and info, 8 s each at most; the parameter read, at most `timeout_s`, 20 s); ends within 45 s, less time for the read after a wait; `busy` error if the lock is held (retry).

Parametersnoderequiredtimeout_sparams_file

inspect_tf

Inspect TF tree

ROS 2 graph only. Rebuild the TF tree from the transforms on `/tf_static` (latched) and `/tf` (listened to for `window_s`) and flag what breaks lookups. Returns a `TfReport`: `frames` (parent, `kind` static/dynamic/both, `age_s`, `stale`, `observed_frequency_hz` on wall time, `sim_frequency_hz` on stamps), `roots`, counts and `findings` `{kind, frames, text}`: `stale` (a frame trails the newest `/tf` stamp by more than `stale_after_s`), `two_parents`, `disconnected` (several roots), `static_and_dynamic`, `cycle`, `no_path`. Ages and rates use header stamps, so they hold on sim time (`real_time_factor` far from 1 means sim). Give `source_frame` and `target_frame` for the `path` between them. It also reads `/robot_description` and flags URDF links with no frame (`declared_not_published`). **Empty `frames`** when nothing is published (read `note`). The listening is capped (3000 messages): `echo` can drop messages at high rates, so the tree is exact but frequencies are approximate (`truncated`). **Raises an MCP error** without a `ros2` CLI. **Mock mode**: a fixed 5 s capture of the demo robot with one stale frame. **Duration**: ROS lock, one call at a time, ends within 45 s; `busy` if held.

Parameterswindow_ssource_frametarget_framestale_after_s

sample_messages

Sample topic messages

ROS 2 graph only; on a DDS-only setup use `list_endpoints` or `peek_dds_samples` on `DCPSPublication` / `DCPSSubscription`. Collect up to `count` live messages from a ROS 2 `topic`, waiting at most `timeout_s` seconds, as a `SampleResult`. **Live mode** streams `ros2 topic echo` with the publishers' QoS (latched topics work) and returns what arrived by the deadline: `count` is the actual number and `note` says `N of M messages` and why it is short (no publisher, silent, slow or too large to print in time; a latched topic usually holds one message, so use `count` 1). Each sample has `payload` (nested named values), `timestamp_ns` (the publisher's clock, sim time on a simulation, 0 for a headerless message such as `std_msgs/String`; see `stamp_source`) and `received_ns` (wall clock when the CLI printed it). **Arrays**: arrays, strings and bytes are cut at 128 elements by default (a 541-beam `LaserScan` loses beams 128 and up); cut fields are listed in `payload._truncated_fields` and `note`. Raise `max_array_length` (up to 65536, or null) or set `arrays_summary_only`. A message over the 1 MiB cap (`TOPICFORGE_MAX_SAMPLE_BYTES`) is dropped with a `note`. **Mock mode** returns demo-robot samples instantly. **Raises an MCP error** without a `ros2` CLI, for an unknown topic, or when the CLI fails. Never publishes. **Logs**: `min_log_level` / `node` (`/rosout` only) stream until `timeout_s` (at most 2000 messages) and return the NEWEST `count` matches in stamp order; `peek_bag_samples` returns the FIRST matches. **Summary and rate**: `LaserScan`, `Odometry`, `Imu`, `Image` and `PointCloud2` samples carry a `summary` of the whole message (other types: null); `rate` gives frequency, interval spread and a `verdict` measured on `received_ns` (when the CLI printed each message). Use `count` >= 10 (50 above 20 Hz); a multi-publisher topic such as `/tf` reads `erratic` without being broken. **Duration**: `ros2` CLI behind the ROS lock, one call at a time; returns within `timeout_s

Parametersnodecounttopicrequiredtimeout_smin_log_levelmax_array_lengtharrays_summary_only

analyze_bag

Analyze bag

Summarize a ROS 2 bag at `path` as a `BagAnalysis`: storage format, duration, message count, per-topic stats and `mode_effective`. Per topic `frequency_hz` is `(n - 1) / (last - first message time)` (`frequency_basis` `topic_span`), with `first_timestamp_ns`, `last_timestamp_ns` and `latched`; a latched topic whose messages all fall within 1 s (e.g. `/tf_static`) has a null `frequency_hz`. When the bag cannot be read locally or is a `.mcap` over 200 MiB, the rate falls back to count / bag duration (`bag_duration`) and `note` says why. **Live mode** runs `ros2 bag info` on `.mcap`, `.db3` or `rosbag2_*` directories (for ROS 1 `.bag` use `peek_bag_samples`); **mock mode** returns fixture data for any bag-like path, and only mock detects anomalies. **Raises an MCP error** for a malformed or missing path (live), unparseable output, or without a `ros2` CLI. **Duration**: ROS lock, one call at a time, ends within 45 s; `busy` if held.

Parameterspathrequired

graph_diff

Diff the ROS 2 graph

ROS 2 graph only. Answer "what changed since it last worked": snapshot the graph (or `bag_path`) and compare it with `baseline`, as a `GraphDiff`. `changes` are node, topic, type, qos, rate and count differences; `added` = present now, absent from the baseline. `not_compared` names what a pair cannot support: nodes and counts need live against live, rates bag against bag, writer QoS a DDS backend. `/rosout` and `/parameter_events` count noise is only tallied in `builtin_changes_count`. The first call stores a snapshot and says so in `note`. Snapshots stay in memory (16, lost on restart). **Raises an MCP error** for an unknown id, a bad bag path or without a `ros2` CLI. **Mock mode**: the first call reports 3 changes, the next 0. **Duration**: ROS lock, one call at a time, ends within 45 s; `busy` if held.

Parametersbag_pathbaseline

diagnose

Diagnose the robot

Answer "is anything wrong": sweep the ROS 2 graph and DDS bus in one call and rank what is found as a `Diagnosis`. Checks, in order: `environment`, `nodes`, `topics`, `dds` (QoS pairs, restarts), `tf` (8 s window), `logs` (`/rosout` errors), `responsiveness` (parameter reads, `node` first); `scope` runs one group. `findings` are `critical`, `warning` or `info`, with `evidence` and, for QoS, `suggested_fixes`; `verdict` is one line. A check that errors or runs out of `budget_s` is in `checks_skipped`, never a tool error, and the verdict then says it is not verified. A clean result does not prove data flows. **Mock mode**: fixtures. **Duration**: holds the ROS lock up to `budget_s` and ends within 45 s; other ROS tools get `busy` meanwhile, `health_check` stays free.

Parametersnodescopebudget_s

list_participants

List DDS participants

List DDS participants observed on the bus as a `ParticipantListing`: `guid`, `vendor` (read from the GUID prefix, `unknown` for vendors that do not put theirs there, e.g. Dust DDS and RTI Connext; see `vendor_source`), optional `name` (the announced EntityName, e.g. `lidar_driver`; a participant with none is normal, not a fault: identify it by its topics), `hostname`, `domain_id` and `is_observer` (TopicForge's own read-only participant, listed like any other). Lifecycle: `status` (`active` / `left`), `first_seen_ns` / `last_seen_ns` (TopicForge's clock), `announced_ns` (DDS source timestamp) and, once left, `lost_ns` + `lost_time_source`: an upper bound of the death (exact after a clean shutdown; after a crash the lease expiry, so up to one lease earlier: 10 s Cyclone, 20 s Fast DDS, 100 s RTI). **Restarts**: a restarted node is a new GUID with the same name; `restarts_by_name` gives `lives` (GUIDs) and `departures` per name, so do not count GUIDs by hand (`seen_count` is not a restart count). Cyclone tracks discovery in the background; right after start the call waits up to 3 s for it. **Only the domain joined at startup is observed** (`health_check` `dds_domain_id`): a participant on another domain is INVISIBLE here, and `domain_id` does not switch domains (restart with `TOPICFORGE_DDS_DOMAIN_ID`). Works at the raw DDS layer, so non-ROS participants appear too. **Read-only by architecture**: it cannot publish, modify QoS or alter the bus. **Raises an MCP error** when no DDS module is active (install `topicforge[dds]`, set `TOPICFORGE_DDS_BACKEND=cyclone`); the mock backend returns fixtures. **Duration**: DDS lock, one call at a time, ends within 45 s; `busy` if held.

Parametersdomain_id

detect_qos_mismatches

Detect QoS mismatches

Explain why DDS readers and writers on a topic do not talk, and who will. Pairs every reader with every writer per topic and returns a `MismatchScan`: `summary` (one line, read it first), `matched` (pairs DDS will connect given the announced QoS; data flow is not observed), `reports` (incompatible or risky QoS pairs, with participant names, requested vs offered values and the failed rule in `details`), `not_matched` (pairs DDS never matches: different partitions or type names; the QoS rules are NOT evaluated for them, and `latent_incompatible_policies` lists what would also break once fixed), `hints` (probable typos in orphan topic names, type id notes), plus `pairs_checked`, `topics_scanned`, `policies_checked` and `policies_unchecked`. Checked: Partition (`*` and `?` wildcards), type name, Reliability, Durability, Deadline, Liveliness, LatencyBudget, Ownership, DestinationOrder, DataRepresentation, History (risky only, where announced). **An empty `reports` with a non-empty `not_matched` still means no data flows**, and an all-empty result does not prove the bus healthy: discovery shows the QoS DECLARED, not runtime behavior (a 'deadline missed' log with compatible QoS means the writer's real period exceeds the deadline, which TopicForge cannot see). Pass `topic` to scope the scan. The lists are capped at 200 each (`truncated`; `*_total` keep the counts). A `late_joiner` match is a VOLATILE writer whose reader joined later on the same host: normal. Reports and partition splits carry `suggested_fixes`: text for a human, never applied by TopicForge; `verify_with` names the call that confirms the repair. **Read-only by architecture**. **Raises an MCP error** when no DDS module is active; mock returns fixtures. **Duration**: DDS lock, one call at a time, ends within 45 s; `busy` if held.

Parameterstopic

peek_dds_samples

Peek DDS samples

Peek recent samples on a raw DDS topic as a `SampleResult`, like `sample_messages` but read from the DDS layer (works without ROS 2). `count` defaults to 5, clamped to 50. (a) The builtin discovery topics (`DCPSParticipant`, `DCPSSubscription`, `DCPSPublication`) return structured discovery records: on Cyclone the CURRENT state (one per live participant or endpoint), not recent events (use `participant_events`); `DCPSPublication` / `DCPSSubscription` are the raw data behind `list_endpoints`. The topic may be `/scan`, `scan` or `rt/scan`. (b) User topics: payload decoding is DISABLED on every backend, so the call returns `count` 0, empty `samples` and a `note`; that does NOT mean the topic is silent. Use `list_endpoints` for its presence, writers, readers and QoS. A user topic not announced on the bus raises an error. Right after start the call waits up to 3 s for discovery. **Read-only by architecture**: it cannot publish. **Raises an MCP error** when no DDS module is active or the topic is not announced. **Duration**: DDS lock, one call at a time, ends within 45 s; `busy` if held.

Parameterscounttopicrequired

participant_events

DDS participant events

Return DDS participant lifecycle events (`discovered` / `lost`) from a recent window, e.g. 'who left in the last 5 minutes?', as a `ParticipantEventListing`, newest first: `guid`, `event_type`, `vendor`, `timestamp_ns`, `time_source`, `observed_ns`, `age_s` (seconds before the response; live only), optional `name` (announced name), `hostname` and `domain_id`. `time_source` is `dds_source_timestamp` (timestamp of the announcement or dispose) or `observed_local` (when TopicForge noticed, weakest). **Crash caveat**: a `lost` timestamp is an upper bound of the death: exact after a clean shutdown, the lease expiry after a crash, so the process died up to one lease earlier (10 s Cyclone, 20 s Fast DDS, 100 s RTI); the two cases cannot be told apart. **Restarts**: a restarted node is a new participant (one `lost` and one `discovered`, different `guid`, same `name`); `restarts_by_name` gives `lives` and `departures` per name over the window, so do not count GUIDs by hand. Capped at 200 events (`truncated`, `note`; reduce `lookback_s`). TopicForge only knows what happened since it started watching (`health_check.observer_started_ns`). **Backends**: Cyclone polls discovery in the background every 0.5 s, so a participant cycle faster than the reader's history depth between two passes can be missed; mock returns a fixture timeline. Right after start the call waits up to 3 s for discovery. **Read-only by architecture**. **Raises an MCP error** when no DDS module is active. **Duration**: DDS lock, one call at a time, ends within 45 s; `busy` if held.

Parametersdomain_idlookback_s

topic_metrics

Topic metrics

Return temporal metrics (frequency, sequence gaps, latency percentiles) for a DDS topic over a recent window as a `TopicMetrics`. **Read `status` first**: `unsupported_user_topic` means the payload is not decoded, so no metric exists: every null or 0 is not a measurement; `no_samples_yet` is a builtin topic with nothing buffered; `ok` means computed. **Limits**: the buffer is filled only when `peek_dds_samples` runs on the topic, so `observed_frequency_hz` reflects how often it was called, not the publish rate: a coarse presence signal. `declared_frequency_hz` is `1 / deadline` of the shortest writer Deadline announced in discovery (declared, not measured; null when none). Also `sequence_gaps_count`, `latency_ns_p50/p95/p99` and availability flags. **Read-only by architecture**. **Raises an MCP error** when no DDS module is active or `window_s` is outside 1..3600. Right after start the call waits up to 3 s for discovery. **Duration**: DDS lock, one call at a time, ends within 45 s; `busy` if held.

Parameterstopicrequiredwindow_sdomain_id

peek_bag_samples

Peek bag samples

Peek up to `count` samples from a recorded bag file as a `SampleResult`: offline, for post-mortem (`peek_dds_samples` and `sample_messages` are live). Formats, by extension: MCAP (`.mcap`), rosbag2 SQLite (`.db3`), ROS 1 (`.bag`). Returns the **first** `count` messages of the topic in recording order, not the last (`sample_messages` keeps the newest log matches). `start_s` / `end_s` limit it to a bag-relative time range, empty with a `note` past the end; on `/rosout`, `min_log_level` and `node` keep the first `count` matching logs in range. **Clocks**: `timestamp_ns` is `header.stamp` when the message has a top-level header (`stamp_source` `header`), else the time in the body of `Clock`, `TFMessage` or `Log` (`payload`), else the bag record time (`recorded`); `recorded_ns` is always the record time. Each `payload` carries `_decode_status` (`full` / `partial` / `raw`). `count` defaults to 5, clamped to 50. **Uses the `rosbags` library** and needs the ROS 2 side of the runtime (an error on a DDS-only setup). Mock returns fixtures on canned paths. A bag without embedded definitions (Humble `.db3`) is decoded with those of its recorded distro, or Humble; `note` says which. Arrays over 4096 elements are cut and `note` lists them. **Read-only by architecture**. **Raises an MCP error** when the path does not exist, the topic is not in the bag, or `rosbags` is missing. **Summary and rate**: `LaserScan`, `Odometry`, `Imu`, `Image` and `PointCloud2` samples carry a `summary` of the whole message (other types: null); `rate` gives frequency, interval spread and a `verdict` measured on `recorded_ns` (the bag record time of the returned messages). Use `count` >= 10 (50 above 20 Hz); a multi-publisher topic such as `/tf` reads `erratic` without being broken. **Duration**: takes no lock (in-process file read, so it runs while a ROS 2 or DDS call is busy); the time follows the bag's size and how deep the first samples are.

Parametersnodepathrequiredcountend_stopicrequiredstart_smin_log_level

list_endpoints

List DDS endpoints

List every DDS endpoint (writer and reader) announced on the bus as an `EndpointListing`; one `EndpointInfo` each with `role`, `dds_topic` (raw, e.g. `rt/scan`), `ros_topic` (`/scan`; `null` with `ros_topic_note` for a non-ROS topic), `type_name`, `type_id`, the owning `participant_guid` with `participant_name`, and a structured `qos`. Use it instead of parsing `peek_dds_samples` and joining GUID prefixes. **Large buses**: pass `fields` `compact` to replace each `qos` by a one-line `qos_summary` and drop `type_id`, `announced_ns` and `ros_topic_note`; the default `full` is verbose, so scope with `topic` or `participant_guid` to read one topic's full QoS. **Orphans**: `by_topic` rolls endpoints up per topic with `writer_count`, `reader_count`, `orphan` (`no_reader` = a writer nobody subscribes to, `no_writer` = a reader nobody publishes to) and the union of partitions. In `qos`, a duration or policy of `None` is infinite/unset or not announced. Among EXCLUSIVE writers the live one with the highest `ownership_strength` delivers; which one owns an instance is runtime state TopicForge cannot observe. **Departed endpoints**: a leaving participant's endpoints are remembered (last 200, 1 h) and shown in `by_topic` as `departed_writers` / `departed_readers` (listed in `endpoints` only with `include_departed`), so a topic that lost its only writer is explained in one call. **Topic filter**: `rt/scan` and `scan` match each other; a filter matching nothing returns a `note` with the closest topics. **This lists discovery facts, not data flow**: it cannot tell a silent or hung writer from a healthy one; pair it with `detect_qos_mismatches`. The observer's own endpoints are excluded unless `include_observer`; ROS 2 service and action endpoints (`rq/`, `rr/`, `rs/`, `rp/`, `ra/`, `ros_discovery_info`) unless `include_internal`. Capped at 500 endpoints (`truncated`, `total`); `by_topic` covers all matches. **Raises an MCP error** when no DDS module is active; mock returns a fixture

Parameterstopicfieldsdomain_idinclude_departedinclude_internalinclude_observerparticipant_guid

Version history

13
  1. v0.9.0LatestNov 3, 2026
  2. v0.8.1Oct 26, 2026
  3. v0.8.0Oct 21, 2026
  4. v0.7.2Oct 18, 2026
  5. v0.7.1Oct 14, 2026
  6. v0.7.0Oct 12, 2026
  7. v0.6.4Oct 8, 2026
  8. v0.6.3Oct 7, 2026
  9. v0.6.2Oct 6, 2026
  10. v0.5.5Oct 2, 2026
  11. v0.5.4Oct 2, 2026
  12. v0.5.3Oct 1, 2026
  13. v0.5.2Sep 16, 2026

Score

90% of the applicable checks met

How complete this listing is, computed from the registry entry, the endpoint and public sources. Not a rating of the server and not a statement that it is safe or working today: reachability is a dated check, and no installs or reviews go into the score.

  • Reached in the last 30 days25 pts
  • Auth declared10 pts

    Declare the credential headers in the registry entry, or keep the endpoint answering the probe so its auth can be classified.

  • Tools listedRequired15 pts
  • DescriptionRequired15 pts
  • Logo10 pts
  • Documentation15 pts
  • License10 pts