Skip to content
verify mcp Beta VerifyMCP is currently in beta. If you notice any issues, get in touch and we’ll put it right.

TopicForge

PYPI · TOPICFORGE

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

Available components

+15 this week 95 Trust /100

Last scanned (20 hours ago)

Scores higher than 98% of PyPI servers

Category IoT and Hardware

Trust breakdown (7 categories)

How this component scores in each security and reliability category. Every signal is checked automatically from public evidence about the published package, including repeated runs of it in an isolated sandbox, and we only credit what we can confirm. How we score → Why this is hard to score →

Supply Chain Security100
  • No malware found by supply-chain analysis.Pass
  • No known CVEs affecting this package version or its production dependencies.Pass
  • Runs hatchling.build at install time, a recognised build step with no custom scripting around it. View diagnostics → Pass
  • 3 of 42 dependencies flagged as unhealthy. View diagnostics → Partial
Provenance & Transparency97
  • Source repository is publicly reachable at the declared URL. View diagnostics → Pass
  • Cryptographically verified build provenance (signed, bound to yaniswav/TopicForge). View diagnostics → Pass
  • Clear OSI-approved license (MIT).Pass
  • Actively maintained (last published 1 days ago).Pass
  • Security-disclosure policy not yet verified: we couldn't inspect the source repository.Unverified
Schema Quality & AI Usability74
  • 100% of prompts and resources have a non-trivial description (not blank, and not just the item's name).Pass
  • AI-judged instruction clarity (excellent).Pass
  • Context-footprint check failed: tool/resource definitions use about 7809 tokens (~459/item across 17 items; 17 tools + 0 resources), over budget; trim descriptions and params. See how to fix → Fail
  • Usage-examples check failed: none of the tools include examples. See how to fix → Fail
Stability & Change Management100
  • No destabilizing schema changes in the last 30 days.Pass
Tool Coverage100
  • 100% of tools have a non-trivial description (not blank, and not just the tool's name).Pass
  • 100% of tool parameters carry a description.Pass
  • Structured output schemas are declared (100% of tools); any adoption earns full credit.Pass
Tool Safety100
  • No prompt-injection markers were found in the server instructions, tool names or descriptions we captured.Pass
  • We read all 17 captured tool definition(s), and no name or description among them implies an irreversible operation.Pass
  • An AI judge read all 18 captured unit(s) of tool text and found none that tries to manipulate the model reading it.Pass
Capabilities100
  • Implements a current MCP spec version (2026-07-28).Pass
Changelog

Every change we have recorded for this component, newest first. Security-relevant changes are always shown. ▲ marks a change for the better, ▼ a change for the worse; unmarked changes are neutral.

  • 4 Nov 26 0
    • Security disclosure: pass → unverified ▼ functional
  • 3 Nov 26 +15
    • Malware scan: unverified → pass ▲ security
  • 2 Nov 26 0
    • Package version: 0.8.1 → 0.9.0 functional
  • 26 Oct 26 −15
    • Malware scan: pass → unverified ▼ security
    • Package version: 0.8.0 → 0.8.1 functional
  • 22 Oct 26 +15
    • Malware scan: unverified → pass ▲ security
  • 21 Oct 26 −15
    • Malware scan: pass → unverified ▼ security
    • Package version: 0.7.2 → 0.8.0 functional
  • 19 Oct 26 0
    • Stability: 0.97 → pass security
  • 18 Oct 26 0
    • Package version: 0.7.1 → 0.7.2 functional
Diagnostics

Diagnostic detail from the automated scan of this channel: what the scanner observed at each step, so you can see exactly where a check passed or failed. It is informational only and never changes the trust score.

Captured 4 Nov 2026 · Analysed pypi/topicforge@0.9.0

Provenance Verified

A signed build attestation was found and verified, binding this exact artifact to the source repository it claims to come from.

Result Verified
Ecosystem pypi
Reason Verified
Discovered via Registry attestation endpoint
Source repo yaniswav/TopicForge
Certificate issuer https://token.actions.githubusercontent.com
Certificate SAN https://github.com/yaniswav/TopicForge/.github/workflows/publish.yml@refs/tags/v0.9.0
Rekor log index 3642347848
Predicate type PyPI publish attestation https://docs.pypi.org/attestations/publish/v1
Subject digest sha256:87f9598f43fd2fb3978809f2539c7a7f98a14f7d4c1e9713d557b3b8aee709b7

Background: How many MCP packages publish verified provenance →

Install scripts 1 script
Hook Tier Command
build_backend allowlisted hatchling.build

Background: Why install scripts are a supply-chain risk →

Dependencies 42 packages
Packages resolved 42
Stale 2
No linked repository 1
Tree resolution Complete

Background: SBOMs and build attestations, explained →

MCP tools · 17 exposed · ~7,580 tokens

The tools this component advertises to a client, with an estimated token cost for each. Expand a tool to see its parameters and schema. The per-tool counts are indicative and are not scored directly; the schema's total context footprint is one signal in Schema Quality & AI Usability. A tool's description is untrusted text the model reads on every call, which is what makes this list a security surface and not just an inventory: how tool poisoning works →

Tool Tokens
analyze_bag ~344

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.

NameTypeReqDescription
pathstringyesBag path: a `.mcap` or `.db3` file or a `rosbag2_*` directory (`peek_bag_samples` also reads ROS 1 `.bag`; `analyze_bag` does not). Existence and format are checked live; mock accepts any well-formed…
NameTypeReqDescription
anomaliesarray–Notes on gaps or clock jumps; mock mode only.
bag_formatstring|null–Detected container: `mcap`, `db3`, `bag` or `unknown`; `None` for `ros2 bag info` text.
duration_snumberyesBag duration in seconds.
message_countintegeryesTotal number of messages across all recorded topics.
mode_effectivestringyes`live` or `mock` (fixtures).
notestring|null–Why the result is less detailed than usual (e.g. rates over the whole bag duration); else `None`.
pathstringyesBag path as supplied.
start_nsinteger|null–Record ns of the bag's first message (rosbag2 `starting_time`), the zero of `start_s`/`end_s`; `null` if unreadable.
storage_formatstring|null–`mcap`, `sqlite3`, or other storage identifier when known.
topicsarrayyesPer-topic statistics for every topic present in the bag.

No examples provided.

detect_qos_mismatches ~472

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.

NameTypeReqDescription
topic––Optional DDS topic to scope the scan to: `scan` or `rt/scan`. Omit to scan all topics.
NameTypeReqDescription
hintsarrayyesLeads that are not findings: probable typos, type id notes, incomplete checks.
matchedarray–Pairs DDS will connect given the announced QoS (data flow is not observed).
matched_totalinteger–Matched pairs before the size cap.
mode_effectivestringyes`live` or `mock` (fixtures).
not_matchedarrayyesPairs DDS never matches (partition or type name). Non-empty with empty `reports` is NOT all-clear: no data flows.
not_matched_totalinteger–Not-matched pairs before the size cap.
notestring|null–One sentence of context, or `None`.
pairs_checkedintegeryesSame-topic (reader, writer) pairs examined.
policies_checkedarrayyesPolicies compared on every pair.
policies_uncheckedarrayyesWhat the scan does not cover, each with a reason; a clean result says nothing about them.
reportsarrayyesPairs sharing partition and type with incompatible or risky QoS.
reports_totalinteger–Reports before the size cap.
summarystring|null–One-line reading of the scan: counts of incompatible, not matched and matched pairs.
topics_scannedintegeryesTopics that had at least one endpoint in scope.
truncatedboolean–True when `reports`, `matched` or `not_matched` hit the 200-entry cap (see `*_total`); narrow with `topic`.

No examples provided.

diagnose ~288

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.

NameTypeReqDescription
budget_snumber–Seconds for the sweep, 10..40, default 30.
node––Node read first (6 s); a failed read is critical.
scopestring–`all`, or one group: `graph`, `tf`, `dds`, `logs`.
NameTypeReqDescription
budget_snumberyes–
checks_runarrayyes–
checks_skippedarrayyesNot verified.
critical_countintegeryesCounts cover all findings, not the capped list.
elapsed_snumberyes–
findingsarrayyesCritical first; at most 50.
findings_totalintegeryes–
info_countintegeryes–
mode_effectivestringyes`live` or `mock` (fixtures).
notestring|null–One sentence of context, or `None`.
verdictstringyesOne line; never an all-clear after a skipped check.
warning_countintegeryes–

No examples provided.

get_node_info ~485

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

NameTypeReqDescription
nodestringyesFully qualified node name as `list_nodes` gives it, e.g. `/robot1/lidar_driver` (a missing leading `/` is added).
params_file––Server path of a ROS 2 parameter `.yaml` (1 MiB max).
timeout_snumber–Seconds to wait for the parameter read, 1..20, default 8. A node that does not answer is reported in `parameters_note`, not as an error.
NameTypeReqDescription
action_clientsarrayyesActions the node calls.
action_serversarrayyesActions the node serves.
duplicate_countintegeryesNodes using this exact full name; above 1, the data above belongs to one of them.
full_namestringyesFully qualified node name, e.g. `/lidar_driver`.
lifecycleboolean–True when `service_servers` has `<full_name>/get_state` (GetState).
mode_effectivestringyes`live` or `mock` (fixtures).
notestring|null–One sentence of context, or `None`.
param_diffarray|null–File against node: differences only.
param_diff_notestring|null––
param_diff_summarystring|null–Left out of `param_diff`: same, live_only, with names.
parametersarray|null–Parameters sorted by name; `None` when unreadable (see `parameters_note`); an empty list means none.
parameters_notestring|null–Why `parameters` is `None`, or what was masked or cut; `None` when complete.
publishersarrayyesTopics the node publishes.
service_clientsarrayyesServices the node calls.
service_serversarrayyesServices the node offers, its own parameter services included.
subscribersarrayyesTopics the node subscribes to.
use_sim_timeboolean|null–Value of the node's `use_sim_time` parameter. `None` when unknown: see `use_sim_time_note`.
use_sim_time_notestring|null–Why `use_sim_time` is `None`. `None` when it is set.

No examples provided.

get_topic_info ~220

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.

NameTypeReqDescription
topicstringyesFully qualified ROS 2 topic name starting with `/`, e.g. `/cmd_vel`. Segments are letters, digits and underscores (not starting with a digit); anything else is rejected before reaching the `ros2` CLI.
NameTypeReqDescription
message_typestringyes–
mode_effectivestringyes`live` or `mock` (fixtures).
namestringyes–
notestring|null–One sentence of context, or `None`.
publisher_countintegeryes–
publisher_nodesarray–Publisher node names (`ros2 topic info --verbose`); empty when unknown.
publisher_qos––QoS of the publishers. `None` when it was not read: see `publisher_qos_note`.
publisher_qos_notestring|null–Why `publisher_qos` is `None`. `None` when it is set.
subscriber_countintegeryes–
subscriber_nodesarray–Subscriber node names; empty when unknown.
subscription_qos––QoS of the subscriptions. `None` when it was not read: see `subscription_qos_note`.
subscription_qos_notestring|null–Why `subscription_qos` is `None`. `None` when it is set.

No examples provided.

graph_diff ~262

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.

NameTypeReqDescription
bag_path––Snapshot this bag instead of the live graph.
baselinestring–`previous` (last snapshot), a `snap-N` id from `snapshot_ids`, or a bag path.
NameTypeReqDescription
added_countintegeryes–
baseline––`null` without baseline.
baseline_idstring|null–`null` without baseline (`note`).
builtin_changes_countintegeryesCount changes on `/rosout`, `/parameter_events`: not in `changes`.
changed_countintegeryes–
changesarrayyes–
current–yes–
mode_effectivestringyes`live` or `mock` (fixtures).
not_comparedobjectyesSkipped comparison and why.
notestring|null–One sentence of context, or `None`.
removed_countintegeryes–
snapshot_idstringyesStored snapshot id.
snapshot_idsarrayyesHeld ids, oldest first.

No examples provided.

health_check ~223

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.

Input schema present but exposes no named parameters.

NameTypeReqDescription
contract_versioninteger–Output contract version; changes when a field is renamed, removed or retyped.
dds_backendstring–DDS backend serving: `mock`, `cyclone` or `none` (DDS module inactive); `fast` is retired.
dds_domain_idinteger|null–DDS domain id observed when the DDS module is active.
dds_inactive_notestring|null–Why `dds_backend` is `none` while ROS 2 serves; else `null`.
dds_securitystring–DDS Security is not handled: protected endpoints and data are invisible.
max_sample_countintegeryesServer cap on samples per call; larger requests are clamped.
middleware_availableboolean–True when a DDS backend serves; else whether the configured binding is importable.
modestringyesMode serving: `mock` or `live` (`live` + `ros_backend` `none`: DDS tools only); may differ from `requested_mode`.
notestring|null–One sentence of context, or `None`.
now_nsinteger|null–Server wall-clock time (ns since epoch) when this report was built.
observed_domain_notestring|null–Which DDS domain is observed: only the one joined at startup; others are invisible.
observer_started_nsinteger|null–ns when the DDS observer joined; nothing earlier was watched. `None` without one.
payload_decodingstring–`disabled`: DDS user-topic payloads are not decoded.
payload_decoding_notestring|null–Reason for `payload_decoding`.
requested_modestringyesMode requested by configuration (may be `auto`).
rmw_implementationstring|null–ROS 2 middleware, e.g. `rmw_fastrtps_cpp`; `None` when `rmw_source` is `none`.
rmw_sourcestring–`env`, `ros2_cli`, `distro_default` (not verified on the graph) or `none`.
ros2_availablebooleanyesWhether a `ros2` CLI is on PATH.
ros2_distrostring|null–`ROS_DISTRO` if set (disclosed by design under the local-trust model).
ros_backendstring–ROS 2 backend: `ros2_cli`, `mock` or `none`.
ros_security_enabledboolean|null–`ROS_SECURITY_ENABLE` is `true`; `None` unless the ros2 CLI serves.
ros_security_keystore_setboolean|null–Whether a keystore variable is set (the path is never shown).
ros_security_notestring|null–Set when `ros_security_enabled`: what stays invisible.
ros_security_strategystring|null–`ROS_SECURITY_STRATEGY` when set to a valid value.
ros_tools_availableboolean–True when the ROS 2 tools can run (`ros_backend` is not `none`); on a DDS-only setup use `list_endpoints`.
server_versionstringyesTopicForge version.
sim_clock_publishedboolean|null–Whether `/clock` had a publisher at the last graph read (stamps may be sim time); `None` in mock or unread.
tracker_cache_evictionsinteger|null–Discovery entries dropped from a full tracker cache; non-zero means the bus is bigger than listed.
tracker_errorsinteger|null–Tracker passes that raised; non-zero means gaps.
tracker_last_pass_nsinteger|null–ns of the last tracker pass; far older than `now_ns` means stale lifecycle.
tracker_passesinteger|null–Completed discovery tracker passes since start.
tracker_runningboolean|null–Whether the discovery tracker thread is alive (Cyclone); `None` without a tracker.

No examples provided.

inspect_tf ~431

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.

NameTypeReqDescription
source_frame––Frame name, e.g. `base_link`. Give `source_frame` and `target_frame` together to get the chain between them in `path`.
stale_after_snumber–A frame is stale past this many seconds of stamp time, default 2.
target_frame––See `source_frame`.
window_snumber–Seconds to listen to `/tf`, 1..20, default 5.
NameTypeReqDescription
declared_frames_missingarray–URDF links with no TF frame.
dynamic_countintegeryesFrames with a dynamic transform.
findingsarrayyesEmpty when the tree is sound.
frame_countintegeryes–
framesarrayyesSorted by name.
frames_not_in_urdfarray–TF frames not in the URDF (`map`, `odom` are normal).
hintsarrayyes–
message_countintegeryesTransforms captured, repeats included.
mode_effectivestringyes`live` or `mock` (fixtures).
notestring|null–One sentence of context, or `None`.
patharray|null–Frames from `source_frame` to `target_frame`.
path_notestring|null–Why `path` is `None`, or context.
real_time_factornumber|nullyesStamp time over wall time: about 1 on a real robot, other on sim.
reference_nsinteger|nullyesNewest `/tf` stamp: the clock for `age_s`.
rootsarrayyesFrames with no parent; one in a sound tree.
stale_countintegeryes–
stamp_span_snumber|nullyes–
static_countintegeryesFrames with a static transform.
transform_countintegeryesDistinct parent-child pairs.
truncatedbooleanyesMessages were dropped or the cap was hit.
urdf_link_countinteger|null––
urdf_notestring|null––
urdf_sourcestring––
window_actual_snumberyes–
window_snumberyes–

No examples provided.

list_endpoints ~841

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 fixtur…

NameTypeReqDescription
domain_idinteger–Ignored: it does not switch domains and is only range-checked (0..232). The domain observed is the one joined at startup (`TOPICFORGE_DDS_DOMAIN_ID`), reported in the response `domain_id`; `note` fla…
fieldsstring–`full` (default) or `compact`: one-line `qos_summary` instead of the full `qos`, no `type_id`, `announced_ns` or `ros_topic_note`. Use `compact` on large buses.
include_departedboolean–Also list endpoints whose participant left (flagged with `gone_ns`); `by_topic` reports them either way. Default false.
include_internalboolean–Also list ROS 2 service and action endpoints (`rq/`, `rr/`, `rs/`, `rp/`, `ra/`, `ros_discovery_info`), most of a small robot's endpoints. Default false.
include_observerboolean–Include TopicForge's own observer endpoints. Default false.
participant_guid––Only endpoints of this participant (a guid from `list_participants`). Omit for all.
topic––Only endpoints on this DDS topic: `scan` or `rt/scan` (exact name first, then the alternate form). Omit for every topic.
NameTypeReqDescription
by_topicarrayyesRoll-up over every matching endpoint.
departed_endpoint_countinteger–Departed endpoints matching the filters; listed in `endpoints` only with `include_departed`.
domain_idintegeryes–
endpointsarrayyesMatching endpoints, capped (see `truncated`).
excluded_observer_endpoint_countinteger–TopicForge's own endpoints left out of `endpoints` (counted in `total`).
fieldsstring–Detail level served: `compact` drops per-endpoint QoS detail, type id and notes.
hidden_internal_endpoint_countinteger–Service/action endpoints (`rq/`, `rr/`, `rs/`, `rp/`, `ra/`, `ros_discovery_info`) hidden unless `include_internal`.
hintsarray–Facts true of every endpoint, stated once (e.g. liveness is not observed).
mode_effectivestringyes`live` or `mock` (fixtures).
notestring|null–Set when a `topic` filter matched nothing: the closest known topics.
observer_guidstring|null–GUID of TopicForge's own participant, `None` in mock.
returnedintegeryes–
snapshot_nsintegeryesWall-clock time of the snapshot, ns since epoch.
totalintegeryesEndpoints in the discovery cache before any filter.
truncatedbooleanyesTrue when matching endpoints exceeded the cap.

No examples provided.

list_nodes ~208

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.

Input schema present but exposes no named parameters.

NameTypeReqDescription
by_namespaceobject–Distinct full names per namespace (`/` is the root), sorted by namespace.
duplicatesarrayyesFull names that appear more than once on the graph. Empty when every name is unique.
lifecycle_countinteger–Nodes with `lifecycle` true.
lifecycle_notestring|null–Why `lifecycle` could not be determined (service list failed), else `None`.
mode_effectivestringyes`live` or `mock` (fixtures).
nodesarrayyesNodes on the graph, one entry per distinct full name, sorted by name.
notestring|null–One sentence of context, or `None`.
returnedintegeryesLength of `nodes`.
totalintegeryesDistinct node names on the graph (no cap applies).
truncatedbooleanyesTrue when `returned` is less than `total` because of a cap.

No examples provided.

list_participants ~523

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.

NameTypeReqDescription
domain_idinteger–Ignored: it does not switch domains and is only range-checked (0..232). The domain observed is the one joined at startup (`TOPICFORGE_DDS_DOMAIN_ID`), reported in the response `domain_id`; `note` fla…
NameTypeReqDescription
domain_idintegeryesDDS domain observed (joined at startup).
mode_effectivestringyes`live` or `mock` (fixtures).
notestring|null–One sentence of context, or `None`.
participantsarrayyes–
restarts_by_nameobject–Names seen with several GUIDs or lost at least once (absent: ran once). A restart is a new GUID, same name.
restarts_by_nodeobject–`restarts_by_name` keyed by ROS 2 node full name.
restarts_notestring|null–Set when unnamed participants were left out of `restarts_by_name`.
returnedintegeryes–
totalintegeryes–
truncatedbooleanyes–

No examples provided.

list_topics ~144

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.

Input schema present but exposes no named parameters.

NameTypeReqDescription
mode_effectivestringyes`live` or `mock` (fixtures).
notestring|null–One sentence of context, or `None`.
returnedintegeryes–
topicsarrayyes–
totalintegeryes–
truncatedbooleanyes–

No examples provided.

participant_events ~500

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.

NameTypeReqDescription
domain_idinteger–Ignored: it does not switch domains and is only range-checked (0..232). The domain observed is the one joined at startup (`TOPICFORGE_DDS_DOMAIN_ID`), reported in the response `domain_id`; `note` fla…
lookback_sinteger–Window in seconds, 1..86400, default 300. A larger window may hit the 200-event cap.
NameTypeReqDescription
domain_idintegeryesDDS domain observed (joined at startup).
eventsarrayyes–
mode_effectivestringyes`live` or `mock` (fixtures).
notestring|null–One sentence of context, or `None`.
restarts_by_nameobject–Names seen with several GUIDs or lost at least once (absent: ran once). A restart is a new GUID, same name. Window only.
restarts_notestring|null–Set when unnamed participants were left out of `restarts_by_name`.
returnedintegeryes–
totalintegeryes–
truncatedbooleanyesTrue when cut to the 200-event cap.

No examples provided.

peek_bag_samples ~863

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.

NameTypeReqDescription
countinteger–Messages to return, default 5, at most 50 (`health_check.max_sample_count`): more is capped to 50, negative raises an error. The result's `count` may be lower.
end_s––Only messages recorded before this bag-relative second (exclusive), default to the end.
min_log_level––`/rosout` (`rcl_interfaces/msg/Log`) only: keep messages at or above this level (debug 10, info 20, warn 30, error 40, fatal 50). Other topics: error.
node––`/rosout` only: keep logs of this node and its child loggers, e.g. `/robot1/driver` (also `driver`, `robot1.driver`). Exact name, never a prefix. Other topics: error.
pathstringyesBag path: a `.mcap` or `.db3` file or a `rosbag2_*` directory (`peek_bag_samples` also reads ROS 1 `.bag`; `analyze_bag` does not). Existence and format are checked live; mock accepts any well-formed…
start_s––Only messages recorded at or after this many seconds from the bag's first message (`analyze_bag.start_ns`), default from the start.
topicstringyesFully qualified ROS 2 topic name starting with `/`, e.g. `/cmd_vel`. Segments are letters, digits and underscores (not starting with a digit); anything else is rejected before reaching the `ros2` CLI.
NameTypeReqDescription
countintegeryesSamples in `samples`: fewer than requested when the topic gave fewer; a request above 50 is capped to 50.
mode_effectivestringyes`live` or `mock` (fixtures).
notestring|null–Why `samples` is empty or limited, when not obvious; else `None`.
rate––How the topic delivered the messages, with a verdict; `null` for `peek_dds_samples`. Use `count` >= 10.
samplesarrayyesThe samples, in backend order.
topicstringyes–

No examples provided.

peek_dds_samples ~392

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.

NameTypeReqDescription
countinteger–Messages to return, default 5, at most 50 (`health_check.max_sample_count`): more is capped to 50, negative raises an error. The result's `count` may be lower.
topicstringyesDDS topic name: bare (`scan`), ROS 2 mangled (`rt/scan`) or a builtin discovery topic (`DCPSParticipant`, `DCPSSubscription`, `DCPSPublication`). Letters, digits, `_`, `/` and `::` only.
NameTypeReqDescription
countintegeryesSamples in `samples`: fewer than requested when the topic gave fewer; a request above 50 is capped to 50.
mode_effectivestringyes`live` or `mock` (fixtures).
notestring|null–Why `samples` is empty or limited, when not obvious; else `None`.
rate––How the topic delivered the messages, with a verdict; `null` for `peek_dds_samples`. Use `count` >= 10.
samplesarrayyesThe samples, in backend order.
topicstringyes–

No examples provided.

sample_messages ~979

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_…

NameTypeReqDescription
arrays_summary_onlyboolean–When true, arrays become a type-and-length summary: use it to read the non-array fields of large messages. Default false.
countinteger–Messages to wait for, default 5, at most 50 (more is capped and `note` says so); 0 returns nothing. Fewer arrive when the topic is slow or `timeout_s` runs out.
max_array_length––Longest array, string or bytes value returned in full, 1..65536, default 128 (a 541-beam `LaserScan` is cut to 128 ranges; its `summary` still covers all). Longer values are cut and listed in `_trunc…
min_log_level––`/rosout` (`rcl_interfaces/msg/Log`) only: keep messages at or above this level (debug 10, info 20, warn 30, error 40, fatal 50). Other topics: error.
node––`/rosout` only: keep logs of this node and its child loggers, e.g. `/robot1/driver` (also `driver`, `robot1.driver`). Exact name, never a prefix. Other topics: error.
timeout_snumber–Seconds to wait, 1..40, default 10, covering topic lookup and CLI start-up. The call returns within `timeout_s` plus a few seconds, with what arrived and a `note`. Raise it for a topic slower than 1…
topicstringyesFully qualified ROS 2 topic name starting with `/`, e.g. `/cmd_vel`. Segments are letters, digits and underscores (not starting with a digit); anything else is rejected before reaching the `ros2` CLI.
NameTypeReqDescription
countintegeryesSamples in `samples`: fewer than requested when the topic gave fewer; a request above 50 is capped to 50.
mode_effectivestringyes`live` or `mock` (fixtures).
notestring|null–Why `samples` is empty or limited, when not obvious; else `None`.
rate––How the topic delivered the messages, with a verdict; `null` for `peek_dds_samples`. Use `count` >= 10.
samplesarrayyesThe samples, in backend order.
topicstringyes–

No examples provided.

topic_metrics ~405

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.

NameTypeReqDescription
domain_idinteger–Ignored: it does not switch domains and is only range-checked (0..232). The domain observed is the one joined at startup (`TOPICFORGE_DDS_DOMAIN_ID`), reported in the response `domain_id`; `note` fla…
topicstringyesDDS topic name: bare (`scan`), ROS 2 mangled (`rt/scan`) or a builtin discovery topic (`DCPSParticipant`, `DCPSSubscription`, `DCPSPublication`). Letters, digits, `_`, `/` and `::` only.
window_sinteger–Window in seconds, 1..3600, default 60.
NameTypeReqDescription
declared_frequency_hznumber|null–`1 / deadline` of the shortest announced writer Deadline: promised, not observed; `None` when none announced.
latency_availableboolean–True when a sample carried both a publish and a receive timestamp.
latency_ns_p50integer|null–Median publish-to-receive latency in ns; `None` unless `latency_available`.
latency_ns_p95integer|null–95th-percentile publish-to-receive latency (ns).
latency_ns_p99integer|null–99th-percentile publish-to-receive latency (ns).
mode_effectivestringyes`live` or `mock` (fixtures).
notestring|null–One sentence of context, or `None`.
observed_frequency_hznumber|null–`samples_observed / window_actual_s`; `None` under 2 samples.
samples_observedintegeryesSamples buffered in the window. `0` does NOT mean no publisher, only that no `peek_dds_samples` call captured one.
sequence_gaps_countinteger–Missing sequence numbers in the buffered samples; `0` also when none exist (see `sequence_numbers_available`).
sequence_numbers_availableboolean–True when a sequence number could be read from a sample.
statusstring–`ok`; `no_samples_yet` (empty buffer); `unsupported_user_topic` (payload not decoded: null fields are not measurements).
topicstringyes–
window_actual_snumberyesSeconds actually covered; shorter than `window_s` after a recent start; `0.0` without samples.
window_sintegeryesRequested window in seconds (echoed).

No examples provided.

Install

How do I install the TopicForge MCP server?

TopicForge runs locally as a PyPI package, launched with uvx topicforge. Ready-made configuration for Claude, Cursor, VS Code, Codex and 5 more is on this page, copied from each client's own documentation.

pypi · topicforge

# add to Claude Code
claude mcp add yaniswav-topicforge -- uvx topicforge
Common questions

What is the TopicForge MCP server?

TopicForge is an MCP server listed in the public MCP registry as io.github.yaniswav/topicforge. Read-only MCP server for ROS2 and DDS: topics, participants, QoS mismatches, bags. This page covers its PyPI package (topicforge).

Is the TopicForge MCP server safe to use?

TopicForge scores 95 out of 100 on VerifyMCP. We found no known CVEs affecting it as of 4 November 2026. Its build provenance is signed and verified. That is a record of what we were able to check automatically, not an endorsement. The category breakdown on this page shows every signal behind the number, including the ones we could not confirm.

What tools does the TopicForge MCP server expose?

TopicForge exposes 17 tools: health_check, list_topics, get_topic_info, list_nodes, get_node_info, and 12 more. Their descriptions and schemas cost roughly 7,580 tokens of context every time the server is loaded.

Is the TopicForge MCP server still maintained?

TopicForge is still listed as active in the MCP registry. We last reached this channel on 4 November 2026. Those dates come from our own scans of the registry and the channel itself, not from anything the publisher announced.

What licence is the TopicForge MCP server under?

TopicForge declares the MIT licence, which is OSI-approved. That covers the source only, and says nothing about the cost of any service it calls.