topicforge 0.5.6__tar.gz → 0.6.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. {topicforge-0.5.6 → topicforge-0.6.1}/CHANGELOG.md +95 -1
  2. {topicforge-0.5.6 → topicforge-0.6.1}/PKG-INFO +10 -7
  3. {topicforge-0.5.6 → topicforge-0.6.1}/README.md +8 -6
  4. {topicforge-0.5.6 → topicforge-0.6.1}/docs/TESTING.md +3 -3
  5. {topicforge-0.5.6 → topicforge-0.6.1}/docs/TROUBLESHOOTING.md +2 -2
  6. topicforge-0.6.1/docs/VALIDATION.md +15 -0
  7. topicforge-0.6.1/plugin/LICENSE +21 -0
  8. topicforge-0.6.1/plugin/README.md +60 -0
  9. {topicforge-0.5.6 → topicforge-0.6.1}/pyproject.toml +2 -1
  10. {topicforge-0.5.6 → topicforge-0.6.1}/src/topicforge/__init__.py +1 -1
  11. {topicforge-0.5.6 → topicforge-0.6.1}/src/topicforge/adapters/base.py +3 -3
  12. {topicforge-0.5.6 → topicforge-0.6.1}/src/topicforge/adapters/common/cdr_decoder.py +11 -1
  13. {topicforge-0.5.6 → topicforge-0.6.1}/src/topicforge/adapters/composite.py +9 -3
  14. {topicforge-0.5.6 → topicforge-0.6.1}/src/topicforge/adapters/dds_cyclone/adapter.py +3 -2
  15. {topicforge-0.5.6 → topicforge-0.6.1}/src/topicforge/adapters/dds_dust/adapter.py +3 -3
  16. {topicforge-0.5.6 → topicforge-0.6.1}/src/topicforge/adapters/dds_fast/adapter.py +3 -2
  17. {topicforge-0.5.6 → topicforge-0.6.1}/src/topicforge/adapters/dds_opendds/adapter.py +3 -3
  18. {topicforge-0.5.6 → topicforge-0.6.1}/src/topicforge/adapters/ros2_live/adapter.py +203 -156
  19. topicforge-0.6.1/src/topicforge/adapters/ros2_live/echo_parser.py +165 -0
  20. topicforge-0.6.1/src/topicforge/adapters/ros2_live/echo_stream.py +195 -0
  21. topicforge-0.6.1/src/topicforge/adapters/ros2_live/process_tree.py +193 -0
  22. {topicforge-0.5.6 → topicforge-0.6.1}/src/topicforge/adapters/ros2_mock/adapter.py +11 -5
  23. {topicforge-0.5.6 → topicforge-0.6.1}/src/topicforge/adapters/ros2_mock/fixtures.py +28 -10
  24. {topicforge-0.5.6 → topicforge-0.6.1}/src/topicforge/constants.py +12 -0
  25. {topicforge-0.5.6 → topicforge-0.6.1}/src/topicforge/models/schemas.py +62 -19
  26. {topicforge-0.5.6 → topicforge-0.6.1}/src/topicforge/services/bag_service.py +17 -5
  27. {topicforge-0.5.6 → topicforge-0.6.1}/src/topicforge/services/factory.py +3 -1
  28. {topicforge-0.5.6 → topicforge-0.6.1}/src/topicforge/services/health.py +16 -0
  29. {topicforge-0.5.6 → topicforge-0.6.1}/src/topicforge/services/inspector.py +28 -24
  30. {topicforge-0.5.6 → topicforge-0.6.1}/src/topicforge/tools/handlers.py +81 -36
  31. topicforge-0.6.1/tests/fixtures/ros2_echo/image_noarr.yaml +24 -0
  32. topicforge-0.6.1/tests/fixtures/ros2_echo/image_trunc4.yaml +17 -0
  33. topicforge-0.6.1/tests/fixtures/ros2_echo/scan_edge.yaml +38 -0
  34. topicforge-0.6.1/tests/fixtures/ros2_echo/scan_noarr.yaml +30 -0
  35. topicforge-0.6.1/tests/fixtures/ros2_echo/scan_trunc128.yaml +273 -0
  36. topicforge-0.6.1/tests/fixtures/ros2_echo/scan_trunc3.yaml +46 -0
  37. topicforge-0.6.1/tests/fixtures/ros2_echo/string_latched.yaml +2 -0
  38. topicforge-0.6.1/tests/fixtures/ros2_echo/twist.yaml +18 -0
  39. {topicforge-0.5.6 → topicforge-0.6.1}/tests/integration/ros2/publisher.py +10 -0
  40. topicforge-0.6.1/tests/integration/ros2/test_live_adapter.py +309 -0
  41. {topicforge-0.5.6 → topicforge-0.6.1}/tests/test_bag_omnisim_humble.py +16 -0
  42. {topicforge-0.5.6 → topicforge-0.6.1}/tests/test_cdr_decoder.py +7 -0
  43. {topicforge-0.5.6 → topicforge-0.6.1}/tests/test_composite_adapter.py +8 -5
  44. topicforge-0.6.1/tests/test_echo_parser.py +201 -0
  45. topicforge-0.6.1/tests/test_echo_stream.py +247 -0
  46. {topicforge-0.5.6 → topicforge-0.6.1}/tests/test_health.py +56 -0
  47. {topicforge-0.5.6 → topicforge-0.6.1}/tests/test_live_adapter_graph.py +67 -83
  48. topicforge-0.6.1/tests/test_live_adapter_parse.py +103 -0
  49. {topicforge-0.5.6 → topicforge-0.6.1}/tests/test_live_adapter_subprocess.py +112 -50
  50. topicforge-0.6.1/tests/test_live_sample_notes.py +152 -0
  51. {topicforge-0.5.6 → topicforge-0.6.1}/tests/test_mock_adapter.py +38 -2
  52. topicforge-0.6.1/tests/test_process_tree.py +209 -0
  53. {topicforge-0.5.6 → topicforge-0.6.1}/tests/test_sample_options.py +76 -16
  54. topicforge-0.5.6/tests/fixtures/csv_echo_imu.txt +0 -12
  55. topicforge-0.5.6/tests/fixtures/csv_echo_pose_multi.txt +0 -9
  56. topicforge-0.5.6/tests/integration/ros2/test_live_adapter.py +0 -191
  57. topicforge-0.5.6/tests/test_live_adapter_parse.py +0 -276
  58. {topicforge-0.5.6 → topicforge-0.6.1}/.gitignore +0 -0
  59. {topicforge-0.5.6 → topicforge-0.6.1}/LICENSE +0 -0
  60. {topicforge-0.5.6 → topicforge-0.6.1}/docs/DDS_QUICKSTART.md +0 -0
  61. {topicforge-0.5.6 → topicforge-0.6.1}/docs/TUTORIEL.md +0 -0
  62. {topicforge-0.5.6 → topicforge-0.6.1}/docs/dds-interop-matrix.md +0 -0
  63. {topicforge-0.5.6 → topicforge-0.6.1}/examples/README.md +0 -0
  64. {topicforge-0.5.6 → topicforge-0.6.1}/examples/dds/00_hello_pub_sub/README.md +0 -0
  65. {topicforge-0.5.6 → topicforge-0.6.1}/examples/dds/01_who_is_on_the_bus/README.md +0 -0
  66. {topicforge-0.5.6 → topicforge-0.6.1}/examples/dds/02_why_cant_they_talk/README.md +0 -0
  67. {topicforge-0.5.6 → topicforge-0.6.1}/examples/dds/03_a_node_crashed/README.md +0 -0
  68. {topicforge-0.5.6 → topicforge-0.6.1}/examples/dds/04_late_joiner_misses_data/README.md +0 -0
  69. {topicforge-0.5.6 → topicforge-0.6.1}/examples/dds/05_reliability_in_code/README.md +0 -0
  70. {topicforge-0.5.6 → topicforge-0.6.1}/examples/dds/06_durability_late_joiner_in_code/README.md +0 -0
  71. {topicforge-0.5.6 → topicforge-0.6.1}/examples/dds/07_deadline_in_code/README.md +0 -0
  72. {topicforge-0.5.6 → topicforge-0.6.1}/examples/dds/08_crash_seen_from_inside/README.md +0 -0
  73. {topicforge-0.5.6 → topicforge-0.6.1}/examples/dds/10_lidar_silent_after_driver_swap/README.md +0 -0
  74. {topicforge-0.5.6 → topicforge-0.6.1}/examples/dds/11_who_talks_to_whom/README.md +0 -0
  75. {topicforge-0.5.6 → topicforge-0.6.1}/examples/dds/12_safety_monitor_dropout/README.md +0 -0
  76. {topicforge-0.5.6 → topicforge-0.6.1}/examples/dds/13_deadline_not_offered/README.md +0 -0
  77. {topicforge-0.5.6 → topicforge-0.6.1}/examples/dds/14_restart_loop/README.md +0 -0
  78. {topicforge-0.5.6 → topicforge-0.6.1}/examples/dds/README.md +0 -0
  79. {topicforge-0.5.6 → topicforge-0.6.1}/scripts/agent_eval/README.md +0 -0
  80. {topicforge-0.5.6 → topicforge-0.6.1}/scripts/integration/README.md +0 -0
  81. {topicforge-0.5.6 → topicforge-0.6.1}/scripts/integration/publishers/cyclone_c/README.md +0 -0
  82. {topicforge-0.5.6 → topicforge-0.6.1}/scripts/integration/publishers/cyclone_cpp/README.md +0 -0
  83. {topicforge-0.5.6 → topicforge-0.6.1}/scripts/integration/publishers/cyclone_rust/README.md +0 -0
  84. {topicforge-0.5.6 → topicforge-0.6.1}/scripts/integration/publishers/dust_py/README.md +0 -0
  85. {topicforge-0.5.6 → topicforge-0.6.1}/scripts/integration/publishers/fast_publisher_cpp/README.md +0 -0
  86. {topicforge-0.5.6 → topicforge-0.6.1}/scripts/integration/publishers/fast_py/README.md +0 -0
  87. {topicforge-0.5.6 → topicforge-0.6.1}/scripts/integration/publishers/opensplice_publisher/README.md +0 -0
  88. {topicforge-0.5.6 → topicforge-0.6.1}/scripts/integration/publishers/rti_c/README.md +0 -0
  89. {topicforge-0.5.6 → topicforge-0.6.1}/scripts/integration/publishers/rti_cpp/README.md +0 -0
  90. {topicforge-0.5.6 → topicforge-0.6.1}/src/topicforge/__main__.py +0 -0
  91. {topicforge-0.5.6 → topicforge-0.6.1}/src/topicforge/adapters/__init__.py +0 -0
  92. {topicforge-0.5.6 → topicforge-0.6.1}/src/topicforge/adapters/common/__init__.py +0 -0
  93. {topicforge-0.5.6 → topicforge-0.6.1}/src/topicforge/adapters/common/dds_helpers.py +0 -0
  94. {topicforge-0.5.6 → topicforge-0.6.1}/src/topicforge/adapters/common/dds_introspection.py +0 -0
  95. {topicforge-0.5.6 → topicforge-0.6.1}/src/topicforge/adapters/common/discovery_tracker.py +0 -0
  96. {topicforge-0.5.6 → topicforge-0.6.1}/src/topicforge/adapters/common/endpoints.py +0 -0
  97. {topicforge-0.5.6 → topicforge-0.6.1}/src/topicforge/adapters/common/lifecycle.py +0 -0
  98. {topicforge-0.5.6 → topicforge-0.6.1}/src/topicforge/adapters/common/metrics_buffer.py +0 -0
  99. {topicforge-0.5.6 → topicforge-0.6.1}/src/topicforge/adapters/common/qos_analyzer.py +0 -0
  100. {topicforge-0.5.6 → topicforge-0.6.1}/src/topicforge/adapters/common/qos_endpoints.py +0 -0
  101. {topicforge-0.5.6 → topicforge-0.6.1}/src/topicforge/adapters/common/qos_normalize.py +0 -0
  102. {topicforge-0.5.6 → topicforge-0.6.1}/src/topicforge/adapters/common/qos_scan.py +0 -0
  103. {topicforge-0.5.6 → topicforge-0.6.1}/src/topicforge/adapters/common/topic_filter.py +0 -0
  104. {topicforge-0.5.6 → topicforge-0.6.1}/src/topicforge/adapters/common/xtypes.py +0 -0
  105. {topicforge-0.5.6 → topicforge-0.6.1}/src/topicforge/adapters/dds_cyclone/__init__.py +0 -0
  106. {topicforge-0.5.6 → topicforge-0.6.1}/src/topicforge/adapters/dds_dust/__init__.py +0 -0
  107. {topicforge-0.5.6 → topicforge-0.6.1}/src/topicforge/adapters/dds_fast/__init__.py +0 -0
  108. {topicforge-0.5.6 → topicforge-0.6.1}/src/topicforge/adapters/dds_opendds/__init__.py +0 -0
  109. {topicforge-0.5.6 → topicforge-0.6.1}/src/topicforge/adapters/ros2_live/__init__.py +0 -0
  110. {topicforge-0.5.6 → topicforge-0.6.1}/src/topicforge/adapters/ros2_live/parsers.py +0 -0
  111. {topicforge-0.5.6 → topicforge-0.6.1}/src/topicforge/adapters/ros2_mock/__init__.py +0 -0
  112. {topicforge-0.5.6 → topicforge-0.6.1}/src/topicforge/config/__init__.py +0 -0
  113. {topicforge-0.5.6 → topicforge-0.6.1}/src/topicforge/config/settings.py +0 -0
  114. {topicforge-0.5.6 → topicforge-0.6.1}/src/topicforge/models/__init__.py +0 -0
  115. {topicforge-0.5.6 → topicforge-0.6.1}/src/topicforge/server/__init__.py +0 -0
  116. {topicforge-0.5.6 → topicforge-0.6.1}/src/topicforge/server/app.py +0 -0
  117. {topicforge-0.5.6 → topicforge-0.6.1}/src/topicforge/services/__init__.py +0 -0
  118. {topicforge-0.5.6 → topicforge-0.6.1}/src/topicforge/services/bag_stats.py +0 -0
  119. {topicforge-0.5.6 → topicforge-0.6.1}/src/topicforge/services/sample_budget.py +0 -0
  120. {topicforge-0.5.6 → topicforge-0.6.1}/src/topicforge/telemetry/__init__.py +0 -0
  121. {topicforge-0.5.6 → topicforge-0.6.1}/src/topicforge/telemetry/client.py +0 -0
  122. {topicforge-0.5.6 → topicforge-0.6.1}/src/topicforge/tools/__init__.py +0 -0
  123. {topicforge-0.5.6 → topicforge-0.6.1}/tests/__init__.py +0 -0
  124. {topicforge-0.5.6 → topicforge-0.6.1}/tests/conftest.py +0 -0
  125. {topicforge-0.5.6 → topicforge-0.6.1}/tests/fixtures/ros2_topic_info_verbose_cmd_vel.txt +0 -0
  126. {topicforge-0.5.6 → topicforge-0.6.1}/tests/fixtures/ros2_topic_info_verbose_parameter_events.txt +0 -0
  127. {topicforge-0.5.6 → topicforge-0.6.1}/tests/fixtures/ros2_topic_info_verbose_scan.txt +0 -0
  128. {topicforge-0.5.6 → topicforge-0.6.1}/tests/fixtures/ros2_topic_info_verbose_tf.txt +0 -0
  129. {topicforge-0.5.6 → topicforge-0.6.1}/tests/fixtures/ros2_topic_info_verbose_tf_static.txt +0 -0
  130. {topicforge-0.5.6 → topicforge-0.6.1}/tests/integration/__init__.py +0 -0
  131. {topicforge-0.5.6 → topicforge-0.6.1}/tests/integration/ros2/Dockerfile +0 -0
  132. {topicforge-0.5.6 → topicforge-0.6.1}/tests/integration/ros2/__init__.py +0 -0
  133. {topicforge-0.5.6 → topicforge-0.6.1}/tests/integration/ros2/entrypoint.sh +0 -0
  134. {topicforge-0.5.6 → topicforge-0.6.1}/tests/integration/ros2/run_bench.py +0 -0
  135. {topicforge-0.5.6 → topicforge-0.6.1}/tests/integration/test_real_bus.py +0 -0
  136. {topicforge-0.5.6 → topicforge-0.6.1}/tests/test_analyze_bag_multi_format.py +0 -0
  137. {topicforge-0.5.6 → topicforge-0.6.1}/tests/test_bag_service.py +0 -0
  138. {topicforge-0.5.6 → topicforge-0.6.1}/tests/test_bag_stats.py +0 -0
  139. {topicforge-0.5.6 → topicforge-0.6.1}/tests/test_config.py +0 -0
  140. {topicforge-0.5.6 → topicforge-0.6.1}/tests/test_cyclone_adapter.py +0 -0
  141. {topicforge-0.5.6 → topicforge-0.6.1}/tests/test_dds_cross_vendor.py +0 -0
  142. {topicforge-0.5.6 → topicforge-0.6.1}/tests/test_dds_helpers.py +0 -0
  143. {topicforge-0.5.6 → topicforge-0.6.1}/tests/test_dds_inactive_reason.py +0 -0
  144. {topicforge-0.5.6 → topicforge-0.6.1}/tests/test_dds_introspection.py +0 -0
  145. {topicforge-0.5.6 → topicforge-0.6.1}/tests/test_dds_qos_normalization.py +0 -0
  146. {topicforge-0.5.6 → topicforge-0.6.1}/tests/test_dds_schemas.py +0 -0
  147. {topicforge-0.5.6 → topicforge-0.6.1}/tests/test_discovery_tracker.py +0 -0
  148. {topicforge-0.5.6 → topicforge-0.6.1}/tests/test_dust_adapter.py +0 -0
  149. {topicforge-0.5.6 → topicforge-0.6.1}/tests/test_endpoints.py +0 -0
  150. {topicforge-0.5.6 → topicforge-0.6.1}/tests/test_example_node_spec.py +0 -0
  151. {topicforge-0.5.6 → topicforge-0.6.1}/tests/test_factory.py +0 -0
  152. {topicforge-0.5.6 → topicforge-0.6.1}/tests/test_fast_adapter.py +0 -0
  153. {topicforge-0.5.6 → topicforge-0.6.1}/tests/test_history_and_hints.py +0 -0
  154. {topicforge-0.5.6 → topicforge-0.6.1}/tests/test_honest_outputs.py +0 -0
  155. {topicforge-0.5.6 → topicforge-0.6.1}/tests/test_inspector.py +0 -0
  156. {topicforge-0.5.6 → topicforge-0.6.1}/tests/test_lifecycle_buffer.py +0 -0
  157. {topicforge-0.5.6 → topicforge-0.6.1}/tests/test_metrics_buffer.py +0 -0
  158. {topicforge-0.5.6 → topicforge-0.6.1}/tests/test_opendds_adapter.py +0 -0
  159. {topicforge-0.5.6 → topicforge-0.6.1}/tests/test_peek_bag_samples.py +0 -0
  160. {topicforge-0.5.6 → topicforge-0.6.1}/tests/test_qos_analyzer.py +0 -0
  161. {topicforge-0.5.6 → topicforge-0.6.1}/tests/test_qos_endpoints.py +0 -0
  162. {topicforge-0.5.6 → topicforge-0.6.1}/tests/test_qos_scan.py +0 -0
  163. {topicforge-0.5.6 → topicforge-0.6.1}/tests/test_telemetry.py +0 -0
  164. {topicforge-0.5.6 → topicforge-0.6.1}/tests/test_tools_integration.py +0 -0
  165. {topicforge-0.5.6 → topicforge-0.6.1}/tests/test_topic_metrics.py +0 -0
  166. {topicforge-0.5.6 → topicforge-0.6.1}/tests/test_xtypes.py +0 -0
@@ -5,6 +5,98 @@ All notable changes to TopicForge are documented in this file.
5
5
  The format is based on [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.6.1] - 2026-10-06
9
+
10
+ ### Added
11
+
12
+ - A Claude plugin bundle in `plugin/`: the TopicForge MCP server (started with
13
+ `uvx`, version pinned) and two skills, `diagnose-dds-bus` and
14
+ `inspect-ros2-robot`. Install with `claude plugin marketplace add
15
+ yaniswav/TopicForge`.
16
+ - [docs/VALIDATION.md](docs/VALIDATION.md): the OmniSim team's external validation
17
+ against a simulated robot's ground truth, published as approved by them.
18
+ - `MessageSample.recorded_ns`: the bag record time of a `peek_bag_samples` sample.
19
+ - `MessageSample.stamp_source` value `recorded`: `timestamp_ns` is the bag record
20
+ time because the message has no top-level header.
21
+ - `HealthReport.sim_clock_published` (live ROS 2 only, else null): whether `/clock`
22
+ has a publisher, a hint that nodes may run on simulated time.
23
+
24
+ ### Changed
25
+
26
+ - `peek_bag_samples` `timestamp_ns` is now `header.stamp` when the message has a
27
+ top-level header (`stamp_source` `header`), else the bag record time (`stamp_source`
28
+ `recorded`). It used to be the bag record time for every message, with
29
+ `stamp_source` null. The tool description says it returns the first `count`
30
+ messages, not the last.
31
+ - CI: `actions/checkout` v7, `actions/setup-python` v7, `actions/cache` v6. Jobs
32
+ that run tests or the demos use `ubuntu-24.04` instead of `ubuntu-latest`.
33
+
34
+ ### Fixed
35
+
36
+ - `peek_bag_samples` payloads no longer carry `__msgtype__` in every nested object
37
+ nor a top-level `_msgtype`; the message type stays at the sample level.
38
+
39
+ ## [0.6.0] - 2026-10-05
40
+
41
+ `sample_messages` (live) rewritten after the OmniSim team's report (count
42
+ ignored, empty results on latched topics, wrong timestamps and shifted columns
43
+ on simulated time).
44
+
45
+ ### Breaking
46
+
47
+ - `sample_messages` payloads are nested named fields (`payload.header.stamp.sec`,
48
+ `payload.ranges`) instead of positional `col_0`, `col_1`, ... columns. The
49
+ verbatim row under `_raw_text` is gone: it is present only when a message could
50
+ not be parsed.
51
+ - `_truncated_after_columns` and `_truncated_columns` are replaced by
52
+ `_truncated_fields`, a list of dotted field paths (`["ranges", "intensities"]`)
53
+ for arrays, strings or bytes cut at `max_array_length`. A cut array no longer
54
+ carries the CLI's `'...'` element.
55
+ - `nan`, `inf` and `-inf` floats are returned as the strings `"nan"`, `"inf"`,
56
+ `"-inf"`.
57
+ - `MiddlewareAdapter.sample_messages` returns a `SampleResult` instead of a list
58
+ of `MessageSample` and takes `timeout_s`. `parse_csv_echo` and `parse_echo_yaml`
59
+ are removed.
60
+ - `pyyaml>=6` is a runtime dependency.
61
+ - A failing `ros2 topic echo` (non-zero exit with no message, or a CLI that cannot
62
+ be started) raises an MCP error instead of returning an empty list. `count` 0 now
63
+ validates the topic in live mode too, as it already did in the mock.
64
+
65
+ ### Added
66
+
67
+ - `sample_messages` parameter `timeout_s` (1..45, default 10): a wall deadline for
68
+ the whole call, topic lookup included. The call returns within about `timeout_s`
69
+ plus 2 s (stopping the CLI, decoding what arrived).
70
+ - `MessageSample.stamp_source` (`header` or `none`) and `received_ns` (wall clock
71
+ when TopicForge read the message).
72
+ - A short result carries a `note` (`N of M messages within T s`) that tells "no
73
+ publisher is announced" from "a publisher exists but nothing arrived in time",
74
+ hints `count` 1 for a latched topic, and gives the exit code and stderr when the
75
+ CLI exited early.
76
+ - A message over the per-message size cap (`TOPICFORGE_MAX_SAMPLE_BYTES`, 1 MiB) is
77
+ dropped while it streams and counted in the `note`, so a large `Image` no longer
78
+ costs tens of seconds of YAML decoding. Decoding uses libyaml when available and
79
+ stops at the call deadline.
80
+ - `count` above 50 is capped with a note, and the tool schema declares the
81
+ maximum.
82
+
83
+ ### Fixed
84
+
85
+ - `sample_messages` honours `count`: it streams `ros2 topic echo` and stops at
86
+ `count` messages or the deadline, then stops the process tree (SIGINT then
87
+ SIGKILL on POSIX, `taskkill /F /T` on Windows) (OmniSim D3). The echo is
88
+ started with explicit `--qos-reliability` and `--qos-durability` taken from the
89
+ publishers, so a latched (`transient_local`) topic is no longer returned empty
90
+ because the CLI picked its QoS against a cold daemon.
91
+ - Stopping the CLI kills the whole process tree even when the launcher already
92
+ exited (POSIX: always signals the process group; Windows: a Job Object), so no
93
+ orphan `ros2` process keeps running. The CLI is started with UTF-8 output.
94
+ - The mock `sample_messages` matches the live shape: headerless messages have
95
+ `timestamp_ns` 0 and `stamp_source` `none`.
96
+ - `timestamp_ns` is the top-level `header.stamp` whatever its value, so a
97
+ simulated clock (seconds since the simulation start) no longer falls back to 0
98
+ and shifts every following column by two (OmniSim D4).
99
+
8
100
  ## [0.5.6] - 2026-10-02
9
101
 
10
102
  Fixes from a live run of 0.5.3 and 0.5.5 against OmniSim's simulated
@@ -1202,7 +1294,9 @@ Initial MVP release of TopicForge: ROS Topic Inspector & Bag Analyzer MCP server
1202
1294
  - The write path (publishing, commanding robots) is intentionally out of scope for the MVP.
1203
1295
  - `analyze_bag` in live mode parses `ros2 bag info` text output; deeper anomaly detection remains mock-only for now.
1204
1296
 
1205
- [Unreleased]: https://github.com/yaniswav/TopicForge/compare/v0.5.6...HEAD
1297
+ [Unreleased]: https://github.com/yaniswav/TopicForge/compare/v0.6.1...HEAD
1298
+ [0.6.1]: https://github.com/yaniswav/TopicForge/compare/v0.6.0...v0.6.1
1299
+ [0.6.0]: https://github.com/yaniswav/TopicForge/compare/v0.5.6...v0.6.0
1206
1300
  [0.5.6]: https://github.com/yaniswav/TopicForge/compare/v0.5.5...v0.5.6
1207
1301
  [0.5.5]: https://github.com/yaniswav/TopicForge/compare/v0.5.4...v0.5.5
1208
1302
  [0.5.4]: https://github.com/yaniswav/TopicForge/compare/v0.5.3...v0.5.4
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: topicforge
3
- Version: 0.5.6
3
+ Version: 0.6.1
4
4
  Summary: ROS Topic Inspector & Bag Analyzer MCP server for AI agents
5
5
  Project-URL: Homepage, https://github.com/yaniswav/TopicForge
6
6
  Project-URL: Repository, https://github.com/yaniswav/TopicForge
@@ -42,6 +42,7 @@ Classifier: Programming Language :: Python :: 3.13
42
42
  Requires-Python: >=3.10
43
43
  Requires-Dist: mcp<2,>=1.0.0
44
44
  Requires-Dist: pydantic>=2.6
45
+ Requires-Dist: pyyaml>=6
45
46
  Provides-Extra: all
46
47
  Requires-Dist: cyclonedds>=0.10; extra == 'all'
47
48
  Provides-Extra: bags
@@ -108,8 +109,8 @@ Every response except `health_check` carries `mode_effective` (`"live"` or `"moc
108
109
  | `health_check` | Environment and mode introspection. Always succeeds; reports `mode` next to `requested_mode` |
109
110
  | `list_topics` | Discover the ROS2 graph |
110
111
  | `get_topic_info` | Message type, publisher/subscriber counts and QoS for one topic |
111
- | `sample_messages` | Peek recent messages on a ROS2 topic (count clamped to 50) |
112
- | `analyze_bag` | Summarize a `.mcap` / `.db3` recording or `rosbag2_*` directory (via `ros2 bag info`) |
112
+ | `sample_messages` | Peek recent messages on a ROS2 topic (count capped at 50) |
113
+ | `analyze_bag` | Summarize a `.mcap` / `.db3` recording or `rosbag2_*` directory (via `ros2 bag info`) |
113
114
  | `list_participants` | DDS participants on the domain: vendor, `name` (EntityName QoS, Cyclone) and `hostname` |
114
115
  | `detect_qos_mismatches` | Incompatible QoS pairs between DDS readers and writers |
115
116
  | `peek_dds_samples` | Raw DDS samples; structured on the three builtin discovery topics, presence-only on user topics |
@@ -157,15 +158,17 @@ Samples with comments are in [`.env.example`](.env.example). Any invalid value s
157
158
 
158
159
  ## Limitations
159
160
 
161
+ External validation against a simulated robot's ground truth: [docs/VALIDATION.md](docs/VALIDATION.md).
162
+
160
163
  - DDS validation is partial. The Cyclone adapter has run against a real bus, with Cyclone and Dust DDS participants, on Windows and in CI on Ubuntu and Windows (`.github/workflows/demo.yml`). The Fast DDS adapter has never run against a bus, and 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.
161
164
  - User-topic payloads are not decoded. `peek_dds_samples` on a user topic returns count 0 and a note that the topic is announced on the bus; no traffic is read. `topic_metrics` therefore has data only for the builtin discovery topics and says so in its `status`. It is a discovery-layer probe, not a publish-rate monitor.
162
- - 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 for 0.5.6. A crash and a clean leave cannot be told apart, and `lost_ns` is an upper bound of the death.
165
+ - 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.
163
166
  - 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`.
164
167
  - Single domain: the server observes the domain it joined at startup; changing it needs a restart.
165
168
  - 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.
166
169
  - Fast DDS serves no `list_endpoints`.
167
- - `sample_messages` (live) runs `ros2 topic echo --csv --once` with a short timeout, so it returns at most one message, and a topic with no current publisher returns an empty sample. `timestamp_ns` is the message `header.stamp` for `Header`-stamped types and `0` for headerless ones. 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_after_columns`.
168
- - `analyze_bag` (live) parses `ros2 bag info` text for the totals and counts; anomaly detection is mock-only. Per-topic times, rates (`(n - 1) / span`) and `latched` are added when the bag can be read locally (`.db3` with the standard library, `.mcap` with `rosbags`), else rates fall back to count / bag duration (`frequency_basis`). `peek_bag_samples` reads the file itself, through `rosbags`, and is served only by the ROS2 CLI adapter or the mock; bags that embed no message definitions (Humble `.db3`) are decoded with the Humble definitions, or the distro the bag records, and `note` says so. Without `ros2`, bag tools return fixtures: check `health_check` for `mode: "mock"` before trusting bag output.
170
+ - `sample_messages` (live) streams `ros2 topic echo` until `count` messages arrive or `timeout_s` (1..45, 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) or 0 for 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.
171
+ - `analyze_bag` (live) parses `ros2 bag info` text for the totals and counts; anomaly detection is mock-only. Per-topic times, rates (`(n - 1) / span`) and `latched` are added when the bag can be read locally (`.db3` with the standard library, `.mcap` with `rosbags`), else rates fall back to count / bag duration (`frequency_basis`). `peek_bag_samples` reads the file itself, through `rosbags`, returns the first `count` messages in recording order (not the last), and is served only by the ROS2 CLI adapter or the mock; its `timestamp_ns` is the message's `header.stamp` (`stamp_source` `header`) or, without a top-level header, the bag record time (`stamp_source` `recorded`), and `recorded_ns` is always the bag record time; bags that embed no message definitions (Humble `.db3`) are decoded with the Humble definitions, or the distro the bag records, and `note` says so. Without `ros2`, bag tools return fixtures: check `health_check` for `mode: "mock"` before trusting bag output. `health_check` also reports `sim_clock_published` (live only): true when `/clock` has a publisher, a hint that header stamps may be simulation time.
169
172
  - Synchronous handlers: the tools run on the MCP event loop; on Windows a hung `ros2` launcher can block the server.
170
173
  - No streaming or push subscriptions: tools are strictly request/response.
171
174
 
@@ -188,7 +191,7 @@ When on, each tool call emits one event with exactly six fields:
188
191
  | `tool_name` | `"list_topics"` | One of the twelve tools, never argument values |
189
192
  | `latency_ms` | `12.34` | Handler wall-clock duration, 2 decimals |
190
193
  | `mode` | `"mock"` | Mode of the adapter actually serving: `mock` or `live` |
191
- | `version` | `"0.5.6"` | TopicForge server version |
194
+ | `version` | `"0.6.1"` | TopicForge server version |
192
195
  | `session_id` | `"a1b2c3..."` | Random UUID per process, never persisted |
193
196
  | `success` | `true` | Whether the handler returned or raised |
194
197
 
@@ -49,8 +49,8 @@ Every response except `health_check` carries `mode_effective` (`"live"` or `"moc
49
49
  | `health_check` | Environment and mode introspection. Always succeeds; reports `mode` next to `requested_mode` |
50
50
  | `list_topics` | Discover the ROS2 graph |
51
51
  | `get_topic_info` | Message type, publisher/subscriber counts and QoS for one topic |
52
- | `sample_messages` | Peek recent messages on a ROS2 topic (count clamped to 50) |
53
- | `analyze_bag` | Summarize a `.mcap` / `.db3` recording or `rosbag2_*` directory (via `ros2 bag info`) |
52
+ | `sample_messages` | Peek recent messages on a ROS2 topic (count capped at 50) |
53
+ | `analyze_bag` | Summarize a `.mcap` / `.db3` recording or `rosbag2_*` directory (via `ros2 bag info`) |
54
54
  | `list_participants` | DDS participants on the domain: vendor, `name` (EntityName QoS, Cyclone) and `hostname` |
55
55
  | `detect_qos_mismatches` | Incompatible QoS pairs between DDS readers and writers |
56
56
  | `peek_dds_samples` | Raw DDS samples; structured on the three builtin discovery topics, presence-only on user topics |
@@ -98,15 +98,17 @@ Samples with comments are in [`.env.example`](.env.example). Any invalid value s
98
98
 
99
99
  ## Limitations
100
100
 
101
+ External validation against a simulated robot's ground truth: [docs/VALIDATION.md](docs/VALIDATION.md).
102
+
101
103
  - DDS validation is partial. The Cyclone adapter has run against a real bus, with Cyclone and Dust DDS participants, on Windows and in CI on Ubuntu and Windows (`.github/workflows/demo.yml`). The Fast DDS adapter has never run against a bus, and 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.
102
104
  - User-topic payloads are not decoded. `peek_dds_samples` on a user topic returns count 0 and a note that the topic is announced on the bus; no traffic is read. `topic_metrics` therefore has data only for the builtin discovery topics and says so in its `status`. It is a discovery-layer probe, not a publish-rate monitor.
103
- - 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 for 0.5.6. A crash and a clean leave cannot be told apart, and `lost_ns` is an upper bound of the death.
105
+ - 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.
104
106
  - 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`.
105
107
  - Single domain: the server observes the domain it joined at startup; changing it needs a restart.
106
108
  - 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.
107
109
  - Fast DDS serves no `list_endpoints`.
108
- - `sample_messages` (live) runs `ros2 topic echo --csv --once` with a short timeout, so it returns at most one message, and a topic with no current publisher returns an empty sample. `timestamp_ns` is the message `header.stamp` for `Header`-stamped types and `0` for headerless ones. 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_after_columns`.
109
- - `analyze_bag` (live) parses `ros2 bag info` text for the totals and counts; anomaly detection is mock-only. Per-topic times, rates (`(n - 1) / span`) and `latched` are added when the bag can be read locally (`.db3` with the standard library, `.mcap` with `rosbags`), else rates fall back to count / bag duration (`frequency_basis`). `peek_bag_samples` reads the file itself, through `rosbags`, and is served only by the ROS2 CLI adapter or the mock; bags that embed no message definitions (Humble `.db3`) are decoded with the Humble definitions, or the distro the bag records, and `note` says so. Without `ros2`, bag tools return fixtures: check `health_check` for `mode: "mock"` before trusting bag output.
110
+ - `sample_messages` (live) streams `ros2 topic echo` until `count` messages arrive or `timeout_s` (1..45, 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) or 0 for 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.
111
+ - `analyze_bag` (live) parses `ros2 bag info` text for the totals and counts; anomaly detection is mock-only. Per-topic times, rates (`(n - 1) / span`) and `latched` are added when the bag can be read locally (`.db3` with the standard library, `.mcap` with `rosbags`), else rates fall back to count / bag duration (`frequency_basis`). `peek_bag_samples` reads the file itself, through `rosbags`, returns the first `count` messages in recording order (not the last), and is served only by the ROS2 CLI adapter or the mock; its `timestamp_ns` is the message's `header.stamp` (`stamp_source` `header`) or, without a top-level header, the bag record time (`stamp_source` `recorded`), and `recorded_ns` is always the bag record time; bags that embed no message definitions (Humble `.db3`) are decoded with the Humble definitions, or the distro the bag records, and `note` says so. Without `ros2`, bag tools return fixtures: check `health_check` for `mode: "mock"` before trusting bag output. `health_check` also reports `sim_clock_published` (live only): true when `/clock` has a publisher, a hint that header stamps may be simulation time.
110
112
  - Synchronous handlers: the tools run on the MCP event loop; on Windows a hung `ros2` launcher can block the server.
111
113
  - No streaming or push subscriptions: tools are strictly request/response.
112
114
 
@@ -129,7 +131,7 @@ When on, each tool call emits one event with exactly six fields:
129
131
  | `tool_name` | `"list_topics"` | One of the twelve tools, never argument values |
130
132
  | `latency_ms` | `12.34` | Handler wall-clock duration, 2 decimals |
131
133
  | `mode` | `"mock"` | Mode of the adapter actually serving: `mock` or `live` |
132
- | `version` | `"0.5.6"` | TopicForge server version |
134
+ | `version` | `"0.6.1"` | TopicForge server version |
133
135
  | `session_id` | `"a1b2c3..."` | Random UUID per process, never persisted |
134
136
  | `success` | `true` | Whether the handler returned or raised |
135
137
 
@@ -15,7 +15,7 @@ How to get a working ROS2 environment to point TopicForge at, and how to wire it
15
15
  ```bash
16
16
  python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\Activate.ps1
17
17
  pip install topicforge
18
- python -m topicforge --version # -> topicforge 0.5.6
18
+ python -m topicforge --version # -> topicforge 0.6.1
19
19
  TOPICFORGE_MODE=mock python -m topicforge # blocks on stdio; MCP clients spawn it
20
20
  ```
21
21
 
@@ -41,7 +41,7 @@ python3 -m venv ~/topicforge-venv && source ~/topicforge-venv/bin/activate
41
41
  pip install topicforge # Ubuntu 22.04 ships Python 3.10, which is enough
42
42
  ```
43
43
 
44
- Then run live mode with three terminals. In the first, `ros2 run demo_nodes_cpp talker` publishes `/chatter` at about 1 Hz. In the second, `ros2 topic list` should show `/chatter`. In the third, source ROS2, activate the venv and run `TOPICFORGE_MODE=live python -m topicforge`. The startup line reads `topicforge 0.5.6 ready (mode=live, requested_mode=live, adapter=ros2_cli, telemetry=off)`; `mode=mock` means `ros2` was not found and the server fell back to fixtures.
44
+ Then run live mode with three terminals. In the first, `ros2 run demo_nodes_cpp talker` publishes `/chatter` at about 1 Hz. In the second, `ros2 topic list` should show `/chatter`. In the third, source ROS2, activate the venv and run `TOPICFORGE_MODE=live python -m topicforge`. The startup line reads `topicforge 0.6.1 ready (mode=live, requested_mode=live, adapter=ros2_cli, telemetry=off)`; `mode=mock` means `ros2` was not found and the server fell back to fixtures.
45
45
 
46
46
  ## Linux native
47
47
 
@@ -77,7 +77,7 @@ TopicForge resolves the `ros2` launcher with `shutil.which` (normally `ros2.exe`
77
77
 
78
78
  Discover the graph. Ask "What topics are currently being published, and what message types do they carry?" TopicForge calls `list_topics`. Live mode shows `/chatter` plus `/rosout` and `/parameter_events`; mock mode shows `/cmd_vel`, `/odom`, `/scan`, `/tf` and `/camera/image_raw`.
79
79
 
80
- Inspect and sample. Ask "Show me the latest message on /chatter." TopicForge calls `get_topic_info` then `sample_messages`. The payload exposes fields as positional CSV columns (`col_0`, `col_1`, ...) plus `_raw_text`, the verbatim row from `ros2 topic echo --csv --once`. `timestamp_ns` is `0` for headerless types such as `std_msgs/String`.
80
+ Inspect and sample. Ask "Show me the latest message on /chatter." TopicForge calls `get_topic_info` then `sample_messages`. The payload has the message fields as nested named values. `timestamp_ns` is the `header.stamp` and `0` for headerless types such as `std_msgs/String` (`stamp_source` says which).
81
81
 
82
82
  Record and analyze a bag.
83
83
 
@@ -63,12 +63,12 @@ The adapter was written against the 2.6.x binding and has never run against a bu
63
63
  - `lookback_seconds must be in 1..86400` (`participant_events`): default 300. The lifecycle buffer keeps at most 200 events, newest first, regardless of window.
64
64
  - `window_seconds must be in 1..3600` (`topic_metrics`): default 60. The buffer holds 1000 samples per topic, drop-oldest, so frequency is computed from buffered samples, not a true rolling window.
65
65
  - `DDS topic name is malformed`: DDS names match `^[A-Za-z_/][A-Za-z0-9_/:]*$` (builtin names like `DCPSParticipant` and `::` separators are allowed; whitespace and dashes are not). ROS2 names start with `/` and use `_`, so `my-topic` becomes `/my_topic`.
66
- - `sample_messages` and `peek_*` silently clamp `count` to 50; the `count` field of the result is what was actually returned.
66
+ - `sample_messages` caps `count` at 50 and says so in `note`; `peek_*` clamp silently. The `count` field of the result is what was actually returned.
67
67
 
68
68
  ## Other
69
69
 
70
70
  - `topicforge: command not found`: the entry point is not on PATH. Re-activate the venv, or use `python -m topicforge`.
71
- - `sample_messages` returns no samples: live mode runs `ros2 topic echo --once` with a 3 second timeout; a topic with no active publisher returns nothing. Check `ros2 topic info -v <topic>`.
71
+ - `sample_messages` returns fewer samples than asked: live mode waits `timeout_s` seconds (default 10, max 60, counted from the start of the `ros2` CLI, which can take a few seconds on a slow machine) and `note` says `N of M messages` and whether a publisher exists. Raise `timeout_s` for a topic slower than 1 Hz; check `ros2 topic info -v <topic>`.
72
72
  - `analyze_bag` says the path does not exist: the path is resolved in the shell where TopicForge runs. Under WSL use `/mnt/c/demos/run.mcap`, not `C:\demos\run.mcap`. In mock mode only `.mcap`, `.db3`, `.bag` or extensionless paths are accepted.
73
73
  - Claude Desktop shows no tools: check Help -> View Logs -> MCP, confirm `topicforge --version` runs from the same environment (or use the absolute path of the binary in the config), and validate the config with `python -m json.tool claude_desktop_config.json`, since a JSON error silently drops the whole file.
74
74
  - First live call is slow: `ros2 topic list -t` initializes the DDS middleware, a 1-2 second warm-up.
@@ -0,0 +1,15 @@
1
+ # External validation
2
+
3
+ ## OmniSim (October 2026)
4
+
5
+ The OmniSim team ran TopicForge against a simulated robot and compared its answers with the simulator's ground truth.
6
+
7
+ Setup: [OmniSim v9.1.3](https://github.com/omnilink-tech/omnisim/releases/tag/v9.1.3), a simulated Clearpath Husky with a 541-beam SICK LMS111 lidar in a room with walls at known positions, robot stationary. ROS 2 Humble on Ubuntu 22.04 (WSL2), Fast DDS. TopicForge 0.5.3 in live mode for the ROS 2 tools, and 0.5.5 with the Cyclone backend for the DDS tools.
8
+
9
+ What matched:
10
+ - Every lidar scan TopicForge sampled was bit-identical to the simulator's raw data on the beams it delivered.
11
+ - `list_topics` and `get_topic_info` returned the same names, types and publisher/subscriber counts as the `ros2` CLI.
12
+ - `analyze_bag` returned the same duration and message counts as `ros2 bag info` and an independent read of the bag.
13
+ - With the Cyclone backend, TopicForge saw all seven Fast DDS participants of the ROS 2 graph without extra configuration, and reported participants leaving within about 10 ms of the actual time.
14
+
15
+ What it found: seven discrepancies on TopicForge's side, including lidar arrays cut at 128 values, one message per `sample_messages` call, zero timestamps on simulated time, and Humble bags that `peek_bag_samples` could not decode. All seven are addressed in 0.5.6 and 0.6.0 (full arrays through a `max_array_length` option) and covered by a ROS 2 Humble/Jazzy test bench; the OmniSim run itself has not been repeated on these versions yet. The bag recorded during the run is part of TopicForge's test fixtures (`tests/fixtures/bags/omnisim_humble/`).
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Yanis ETHVIGNOT
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,60 @@
1
+ # TopicForge plugin for Claude
2
+
3
+ TopicForge is a read-only MCP server for ROS 2 and DDS. This plugin bundles the server with two short skills so Claude can inspect a robot stack and diagnose a DDS bus. It cannot publish, command a robot, or change QoS: the server has no write path.
4
+
5
+ ## What is included
6
+
7
+ - MCP server `topicforge`, started with `uvx` from the `topicforge` package on PyPI (version pinned to 0.6.1). It exposes twelve read-only tools: `health_check`, `list_topics`, `get_topic_info`, `sample_messages`, `analyze_bag`, `peek_bag_samples`, `list_participants`, `list_endpoints`, `detect_qos_mismatches`, `participant_events`, `peek_dds_samples`, `topic_metrics`.
8
+ - Skill `diagnose-dds-bus`: what to call, and in which order, when nodes do not talk, a topic gets no data, or a node crashed or restarts.
9
+ - Skill `inspect-ros2-robot`: listing topics, sampling messages and reading bags, including large arrays and simulation time.
10
+
11
+ ## Requirements
12
+
13
+ - [`uv`](https://docs.astral.sh/uv/) on PATH, which provides `uvx`. The first start downloads the package and the Cyclone DDS binding, so it can take a little while.
14
+ - A local MCP server: it runs on your machine, in Claude Code and in Cowork sessions that run on your computer. It does not run in claude.ai chat.
15
+ - ROS 2 on PATH is optional. Without it the ROS 2 tools fall back to fixtures (a fictional demo robot) and `health_check` reports `mode: mock`. The DDS tools work without ROS 2.
16
+
17
+ ## Install
18
+
19
+ From a marketplace, once the plugin is listed:
20
+
21
+ ```
22
+ /plugin install topicforge
23
+ ```
24
+
25
+ Meanwhile, from this repository (Claude Code):
26
+
27
+ ```
28
+ claude plugin marketplace add yaniswav/TopicForge
29
+ claude plugin install topicforge@topicforge
30
+ ```
31
+
32
+ Or load the folder for one session: `claude --plugin-dir ./plugin`.
33
+
34
+ ## Configuration
35
+
36
+ Claude Code asks for two options when the plugin is enabled:
37
+
38
+ | Option | Default | Meaning |
39
+ | --- | --- | --- |
40
+ | `dds_backend` | `cyclone` | `cyclone` joins the DDS bus as a read-only participant. `mock` serves fixtures. `auto` picks the best available. |
41
+ | `dds_domain_id` | `0` | The one DDS domain observed (0 to 232). Fixed at startup. |
42
+
43
+ The server runs with `TOPICFORGE_MODE=auto`: live ROS 2 when `ros2` is on PATH, fixtures otherwise. Cowork does not prompt for options and uses the defaults. To use other settings there, edit `.mcp.json`.
44
+
45
+ Without DDS, set `dds_backend` to `mock`, or use the standalone install `uvx topicforge`.
46
+
47
+ ## Privacy
48
+
49
+ The server reads the local ROS 2 graph and the DDS discovery traffic on the machine and network it runs on, and returns the result to the Claude session. It makes no outbound network calls. Anonymous telemetry exists but is off by default and is not enabled by this plugin. See the main [README](https://github.com/yaniswav/TopicForge#telemetry) for the telemetry contract and [SECURITY.md](https://github.com/yaniswav/TopicForge/blob/main/SECURITY.md).
50
+
51
+ ## Limits
52
+
53
+ TopicForge sees what DDS discovery announces. It cannot see whether data flows, a writer that is alive but silent, other DDS domains, or secured (DDS Security) endpoints. The skills tell Claude to say so.
54
+
55
+ ## Links
56
+
57
+ - Project: https://github.com/yaniswav/TopicForge
58
+ - Site: https://topicforge.horizonvista.xyz
59
+ - Issues: https://github.com/yaniswav/TopicForge/issues
60
+ - License: MIT
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "topicforge"
7
- version = "0.5.6"
7
+ version = "0.6.1"
8
8
  description = "ROS Topic Inspector & Bag Analyzer MCP server for AI agents"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -25,6 +25,7 @@ classifiers = [
25
25
  dependencies = [
26
26
  "mcp>=1.0.0,<2",
27
27
  "pydantic>=2.6",
28
+ "pyyaml>=6",
28
29
  ]
29
30
 
30
31
  [project.optional-dependencies]
@@ -1,5 +1,5 @@
1
1
  """TopicForge: ROS Topic Inspector & Bag Analyzer MCP server."""
2
2
 
3
- __version__ = "0.5.6"
3
+ __version__ = "0.6.1"
4
4
 
5
5
  __all__ = ["__version__"]
@@ -9,11 +9,10 @@ from __future__ import annotations
9
9
 
10
10
  from typing import Literal, Protocol, runtime_checkable
11
11
 
12
- from topicforge.constants import DEFAULT_MAX_ARRAY_LENGTH
12
+ from topicforge.constants import DEFAULT_MAX_ARRAY_LENGTH, DEFAULT_SAMPLE_TIMEOUT_S
13
13
  from topicforge.models import (
14
14
  BagAnalysis,
15
15
  EndpointListing,
16
- MessageSample,
17
16
  MismatchScan,
18
17
  ParticipantEvent,
19
18
  ParticipantInfo,
@@ -96,7 +95,8 @@ class MiddlewareAdapter(Protocol):
96
95
  *,
97
96
  max_array_length: int | None = DEFAULT_MAX_ARRAY_LENGTH,
98
97
  arrays_summary_only: bool = False,
99
- ) -> list[MessageSample]: ...
98
+ timeout_s: float = DEFAULT_SAMPLE_TIMEOUT_S,
99
+ ) -> SampleResult: ...
100
100
 
101
101
  def analyze_bag(self, path: str) -> BagAnalysis: ...
102
102
 
@@ -59,8 +59,18 @@ def iter_field_names(sample: Any) -> list[str]:
59
59
  """List field names on a dynamic-type sample.
60
60
 
61
61
  Tries `__dataclass_fields__`, `__fields__`, `__slots__`, then public
62
- `__dict__` keys. Returns an empty list when none apply.
62
+ `__dict__` keys. Returns an empty list when none apply. Dunder names
63
+ such as the `__msgtype__` marker that `rosbags` messages carry are
64
+ not data and are left out.
63
65
  """
66
+ return [name for name in _raw_field_names(sample) if not _is_dunder(name)]
67
+
68
+
69
+ def _is_dunder(name: str) -> bool:
70
+ return name.startswith("__") and name.endswith("__")
71
+
72
+
73
+ def _raw_field_names(sample: Any) -> list[str]:
64
74
  fields = getattr(sample, "__dataclass_fields__", None)
65
75
  if fields:
66
76
  return list(fields)
@@ -15,11 +15,10 @@ from __future__ import annotations
15
15
  from typing import Any
16
16
 
17
17
  from topicforge.adapters.base import AdapterName, EffectiveMode, MiddlewareAdapter
18
- from topicforge.constants import DEFAULT_MAX_ARRAY_LENGTH
18
+ from topicforge.constants import DEFAULT_MAX_ARRAY_LENGTH, DEFAULT_SAMPLE_TIMEOUT_S
19
19
  from topicforge.models import (
20
20
  BagAnalysis,
21
21
  EndpointListing,
22
- MessageSample,
23
22
  MismatchScan,
24
23
  ParticipantEvent,
25
24
  ParticipantInfo,
@@ -69,17 +68,24 @@ class CompositeAdapter:
69
68
  *,
70
69
  max_array_length: int | None = DEFAULT_MAX_ARRAY_LENGTH,
71
70
  arrays_summary_only: bool = False,
72
- ) -> list[MessageSample]:
71
+ timeout_s: float = DEFAULT_SAMPLE_TIMEOUT_S,
72
+ ) -> SampleResult:
73
73
  return self._ros.sample_messages(
74
74
  topic,
75
75
  count,
76
76
  max_array_length=max_array_length,
77
77
  arrays_summary_only=arrays_summary_only,
78
+ timeout_s=timeout_s,
78
79
  )
79
80
 
80
81
  def analyze_bag(self, path: str) -> BagAnalysis:
81
82
  return self._ros.analyze_bag(path)
82
83
 
84
+ def sim_clock_published(self) -> bool | None:
85
+ """The ROS 2 half's `/clock` probe, `None` when it has none."""
86
+ probe = getattr(self._ros, "sim_clock_published", None)
87
+ return probe() if callable(probe) else None
88
+
83
89
  # ----- DDS surface -> DDS adapter -----
84
90
 
85
91
  def observer_status(self) -> dict[str, Any] | None:
@@ -63,7 +63,7 @@ from topicforge.adapters.common import (
63
63
  from topicforge.adapters.common import (
64
64
  cyclone_extract_topic_name as _extract_topic_name,
65
65
  )
66
- from topicforge.constants import DEFAULT_MAX_ARRAY_LENGTH
66
+ from topicforge.constants import DEFAULT_MAX_ARRAY_LENGTH, DEFAULT_SAMPLE_TIMEOUT_S
67
67
  from topicforge.models import (
68
68
  BagAnalysis,
69
69
  EndpointListing,
@@ -334,7 +334,8 @@ class CycloneDdsAdapter:
334
334
  *,
335
335
  max_array_length: int | None = DEFAULT_MAX_ARRAY_LENGTH,
336
336
  arrays_summary_only: bool = False,
337
- ) -> list[MessageSample]:
337
+ timeout_s: float = DEFAULT_SAMPLE_TIMEOUT_S,
338
+ ) -> SampleResult:
338
339
  raise AdapterError(DDS_ONLY_ERROR_MSG)
339
340
 
340
341
  def analyze_bag(self, path: str) -> BagAnalysis:
@@ -12,11 +12,10 @@ import logging
12
12
 
13
13
  from topicforge.adapters.base import AdapterError, AdapterName, EffectiveMode
14
14
  from topicforge.adapters.common import validate_domain_id
15
- from topicforge.constants import DEFAULT_MAX_ARRAY_LENGTH
15
+ from topicforge.constants import DEFAULT_MAX_ARRAY_LENGTH, DEFAULT_SAMPLE_TIMEOUT_S
16
16
  from topicforge.models import (
17
17
  BagAnalysis,
18
18
  EndpointListing,
19
- MessageSample,
20
19
  MismatchScan,
21
20
  ParticipantEvent,
22
21
  ParticipantInfo,
@@ -66,7 +65,8 @@ class DustDdsAdapter:
66
65
  *,
67
66
  max_array_length: int | None = DEFAULT_MAX_ARRAY_LENGTH,
68
67
  arrays_summary_only: bool = False,
69
- ) -> list[MessageSample]:
68
+ timeout_s: float = DEFAULT_SAMPLE_TIMEOUT_S,
69
+ ) -> SampleResult:
70
70
  raise AdapterError(_DUST_ROADMAP_MSG)
71
71
 
72
72
  def analyze_bag(self, path: str) -> BagAnalysis:
@@ -52,7 +52,7 @@ from topicforge.adapters.common import (
52
52
  from topicforge.adapters.common import (
53
53
  is_removal as _is_removal,
54
54
  )
55
- from topicforge.constants import DEFAULT_MAX_ARRAY_LENGTH
55
+ from topicforge.constants import DEFAULT_MAX_ARRAY_LENGTH, DEFAULT_SAMPLE_TIMEOUT_S
56
56
  from topicforge.models import (
57
57
  BagAnalysis,
58
58
  EndpointListing,
@@ -241,7 +241,8 @@ class FastDdsAdapter:
241
241
  *,
242
242
  max_array_length: int | None = DEFAULT_MAX_ARRAY_LENGTH,
243
243
  arrays_summary_only: bool = False,
244
- ) -> list[MessageSample]:
244
+ timeout_s: float = DEFAULT_SAMPLE_TIMEOUT_S,
245
+ ) -> SampleResult:
245
246
  raise AdapterError(DDS_ONLY_ERROR_MSG)
246
247
 
247
248
  def analyze_bag(self, path: str) -> BagAnalysis:
@@ -16,11 +16,10 @@ import logging
16
16
 
17
17
  from topicforge.adapters.base import AdapterError, AdapterName, EffectiveMode
18
18
  from topicforge.adapters.common import validate_domain_id
19
- from topicforge.constants import DEFAULT_MAX_ARRAY_LENGTH
19
+ from topicforge.constants import DEFAULT_MAX_ARRAY_LENGTH, DEFAULT_SAMPLE_TIMEOUT_S
20
20
  from topicforge.models import (
21
21
  BagAnalysis,
22
22
  EndpointListing,
23
- MessageSample,
24
23
  MismatchScan,
25
24
  ParticipantEvent,
26
25
  ParticipantInfo,
@@ -72,7 +71,8 @@ class OpenDdsAdapter:
72
71
  *,
73
72
  max_array_length: int | None = DEFAULT_MAX_ARRAY_LENGTH,
74
73
  arrays_summary_only: bool = False,
75
- ) -> list[MessageSample]:
74
+ timeout_s: float = DEFAULT_SAMPLE_TIMEOUT_S,
75
+ ) -> SampleResult:
76
76
  raise AdapterError(_OPENDDS_ROADMAP_MSG)
77
77
 
78
78
  def analyze_bag(self, path: str) -> BagAnalysis: