Skip to main content
Glama

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 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), 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.

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:

{
  "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.

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.

Related MCP server: ros2-mcp

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.

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

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

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

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

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

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

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.

  • 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.

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

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/.

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

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.

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: 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. Integration or support work for a specific ROS 2 / DDS setup: ethvignot.yanis@gmail.com.

Available Tools

18 tools
analyze_bagA
Read-onlyIdempotent

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 the bounded scan (8 s) is cut, the rate falls back to count / bag duration (bag_duration) and note says why. anomalies lists observed clock steps, silences of steady topics and TF edges, and backward header stamps (topic, time offset, size, no cause); empty means none found by these checks, not clean, and note says if the scan was cut or skipped. Live mode runs ros2 bag info, or rosbags without ros2 or when it fails (reader says which); mock mode returns fixtures for any bag-like path. Raises an MCP error for a missing path, a directory without metadata.yaml or an unreadable bag. Duration: ROS lock, one call at a time, ends within 45 s; busy if held.

Inputs: path (required). Returns: Structured summary of a ROS2 bag, from `ros2 bag info`, `rosbags` or a mock fix…

detect_qos_mismatchesA
Read-onlyIdempotent

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.

Inputs: topic. Returns: Result of `detect_qos_mismatches`: findings plus what was and was not checked.

diagnoseA
Read-onlyIdempotent

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, params_file diff); scope runs one group. findings are critical, warning or info, with evidence and suggested_fixes (QoS changes, else read-only commands); 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.

Inputs: node, scope, budget_s, params_file. Returns: note, verdict, budget_s, findings, +8 more

get_imageA
Read-onlyIdempotent

One live frame as image content plus JSON metadata. CompressedImage passes through; raw needs topicforge[image]. Needs the sampling helper. Big best-effort frames can drop. Mock: a fixed gradient.

Inputs: topic (required), max_edge.

get_node_infoA
Read-onlyIdempotent

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

Inputs: node (required), timeout_s, params_file, param_diff_only. Returns: Detail of one node: its interfaces, its parameters and `use_sim_time`.

get_topic_infoA
Read-onlyIdempotent

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.

Inputs: topic (required). Returns: Detail of a single ROS2 topic: counts, QoS per side and the nodes on each side.

graph_diffA
Read-onlyIdempotent

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.

Inputs: bag_path, baseline. Returns: note, changes, current, baseline, +9 more

health_checkA
Read-onlyIdempotent

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.

Inputs: no named parameters. Returns: Result of `health_check`. Always succeeds, even when the host is unhealthy.

inspect_tfA
Read-onlyIdempotent

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). A slow or intermittent publisher needs a longer window_s (diagnose uses 8 s). 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) and joints on the wrong stream (fixed_joint_on_tf, movable_joint_on_tf_static). 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.

Inputs: window_s, urdf_path, source_frame, target_frame, +1 more. Returns: The TF tree as captured: frames, roots, findings and an optional path.

list_endpointsA
Read-onlyIdempotent

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. Duration: DDS lock, one call at a time, ends within 45 s; busy if held.

Inputs: topic, fields, domain_id, include_departed, +3 more. Returns: note, hints, total, fields, +11 more

list_nodesA
Read-onlyIdempotent

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.

Inputs: no named parameters. Returns: note, nodes, total, returned, +6 more

list_participantsA
Read-onlyIdempotent

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.

Inputs: domain_id. Returns: note, total, returned, domain_id, +6 more

list_topicsA
Read-onlyIdempotent

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. counts_unknown: true = counts 0/0 are unknown, not an orphan. 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.

Inputs: no named parameters. Returns: note, total, topics, returned, +2 more

participant_eventsA
Read-onlyIdempotent

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.

Inputs: domain_id, lookback_s. Returns: note, total, events, returned, +5 more

peek_bag_samplesA
Read-onlyIdempotent

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, PointCloud2 and GoalStatusArray (goals per status) 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.

Inputs: path (required), topic (required), node, count, +3 more. Returns: Samples returned by `sample_messages`, `peek_dds_samples` and `peek_bag_samples…

peek_dds_samplesA
Read-onlyIdempotent

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: decoding is off by default (health_check dds_decode): count 0, empty samples, a note; that does NOT mean the topic is silent. Use list_endpoints for its presence, writers, readers and QoS. When on, ROS 2 types are read (bounded, note if refused). 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.

Inputs: topic (required), count. Returns: Samples returned by `sample_messages`, `peek_dds_samples` and `peek_bag_samples…

sample_messagesA
Read-onlyIdempotent

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 or its helper 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 at arrival). 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, PointCloud2 and GoalStatusArray (goals per status) samples carry a summary of the whole message (other types: null); rate gives frequency, interval spread and a verdict measured on received_ns (when each message arrived). 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 (at most 40) plus about 3 s to stop the CLI and decode; ends within 45 s, less time to collect after a wait; busy error if the lock is held (retry).

Inputs: topic (required), node, count, window_s, +4 more. Returns: Samples returned by `sample_messages`, `peek_dds_samples` and `peek_bag_samples…

topic_metricsA
Read-onlyIdempotent

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 too little buffered or read. Limits: frequency_basis reader_arrivals (decoding on) is a real publish rate from a bounded read of up to 10 s; otherwise the buffer fills only when peek_dds_samples runs, so observed_frequency_hz is how often it was called. 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.

Inputs: topic (required), window_s, domain_id. Returns: Temporal metrics for a single DDS topic over a recent window.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    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

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      A
      quality
      D
      maintenance
      Read-only Modbus TCP monitoring server that exposes safe MCP tools for AI agents to read holding/input registers, coils, and device identity from industrial devices without write access.
      4
      Apache 2.0
    • A
      license
      Not graded
      quality
      D
      maintenance
      Read-only MCP server that exposes Autopsy digital forensics case data as tools, enabling LLM clients like Cline to browse filesystems, query artifacts, and search keywords without modifying the case.
      1
      MIT
    • A
      license
      Not graded
      quality
      D
      maintenance
      A read-only MCP server for bounded semantic inspection of ROS 2 perception systems, enabling discovery and metadata extraction from sensors such as cameras, depth sensors, and LiDARs without device configuration or actuation.
      Apache 2.0