topicforge 0.5.0__tar.gz → 0.5.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.
- {topicforge-0.5.0 → topicforge-0.5.1}/CHANGELOG.md +167 -1
- {topicforge-0.5.0 → topicforge-0.5.1}/PKG-INFO +6 -4
- {topicforge-0.5.0 → topicforge-0.5.1}/README.md +1 -1
- {topicforge-0.5.0 → topicforge-0.5.1}/docs/DDS_QUICKSTART.md +1 -1
- topicforge-0.5.1/docs/TUTORIEL.md +204 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/docs/product-plan.md +4 -4
- {topicforge-0.5.0 → topicforge-0.5.1}/pyproject.toml +35 -2
- {topicforge-0.5.0 → topicforge-0.5.1}/src/topicforge/__init__.py +1 -1
- {topicforge-0.5.0 → topicforge-0.5.1}/src/topicforge/adapters/common/__init__.py +30 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/src/topicforge/adapters/common/cdr_decoder.py +22 -5
- {topicforge-0.5.0 → topicforge-0.5.1}/src/topicforge/adapters/common/dds_helpers.py +18 -0
- topicforge-0.5.1/src/topicforge/adapters/common/dds_introspection.py +184 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/src/topicforge/adapters/common/lifecycle.py +33 -4
- {topicforge-0.5.0 → topicforge-0.5.1}/src/topicforge/adapters/common/metrics_buffer.py +70 -20
- {topicforge-0.5.0 → topicforge-0.5.1}/src/topicforge/adapters/common/qos_analyzer.py +21 -9
- topicforge-0.5.1/src/topicforge/adapters/common/qos_endpoints.py +95 -0
- topicforge-0.5.1/src/topicforge/adapters/common/qos_normalize.py +178 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/src/topicforge/adapters/common/xtypes.py +8 -5
- {topicforge-0.5.0 → topicforge-0.5.1}/src/topicforge/adapters/dds_cyclone/adapter.py +92 -207
- {topicforge-0.5.0 → topicforge-0.5.1}/src/topicforge/adapters/dds_dust/adapter.py +2 -2
- {topicforge-0.5.0 → topicforge-0.5.1}/src/topicforge/adapters/dds_fast/adapter.py +82 -199
- {topicforge-0.5.0 → topicforge-0.5.1}/src/topicforge/adapters/dds_opendds/adapter.py +16 -10
- {topicforge-0.5.0 → topicforge-0.5.1}/src/topicforge/adapters/ros2_mock/adapter.py +3 -4
- topicforge-0.5.1/src/topicforge/constants.py +28 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/src/topicforge/services/bag_service.py +3 -13
- {topicforge-0.5.0 → topicforge-0.5.1}/src/topicforge/services/health.py +1 -1
- {topicforge-0.5.0 → topicforge-0.5.1}/src/topicforge/services/inspector.py +2 -6
- {topicforge-0.5.0 → topicforge-0.5.1}/src/topicforge/tools/handlers.py +8 -5
- {topicforge-0.5.0 → topicforge-0.5.1}/tests/integration/test_scenarios_schema.py +2 -1
- {topicforge-0.5.0 → topicforge-0.5.1}/tests/test_bag_service.py +5 -2
- {topicforge-0.5.0 → topicforge-0.5.1}/tests/test_cdr_decoder.py +34 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/tests/test_dds_helpers.py +41 -0
- topicforge-0.5.1/tests/test_dds_introspection.py +215 -0
- topicforge-0.5.1/tests/test_dds_qos_normalization.py +286 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/tests/test_health.py +1 -1
- {topicforge-0.5.0 → topicforge-0.5.1}/tests/test_lifecycle_buffer.py +38 -1
- {topicforge-0.5.0 → topicforge-0.5.1}/tests/test_metrics_buffer.py +85 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/tests/test_opendds_adapter.py +4 -2
- {topicforge-0.5.0 → topicforge-0.5.1}/tests/test_qos_analyzer.py +21 -0
- topicforge-0.5.1/tests/test_qos_endpoints.py +156 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/tests/test_tools_integration.py +23 -0
- topicforge-0.5.0/src/topicforge/services/constants.py +0 -21
- {topicforge-0.5.0 → topicforge-0.5.1}/.gitignore +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/LICENSE +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/docs/MIGRATION_v0.1_to_v0.2.md +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/docs/MIGRATION_v0.2_to_v0.3.md +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/docs/MIGRATION_v0.3_to_v0.4.md +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/docs/TESTING.md +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/docs/TROUBLESHOOTING.md +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/docs/dds-interop-matrix.md +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/docs/pro.md +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/examples/README.md +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/scripts/integration/README.md +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/src/topicforge/__main__.py +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/src/topicforge/adapters/__init__.py +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/src/topicforge/adapters/base.py +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/src/topicforge/adapters/composite.py +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/src/topicforge/adapters/dds_cyclone/__init__.py +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/src/topicforge/adapters/dds_dust/__init__.py +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/src/topicforge/adapters/dds_fast/__init__.py +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/src/topicforge/adapters/dds_opendds/__init__.py +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/src/topicforge/adapters/ros2_live/__init__.py +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/src/topicforge/adapters/ros2_live/adapter.py +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/src/topicforge/adapters/ros2_mock/__init__.py +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/src/topicforge/adapters/ros2_mock/fixtures.py +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/src/topicforge/config/__init__.py +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/src/topicforge/config/settings.py +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/src/topicforge/models/__init__.py +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/src/topicforge/models/schemas.py +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/src/topicforge/server/__init__.py +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/src/topicforge/server/app.py +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/src/topicforge/services/__init__.py +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/src/topicforge/services/factory.py +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/src/topicforge/telemetry/__init__.py +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/src/topicforge/telemetry/client.py +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/src/topicforge/tools/__init__.py +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/tests/__init__.py +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/tests/conftest.py +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/tests/fixtures/csv_echo_imu.txt +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/tests/fixtures/csv_echo_pose_multi.txt +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/tests/integration/__init__.py +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/tests/integration/conftest.py +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/tests/integration/scenarios/lifecycle_tracking.json +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/tests/integration/scenarios/multi_vendor_basic.json +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/tests/integration/scenarios/qos_mismatch_detection.json +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/tests/integration/scenarios/topic_metrics_frequency.json +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/tests/integration/scenarios/topic_metrics_sequence_gaps.json +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/tests/integration/scenarios/xtypes_decode.json +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/tests/integration/test_real_bus.py +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/tests/test_analyze_bag_multi_format.py +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/tests/test_composite_adapter.py +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/tests/test_config.py +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/tests/test_cyclone_adapter.py +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/tests/test_dds_cross_vendor.py +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/tests/test_dds_schemas.py +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/tests/test_dust_adapter.py +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/tests/test_factory.py +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/tests/test_fast_adapter.py +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/tests/test_inspector.py +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/tests/test_live_adapter_parse.py +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/tests/test_live_adapter_subprocess.py +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/tests/test_mock_adapter.py +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/tests/test_peek_bag_samples.py +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/tests/test_pro_hook.py +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/tests/test_telemetry.py +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/tests/test_topic_metrics.py +0 -0
- {topicforge-0.5.0 → topicforge-0.5.1}/tests/test_xtypes.py +0 -0
|
@@ -7,6 +7,171 @@ and this project adheres to [Semantic Versioning 2.0.0](https://semver.org/spec/
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.5.1] - 2026-08-22
|
|
11
|
+
|
|
12
|
+
### Fixed (hotfix — MCP SDK 2.0 incompatibility)
|
|
13
|
+
|
|
14
|
+
- **Hard-pinned `mcp < 2` — TopicForge was uninstallable from 2026-07-28
|
|
15
|
+
to 2026-08-22.** The MCP Python SDK released `2.0.0` on 2026-07-28
|
|
16
|
+
alongside the `2026-07-28` protocol revision. That release removes the
|
|
17
|
+
`mcp.server.fastmcp` module entirely (`mcp/server/` now ships `auth`,
|
|
18
|
+
`lowlevel`, and `mcpserver`), so `server/app.py`'s
|
|
19
|
+
`from mcp.server.fastmcp import FastMCP` raises `ImportError` against
|
|
20
|
+
it. The dependency was declared as an unbounded `mcp>=1.0.0`, so every
|
|
21
|
+
fresh `pip install topicforge` resolved to `2.0.0` and produced a
|
|
22
|
+
server that could not start. Local development and CI both masked the
|
|
23
|
+
break — the dev environment had `mcp 1.27.1` already installed, and
|
|
24
|
+
the last CI run predates the SDK release. Pinning `<2` restores
|
|
25
|
+
installability on the 1.x line; migrating to the 2.x API
|
|
26
|
+
(`FastMCP` -> `MCPServer`, transport options moved from the
|
|
27
|
+
constructor to `.run()`, stateless protocol) is tracked separately and
|
|
28
|
+
is deliberately **not** bundled into this hotfix.
|
|
29
|
+
|
|
30
|
+
### Note on scope
|
|
31
|
+
|
|
32
|
+
- This release also publishes the external-audit work (Lots 0-5,
|
|
33
|
+
2026-07-08) that had accumulated under `[Unreleased]`. The Cyclone
|
|
34
|
+
`take_iter` -> `read_iter` change documented below is still **validated
|
|
35
|
+
by static analysis only** (`ruff` + `py_compile`); it has not been
|
|
36
|
+
exercised against a real multi-vendor DDS bus. It ships here because
|
|
37
|
+
leaving the package uninstallable was the larger harm — a broken
|
|
38
|
+
install affects every user, while this change can only affect users
|
|
39
|
+
running the Cyclone backend against a live bus. Real-bus validation
|
|
40
|
+
remains open.
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
|
|
44
|
+
- **DDS pure logic extracted for testability (Lot 0, external audit
|
|
45
|
+
2026-07-08).** The QoS-profile normalizers (`_cyclone_qos_to_profile` /
|
|
46
|
+
`_fast_qos_to_profile`) and the discovery-sample field extractors
|
|
47
|
+
(`_extract_guid` / `_extract_vendor_id` / `_extract_hostname` /
|
|
48
|
+
`_extract_topic_name` / `_is_removal`) were moved out of
|
|
49
|
+
`adapters/dds_cyclone/adapter.py` and `adapters/dds_fast/adapter.py` —
|
|
50
|
+
which import their vendor binding at module top level and were therefore
|
|
51
|
+
never exercised by the test suite — into the binding-free
|
|
52
|
+
`adapters/common/qos_normalize.py` and `adapters/common/dds_introspection.py`.
|
|
53
|
+
The adapters import them back under their original private names, so every
|
|
54
|
+
call site is unchanged (verified by `ruff check` static analysis, since the
|
|
55
|
+
adapters are not importable without their SDKs). `fast_qos_to_profile`
|
|
56
|
+
takes the binding's int→str enum maps as parameters so it stays
|
|
57
|
+
import-free. This is the highest-value item from the audit: the QoS
|
|
58
|
+
normalization feeds `detect_qos_mismatches` (the flagship DDS diagnostic)
|
|
59
|
+
and was previously untestable and untested.
|
|
60
|
+
|
|
61
|
+
### Added
|
|
62
|
+
|
|
63
|
+
- `tests/test_dds_qos_normalization.py` and `tests/test_dds_introspection.py`
|
|
64
|
+
drive the extracted logic with synthetic duck-typed objects (no
|
|
65
|
+
`cyclonedds` / `fastdds` needed), including regression guards for the
|
|
66
|
+
"renamed policy key silently yields no QoS profile → no mismatch ever
|
|
67
|
+
reported" failure mode. The extracted modules are now ~91–92% covered.
|
|
68
|
+
- `pytest-cov` and `rosbags` added to the `[dev]` extra, plus `[tool.coverage]`
|
|
69
|
+
config with a `fail_under = 85` floor (the two binding-only adapter shells
|
|
70
|
+
are `omit`ted as structurally unreachable without their SDKs). Adding
|
|
71
|
+
`rosbags` un-skips the real `.db3` bag-analysis I/O test.
|
|
72
|
+
|
|
73
|
+
### Fixed
|
|
74
|
+
|
|
75
|
+
- `tests/test_bag_service.py` bag-generation helper updated for the current
|
|
76
|
+
`rosbags` API (`Writer(..., version=Writer.VERSION_LATEST)`; typestore keyed
|
|
77
|
+
by `std_msgs/msg/String`, not `std_msgs__msg__String`). The test was
|
|
78
|
+
previously auto-skipped and had gone silently stale — un-skipping it in CI
|
|
79
|
+
surfaced the drift.
|
|
80
|
+
- **`topic_metrics` frequency was wrong (Lot 2, audit C5).** It divided the
|
|
81
|
+
sample count by `(now − oldest_sample)` — folding in idle time since the
|
|
82
|
+
last peek — and counted N intervals instead of N−1. Now measured as
|
|
83
|
+
`(N−1) / (newest − oldest)` over the samples' own arrival span; samples
|
|
84
|
+
surfaced by a single opportunistic peek share one timestamp (span 0) and
|
|
85
|
+
correctly yield `frequency_hz_observed = null` instead of a fabricated rate.
|
|
86
|
+
- **`topic_metrics` sequence-gap count exploded on multi-writer topics,
|
|
87
|
+
publisher restarts, and counter wrap (Lot 2, audit C6).** Gaps are now
|
|
88
|
+
counted per writer (new best-effort `MetricsSample.writer_guid`), so
|
|
89
|
+
independent writers' counter offsets are not read as phantom gaps, and a
|
|
90
|
+
single jump wider than 10 000 is treated as a reset/wrap discontinuity
|
|
91
|
+
rather than that many losses.
|
|
92
|
+
- **QoS Deadline false negative (Lot 2, audit C3/P1-3).**
|
|
93
|
+
`detect_qos_mismatches` now flags a reader that requests a finite Deadline
|
|
94
|
+
against a writer that offers none — an absent deadline is the infinite
|
|
95
|
+
(loosest) period and cannot satisfy a finite request. The previous rule
|
|
96
|
+
required both sides non-null and silently missed this incompatibility.
|
|
97
|
+
- **`LifecycleBuffer` participant map was unbounded (Lot 3, audit P1-4 / M1 /
|
|
98
|
+
P1).** Only the event ring was capped; the participant dict grew one entry
|
|
99
|
+
per GUID ever seen (a churny bus mints a fresh RTPS GUID on each node
|
|
100
|
+
restart), and `list_participants` returned every tombstone forever. Now
|
|
101
|
+
capped at `MAX_PARTICIPANTS = 4096`, evicting `"left"` tombstones first then
|
|
102
|
+
the oldest-inserted entry — the docstring's "Bounded" claim is now true.
|
|
103
|
+
- **`MetricsBuffer` topic map was unbounded (Lot 3, audit P2-5).** Per-topic
|
|
104
|
+
rings were capped but the number of topic keys was not; now capped at
|
|
105
|
+
`MAX_TOPICS = 4096`, oldest-inserted topic evicted on overflow.
|
|
106
|
+
- **OpenDDS stub `is_available()` always returns False (Lot 4, audit S1).** A
|
|
107
|
+
stub that advertised availability (when a `pyopendds` module happened to be
|
|
108
|
+
importable) could be auto-selected by the factory, after which every tool
|
|
109
|
+
call raised. Now consistent with the Dust stub.
|
|
110
|
+
- **`iter_field_names` mis-decoded a string `__slots__` (Lot 4, audit C2).**
|
|
111
|
+
`__slots__ = "value"` was exploded into `['v','a','l','u','e']`; a bare
|
|
112
|
+
string slot is now treated as a single field name.
|
|
113
|
+
- **`decode_field_value` recursion is depth-capped at 32 (Lot 4, audit M6).**
|
|
114
|
+
A pathologically deep decoded object graph collapses to `repr()` instead of
|
|
115
|
+
risking `RecursionError`.
|
|
116
|
+
- **`_encode_raw_bytes` slices before hex-encoding (Lot 4, audit M5).** A large
|
|
117
|
+
raw payload no longer allocates its full 2×-size hex string only to truncate
|
|
118
|
+
it to the 4096-char preview.
|
|
119
|
+
|
|
120
|
+
### Added (Lot 4 — test hardening)
|
|
121
|
+
|
|
122
|
+
- End-to-end test that a failing tool call surfaces as an MCP error
|
|
123
|
+
(`ToolError`) rather than being masked as a success — pins the thin-handler
|
|
124
|
+
contract (CLAUDE.md §8, audit test-gap #4).
|
|
125
|
+
- Test pinning that every canonical vendor tag is a valid
|
|
126
|
+
`ParticipantInfo`/`ParticipantEvent.vendor` Literal (guards against
|
|
127
|
+
vendor-map ↔ schema drift, audit P2-3). Scenario allowlist `_KNOWN_TOOLS`
|
|
128
|
+
now includes the 11th tool `peek_bag_samples`.
|
|
129
|
+
|
|
130
|
+
### Removed (Lot 4)
|
|
131
|
+
|
|
132
|
+
- Dead `annotate_full` / `annotate_partial` imports and the `_ = (...)`
|
|
133
|
+
unused-suppressor from `services/bag_service.py`.
|
|
134
|
+
|
|
135
|
+
### Documentation (Lot 1 — reconcile the strategic source of truth)
|
|
136
|
+
|
|
137
|
+
- **`docs/product-plan.md` realigned on the shipped 11-tool surface (audit
|
|
138
|
+
M6/P1-6).** §1 and §4 said "five typed tools today" / DDS "roadmapped"
|
|
139
|
+
while six DDS/observability tools had shipped across v0.2.0–v0.4.0. §11's
|
|
140
|
+
risk register carried a self-imposed governance gate — "any 9th tool needs
|
|
141
|
+
an explicit re-scope discussion documented in this register before code
|
|
142
|
+
lands" — that was crossed during v0.4.0 without the discussion being
|
|
143
|
+
recorded. Added a retroactive re-scope decision closing that gap: the three
|
|
144
|
+
ceiling-breaking tools are accepted, the new ceiling is 11 tools, a 12th
|
|
145
|
+
needs a documented re-scope.
|
|
146
|
+
- **User-topic `raw` decode honesty (audit C1).** README and the
|
|
147
|
+
`peek_dds_samples` tool description no longer imply the `raw` fallback
|
|
148
|
+
preserves the payload in `_raw_bytes_hex` — on the current user-topic raw
|
|
149
|
+
path that field is empty (a `raw` status means "present but not decoded").
|
|
150
|
+
Capturing the on-wire CDR bytes is stated as roadmapped rather than done.
|
|
151
|
+
|
|
152
|
+
### Changed (Lot 5 — DDS adapter deduplication)
|
|
153
|
+
|
|
154
|
+
- **QoS-mismatch endpoint pairing deduplicated (audit D1/M7/P2).** The ~40
|
|
155
|
+
identical lines in each adapter's `detect_qos_mismatches` (group endpoints by
|
|
156
|
+
topic, pair reader × writer, build `MismatchReport`) moved to the binding-free
|
|
157
|
+
`common/qos_endpoints.detect_mismatches_across_endpoints`, unit-tested without
|
|
158
|
+
a binding. Each writer's QoS profile is now parsed once per topic instead of
|
|
159
|
+
once per reader (fixes the O(readers × writers) re-parse). Both adapters
|
|
160
|
+
delegate to it.
|
|
161
|
+
- **Shared `validate_domain_id` (`common/dds_helpers`).** The identical 0..232
|
|
162
|
+
bound check in all four DDS adapter constructors (Cyclone, Fast, OpenDDS,
|
|
163
|
+
Dust) is now defined once.
|
|
164
|
+
- **Cyclone discovery/sample reads switched from `take_iter` to `read_iter`
|
|
165
|
+
(audit A1/P1-5).** Destructive `take` drained the builtin discovery cache,
|
|
166
|
+
risking spurious lost / re-discovered participant flapping across polls;
|
|
167
|
+
`read` is non-destructive — the correct choice for read-only observability.
|
|
168
|
+
⚠️ **Requires real-bus validation on `scripts/integration/` before release**:
|
|
169
|
+
the read-vs-take semantics cannot be exercised without `cyclonedds` installed
|
|
170
|
+
(the adapter is not importable in the unit environment; these edits are
|
|
171
|
+
validated only by `ruff` static analysis + `py_compile`).
|
|
172
|
+
|
|
173
|
+
Baseline: 399 → 485 passed, 24 → 23 skipped, ruff clean, coverage 89.12%.
|
|
174
|
+
|
|
10
175
|
## [0.5.0] - 2026-05-21
|
|
11
176
|
|
|
12
177
|
### Sprint v0.5.0 — Polish + validation (pre-marketing-publication)
|
|
@@ -713,7 +878,8 @@ Initial MVP release of TopicForge — ROS Topic Inspector & Bag Analyzer MCP ser
|
|
|
713
878
|
- The write path (publishing, commanding robots) is intentionally out of scope for the MVP.
|
|
714
879
|
- `analyze_bag` in live mode parses `ros2 bag info` text output; deeper anomaly detection remains mock-only for now.
|
|
715
880
|
|
|
716
|
-
[Unreleased]: https://github.com/yaniswav/TopicForge/compare/v0.5.
|
|
881
|
+
[Unreleased]: https://github.com/yaniswav/TopicForge/compare/v0.5.1...HEAD
|
|
882
|
+
[0.5.1]: https://github.com/yaniswav/TopicForge/compare/v0.5.0...v0.5.1
|
|
717
883
|
[0.5.0]: https://github.com/yaniswav/TopicForge/compare/v0.4.0...v0.5.0
|
|
718
884
|
[0.4.0]: https://github.com/yaniswav/TopicForge/compare/v0.3.0...v0.4.0
|
|
719
885
|
[0.3.0]: https://github.com/yaniswav/TopicForge/compare/v0.2.0...v0.3.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: topicforge
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.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
|
|
@@ -39,7 +39,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
39
39
|
Classifier: Programming Language :: Python :: 3.12
|
|
40
40
|
Classifier: Programming Language :: Python :: 3.13
|
|
41
41
|
Requires-Python: >=3.11
|
|
42
|
-
Requires-Dist: mcp
|
|
42
|
+
Requires-Dist: mcp<2,>=1.0.0
|
|
43
43
|
Requires-Dist: pydantic>=2.6
|
|
44
44
|
Provides-Extra: all
|
|
45
45
|
Requires-Dist: cyclonedds>=0.10; extra == 'all'
|
|
@@ -63,7 +63,9 @@ Requires-Dist: fastdds<3,>=2.6.1; extra == 'dds-fast'
|
|
|
63
63
|
Provides-Extra: dds-opendds
|
|
64
64
|
Requires-Dist: pyopendds>=0.1; extra == 'dds-opendds'
|
|
65
65
|
Provides-Extra: dev
|
|
66
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
66
67
|
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
68
|
+
Requires-Dist: rosbags>=0.9; extra == 'dev'
|
|
67
69
|
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
68
70
|
Description-Content-Type: text/markdown
|
|
69
71
|
|
|
@@ -254,7 +256,7 @@ Six DDS / observability tools (in addition to the five ROS2 tools above) :
|
|
|
254
256
|
|
|
255
257
|
**Composite adapter (v0.4.0 Phase 1+).** When `TOPICFORGE_MODE=live` is paired with a DDS backend (`cyclone`, `fast`, …), TopicForge instantiates **both** a ROS2 CLI adapter and the chosen DDS adapter and routes per-tool category — the 5 ROS2 tools hit the CLI, the DDS / observability tools hit the DDS backend. ROS2-only or DDS-only setups still work — the missing half is skipped and the present half serves what it can. The mock backend continues to expose all 11 tools against deterministic fixtures for local development.
|
|
256
258
|
|
|
257
|
-
**`peek_dds_samples` payload shape (v0.4.0 Phase 1.5).** Full-fidelity on the 4 builtin DCPS topics (`DCPSParticipant`, `DCPSSubscription`, `DCPSPublication`). Arbitrary user topics return best-effort decoded samples with a `_decode_status` annotation : `"full"` (every IDL field decoded — currently a v0.4.0+ Cyclone XTypes path), `"partial"` (some fields decoded, others opaque), or `"raw"` (binding could not resolve the dynamic XTypes
|
|
259
|
+
**`peek_dds_samples` payload shape (v0.4.0 Phase 1.5).** Full-fidelity on the 4 builtin DCPS topics (`DCPSParticipant`, `DCPSSubscription`, `DCPSPublication`). Arbitrary user topics return best-effort decoded samples with a `_decode_status` annotation : `"full"` (every IDL field decoded — currently a v0.4.0+ Cyclone XTypes path), `"partial"` (some fields decoded, others opaque), or `"raw"` (binding could not resolve the dynamic XTypes). The diagnostic key `_decode_note` carries a short explanation when the status is non-`full`. The wire shape is identical across Cyclone and Fast backends. **Caveat (v0.5.x):** on the current user-topic *raw* path `_raw_bytes_hex` is **empty** — a `"raw"` status means "topic present on the bus but not decoded", not "here are the serialized bytes to re-decode". Capturing the on-wire CDR bytes into the fallback is roadmapped ; until then use the Cyclone full/partial XTypes path for actual user-topic payloads. The 4 builtin DCPS topics are unaffected (always structured).
|
|
258
260
|
|
|
259
261
|
**`RTI Connext`** is v0.4.0+ Pro tier (BYO license — see `docs/pro.md`).
|
|
260
262
|
|
|
@@ -185,7 +185,7 @@ Six DDS / observability tools (in addition to the five ROS2 tools above) :
|
|
|
185
185
|
|
|
186
186
|
**Composite adapter (v0.4.0 Phase 1+).** When `TOPICFORGE_MODE=live` is paired with a DDS backend (`cyclone`, `fast`, …), TopicForge instantiates **both** a ROS2 CLI adapter and the chosen DDS adapter and routes per-tool category — the 5 ROS2 tools hit the CLI, the DDS / observability tools hit the DDS backend. ROS2-only or DDS-only setups still work — the missing half is skipped and the present half serves what it can. The mock backend continues to expose all 11 tools against deterministic fixtures for local development.
|
|
187
187
|
|
|
188
|
-
**`peek_dds_samples` payload shape (v0.4.0 Phase 1.5).** Full-fidelity on the 4 builtin DCPS topics (`DCPSParticipant`, `DCPSSubscription`, `DCPSPublication`). Arbitrary user topics return best-effort decoded samples with a `_decode_status` annotation : `"full"` (every IDL field decoded — currently a v0.4.0+ Cyclone XTypes path), `"partial"` (some fields decoded, others opaque), or `"raw"` (binding could not resolve the dynamic XTypes
|
|
188
|
+
**`peek_dds_samples` payload shape (v0.4.0 Phase 1.5).** Full-fidelity on the 4 builtin DCPS topics (`DCPSParticipant`, `DCPSSubscription`, `DCPSPublication`). Arbitrary user topics return best-effort decoded samples with a `_decode_status` annotation : `"full"` (every IDL field decoded — currently a v0.4.0+ Cyclone XTypes path), `"partial"` (some fields decoded, others opaque), or `"raw"` (binding could not resolve the dynamic XTypes). The diagnostic key `_decode_note` carries a short explanation when the status is non-`full`. The wire shape is identical across Cyclone and Fast backends. **Caveat (v0.5.x):** on the current user-topic *raw* path `_raw_bytes_hex` is **empty** — a `"raw"` status means "topic present on the bus but not decoded", not "here are the serialized bytes to re-decode". Capturing the on-wire CDR bytes into the fallback is roadmapped ; until then use the Cyclone full/partial XTypes path for actual user-topic payloads. The 4 builtin DCPS topics are unaffected (always structured).
|
|
189
189
|
|
|
190
190
|
**`RTI Connext`** is v0.4.0+ Pro tier (BYO license — see `docs/pro.md`).
|
|
191
191
|
|
|
@@ -95,7 +95,7 @@ Both real backends and the mock fixtures encode the canonical "subscriber doesn'
|
|
|
95
95
|
|
|
96
96
|
An LLM reading this output has enough information to suggest a concrete fix ("the writer is BEST_EFFORT but the reader requires RELIABLE — either relax the reader or upgrade the writer"). That is the diagnostic loop the DDS module is designed to support — and it works identically regardless of which backend produced the discovery samples, because the vendor-neutral pure analyzer at `src/topicforge/adapters/common/qos_analyzer.py` operates on canonical `QosProfile` Pydantic models.
|
|
97
97
|
|
|
98
|
-
The analyzer covers the four MVP policies — **Reliability**, **Durability**, **History**, **Deadline** — that explain the bulk of real-world mismatch cases. Liveliness, Ownership, Partition, TimeBasedFilter, and LatencyBudget are v0.
|
|
98
|
+
The analyzer covers the four MVP policies — **Reliability**, **Durability**, **History**, **Deadline** — that explain the bulk of real-world mismatch cases. Liveliness, Ownership, Partition, TimeBasedFilter, and LatencyBudget are v0.5.x patches.
|
|
99
99
|
|
|
100
100
|
---
|
|
101
101
|
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# TopicForge — Tutorial
|
|
2
|
+
|
|
3
|
+
TopicForge is a read-only Model Context Protocol (MCP) server that gives an AI agent grounded, structured visibility into a ROS2 robotics stack and the raw DDS layer beneath it — topics, participants, QoS, recorded bags — without ever being able to publish, call a service, or command a robot.
|
|
4
|
+
|
|
5
|
+
**Who this is for.** ROS2 developers, robotics ML/CV engineers, and anyone who wants their AI assistant to answer "what's actually happening on my robot's graph right now" instead of guessing from training data.
|
|
6
|
+
|
|
7
|
+
**The read-only guarantee, in one sentence.** There is no write path anywhere in TopicForge's architecture — not a locked-down permission you could misconfigure, but code that was never written, so there is nothing to flip and nothing to exploit into a write.
|
|
8
|
+
|
|
9
|
+
This tutorial covers installing TopicForge, using its eleven tools, and wiring it into a recurring monitoring workflow. For OS-by-OS environment setup (WSL2, native Linux, Docker, native Windows), see [`TESTING.md`](TESTING.md).
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Quickstart
|
|
14
|
+
|
|
15
|
+
Requires Python 3.11+. This section needs no ROS2 install — the mock adapter serves deterministic fixtures for a small demo robot, so you can try every tool cold.
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install topicforge
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Add it to your MCP client. For Claude Desktop, edit `claude_desktop_config.json`:
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"mcpServers": {
|
|
26
|
+
"topicforge": {
|
|
27
|
+
"command": "topicforge",
|
|
28
|
+
"env": { "TOPICFORGE_MODE": "mock" }
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Restart Claude Desktop. All eleven tools appear under the hammer icon. Ask something like:
|
|
35
|
+
|
|
36
|
+
> What topics are being published right now, and what message types do they carry?
|
|
37
|
+
|
|
38
|
+
The agent calls `list_topics`. In mock mode you'll see the five fixture topics of the demo robot — `/cmd_vel`, `/odom`, `/scan`, `/tf`, `/camera/image_raw` — deterministic across runs, so you can build a mental model of the tool surface before pointing it at a real graph.
|
|
39
|
+
|
|
40
|
+
When you're ready for a real ROS2 environment, switch `TOPICFORGE_MODE` to `live` or `auto` — see [Advanced options](#advanced-options) below, and [`TESTING.md`](TESTING.md) for full setup paths per OS.
|
|
41
|
+
|
|
42
|
+
**Windows note.** File paths in this document are shown in POSIX style (`/tmp/demo.mcap`) because that's the form MCP clients pass internally, but TopicForge itself runs natively on Windows. Path resolution goes through `pathlib`, so both `C:\demos\run.mcap` and `C:/demos/run.mcap` work when you pass a bag path yourself.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## The eleven tools
|
|
47
|
+
|
|
48
|
+
| Tool | What it does | When to use it |
|
|
49
|
+
| --- | --- | --- |
|
|
50
|
+
| `health_check` | Reports effective mode (`live`/`mock`), whether `ros2` is on PATH, the active DDS backend, and other environment state. Always succeeds. | Call first when something looks wrong — every other tool can raise an error, this one won't. |
|
|
51
|
+
| `list_topics` | Lists every topic on the current ROS2 graph. | Discover what's currently being published before drilling into anything specific. |
|
|
52
|
+
| `get_topic_info` | Structured detail for one topic — message type, publisher/subscriber counts, QoS reliability. | Check a topic's shape and who's connected to it before subscribing or debugging. |
|
|
53
|
+
| `sample_messages` | Peeks recent messages on a ROS2 topic. | See real payload content without shelling out to `ros2 topic echo` yourself. |
|
|
54
|
+
| `analyze_bag` | Summarizes a `.mcap` / `.db3` / `.bag` recording — duration, message count, per-topic stats. | Get a quick overview of a recorded run before deciding whether to dig deeper. |
|
|
55
|
+
| `list_participants` | Lists DDS participants observed on a domain, at the raw DDS layer beneath ROS. | Diagnose why a participant isn't visible to the ROS graph, or inspect a non-ROS DDS stack. |
|
|
56
|
+
| `detect_qos_mismatches` | Finds incompatible QoS pairs between DDS readers and writers. | A subscriber isn't receiving despite an apparently healthy publisher — this tells you why. |
|
|
57
|
+
| `peek_dds_samples` | Peeks raw DDS samples, including the DCPS builtin discovery topics and (best-effort decoded) user topics. | Inspect non-ROS DDS topics, or the discovery layer itself. |
|
|
58
|
+
| `participant_events` | Timeline of participant discovered/lost events over a lookback window. | Investigate churn — nodes restarting, dropping off, or new ones joining. |
|
|
59
|
+
| `topic_metrics` | Observed frequency, latency percentiles, and sequence gaps for a topic over a time window. | Check whether a topic is actually meeting its declared publish rate or QoS Deadline. |
|
|
60
|
+
| `peek_bag_samples` | Peeks recent samples from a recorded bag file (MCAP, `.db3`, or legacy ROS1 `.bag`). | Inspect payload content from a past recording without replaying the whole bag. |
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Advanced options
|
|
65
|
+
|
|
66
|
+
### Environment variables
|
|
67
|
+
|
|
68
|
+
| Variable | Default | Purpose |
|
|
69
|
+
| --- | --- | --- |
|
|
70
|
+
| `TOPICFORGE_MODE` | `auto` | Selects `mock`, `live`, or `auto` (see below). |
|
|
71
|
+
| `TOPICFORGE_LOG_LEVEL` | `INFO` | `DEBUG`, `INFO`, `WARNING`, or `ERROR`. |
|
|
72
|
+
| `TOPICFORGE_ROS2_BIN` | `ros2` | Overrides the resolved `ros2` executable — useful when it isn't on PATH. |
|
|
73
|
+
| `TOPICFORGE_TELEMETRY` | off | Opt-in anonymous telemetry. On-values: `on`, `1`, `true`, `yes`, `enabled`. See [Privacy](#privacy). |
|
|
74
|
+
| `TOPICFORGE_DDS_BACKEND` | `mock` | Selects the DDS backend: `mock`, `cyclone`, `fast`, or `auto`. A few more identifiers exist in the settings schema (`rti`, `opensplice`, `coredx`, `intercom`, `opendds`, `dust`) — see [Choosing a DDS backend](#choosing-a-dds-backend). |
|
|
75
|
+
| `TOPICFORGE_DDS_DOMAIN_ID` | `0` | DDS domain id to observe, `0`–`232`. |
|
|
76
|
+
|
|
77
|
+
Add any of these to the same `env` object shown in the Quickstart config block.
|
|
78
|
+
|
|
79
|
+
### The three modes
|
|
80
|
+
|
|
81
|
+
- **`mock`** — deterministic fixtures, no ROS2 or DDS SDK required. Use for development, demos, CI, or evaluating the tool surface before touching a real graph.
|
|
82
|
+
- **`live`** — talks to a real environment: the `ros2` CLI for the 5 ROS2 graph tools, and the configured DDS backend for the 6 DDS/observability tools. Use once ROS2 and/or a DDS backend are actually reachable from the shell that spawns TopicForge.
|
|
83
|
+
- **`auto`** (default) — resolves to `live` if the `ros2` executable is found on PATH, otherwise falls back to `mock`. The DDS backend has its own `auto` resolution (see below), independent of `TOPICFORGE_MODE`.
|
|
84
|
+
|
|
85
|
+
### DDS extras, per vendor
|
|
86
|
+
|
|
87
|
+
Plain `pip install topicforge` gives you the 5 ROS2 tools plus mock fixtures for all eleven. To talk to a real DDS bus, install one of:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
pip install topicforge[dds-cyclone] # Eclipse CycloneDDS only
|
|
91
|
+
pip install topicforge[dds-fast] # eProsima Fast DDS only
|
|
92
|
+
pip install topicforge[dds] # both — the recommended default
|
|
93
|
+
pip install topicforge[all] # currently equivalent to [dds]
|
|
94
|
+
pip install topicforge[bags] # rosbags library — required for peek_bag_samples (no fallback);
|
|
95
|
+
# analyze_bag falls back to `ros2 bag info` text parsing without it
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`topicforge[dds-opendds]` and `topicforge[dds-dust]` also exist in `pyproject.toml`, but as of this writing they pin `pyopendds` and `dust-dds-python` — packages not currently maintained on PyPI. Installing either extra fails at `pip install` time; they exist so the auto-detect framework has a known module name to probe once upstream ships a working release. Don't rely on them yet.
|
|
99
|
+
|
|
100
|
+
### Choosing a DDS backend
|
|
101
|
+
|
|
102
|
+
Set `TOPICFORGE_DDS_BACKEND` explicitly (`cyclone` or `fast`) if you have a preference and both are installed, or leave it on `auto` and let TopicForge pick. In practice, for the free tier `auto` resolves to Fast DDS if installed, otherwise CycloneDDS if installed, otherwise `mock`. The full priority chain also probes Pro-tier vendors (`rti`, `opensplice`, `coredx`, `intercom`) first, but only if you've separately installed the `topicforge-pro` add-on — without it, those probes are skipped automatically and fall through to the OSS vendors. Selecting `rti` explicitly without the Pro add-on falls back to the ROS2-CLI-only adapter with a logged warning rather than failing outright. `opendds` and `dust` are recognized identifiers with no working install path today (see the extras caveat above).
|
|
103
|
+
|
|
104
|
+
### Domain id
|
|
105
|
+
|
|
106
|
+
`TOPICFORGE_DDS_DOMAIN_ID` (default `0`, matching the default used by most ROS2 setups and by `cyclonedds` itself) sets the domain a live DDS adapter joins **at startup** — it does not change per call. `list_participants`, `participant_events`, and `topic_metrics` also accept a `domain_id` parameter, but on the real Cyclone/Fast adapters this parameter is accepted for protocol uniformity only; the adapter still reports what it sees on the domain it joined at construction time, not a different domain picked per call. To observe a different domain, restart the server with a different `TOPICFORGE_DDS_DOMAIN_ID`.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Scheduled-task prompts
|
|
111
|
+
|
|
112
|
+
TopicForge's tools are read-only, which makes them safe to run unattended on a schedule — a cron-triggered agent session, a scheduled task in whatever orchestrator you use, or a recurring reminder in your MCP client. The prompts below are generic: swap in your own topic names, domain ids, and bag paths. They're written as instructions to hand an agent, not as commands to run yourself.
|
|
113
|
+
|
|
114
|
+
**Catches:** silent reader/writer QoS incompatibilities that block delivery — introduced by a new node, a config change, or a vendor default drifting from the rest of the bus.
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
Run detect_qos_mismatches across the whole bus (no topic filter). For every
|
|
118
|
+
result with severity "incompatible", explain in plain language which QoS
|
|
119
|
+
policy is blocking delivery, which side (reader or writer) is the outlier,
|
|
120
|
+
and the smallest concrete change that would fix it (e.g. "relax the reader
|
|
121
|
+
to BEST_EFFORT" or "raise the writer to TRANSIENT_LOCAL durability"). List
|
|
122
|
+
any "risky" (non-blocking) mismatches separately as a lower-priority note.
|
|
123
|
+
If nothing is found, say so in one line — do not pad the report.
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**Catches:** environment drift before you trust the stack in the field — wrong mode silently active, `ros2` not actually on PATH, DDS backend quietly falling back to mock.
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
Before I start today's run, call health_check and confirm: the effective
|
|
130
|
+
mode is "live" (not silently "mock"), the ros2 CLI is detected, and the
|
|
131
|
+
active DDS backend matches what I expect. Then call list_topics and
|
|
132
|
+
list_participants(domain_id=0), and tell me if the topic count or
|
|
133
|
+
participant count looks abnormally low compared to a normal startup.
|
|
134
|
+
Flag anything that looks like a partial or degraded environment before
|
|
135
|
+
I rely on it.
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
**Catches:** flapping nodes (crash-restart loops), unexpected disconnects mid-run, or a new participant joining the bus that nobody deployed on purpose.
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
Call participant_events(domain_id=0, lookback_seconds=3600) and summarize
|
|
142
|
+
the last hour of DDS participant activity. Group by guid: which
|
|
143
|
+
participants were discovered then lost more than once (possible crash
|
|
144
|
+
loop), which are new, and which have been stable the whole window. Call
|
|
145
|
+
out anything under a hostname or vendor you don't recognize.
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**Catches:** a critical topic silently dropping below its declared publish rate, latency creeping up, or sequence gaps appearing — degradation that doesn't throw an error but breaks downstream consumers.
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
For each of these topics: <topic-1>, <topic-2>, <topic-3> — first call
|
|
152
|
+
peek_dds_samples(topic=<topic>, count=10) to warm up the metrics buffer,
|
|
153
|
+
then call topic_metrics(topic=<topic>, window_seconds=60) and report
|
|
154
|
+
frequency_hz_observed against frequency_hz_declared, the p50/p95/p99
|
|
155
|
+
latency, and sequence_gaps_count. Flag any topic where the observed
|
|
156
|
+
frequency is more than 20% below the declared rate, or where
|
|
157
|
+
sequence_gaps_count is greater than zero.
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
**Catches:** recorded test or field runs that only get inspected reactively after something breaks, instead of on a regular cadence — letting anomalies (duration mismatches, missing topics, gaps) surface while they're still cheap to investigate.
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
Analyze the bag at <path-to-bag>. Report duration, total message count,
|
|
164
|
+
and per-topic message counts. Then peek 5 samples from <topic-of-interest>
|
|
165
|
+
in that bag and tell me if the payload shape looks consistent with a
|
|
166
|
+
normal run. Flag anything anomalous: missing topics you'd expect to see,
|
|
167
|
+
a duration much shorter or longer than usual, or a topic with a
|
|
168
|
+
suspiciously low message count for its expected rate.
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Privacy
|
|
174
|
+
|
|
175
|
+
TopicForge is built so the question "what does this send off my machine?" has a short, verifiable answer.
|
|
176
|
+
|
|
177
|
+
### Telemetry
|
|
178
|
+
|
|
179
|
+
Telemetry is **off by default**. It's opt-in via `TOPICFORGE_TELEMETRY=on` (accepted on-values: `on`, `1`, `true`, `yes`, `enabled` — anything else, including an unset variable, stays off).
|
|
180
|
+
|
|
181
|
+
When enabled, each tool call emits exactly six fields: `tool_name`, `latency_ms`, `mode`, `version`, `session_id`, `success`. `session_id` is a random id generated per server process — it isn't tied to your identity or any persistent identifier, and it's never written to disk.
|
|
182
|
+
|
|
183
|
+
When disabled — the default — this isn't "we choose not to send it," it's "the code path doesn't exist." The instrumentation wrapper returns the tool handler unmodified: no event object is built, no transport is constructed, no network code executes. This no-op behavior is covered by a dedicated test in the codebase.
|
|
184
|
+
|
|
185
|
+
### What never leaves your machine
|
|
186
|
+
|
|
187
|
+
Regardless of the telemetry setting, TopicForge never transmits topic names, message payloads, bag file paths or contents, hostnames, or any other environment variable. The service and adapter layers that see this data have no code path into the telemetry module — by construction, not just by convention.
|
|
188
|
+
|
|
189
|
+
### Bags are read locally
|
|
190
|
+
|
|
191
|
+
`analyze_bag` and `peek_bag_samples` open bag files on the filesystem where TopicForge runs and parse them in-process — via `ros2 bag info` in live mode, or the `rosbags` library when installed (`pip install topicforge[bags]`). Nothing about a bag's content or path is uploaded anywhere.
|
|
192
|
+
|
|
193
|
+
### Read-only, restated
|
|
194
|
+
|
|
195
|
+
The same architectural property that keeps TopicForge from commanding your robot also bounds what it can leak: there is no write path anywhere in the codebase — not to the bus, not to a remote endpoint — other than the six-field telemetry event described above, and that event stays off unless you explicitly turn it on.
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## Where to go next
|
|
200
|
+
|
|
201
|
+
- [`DDS_QUICKSTART.md`](DDS_QUICKSTART.md) — a deeper tour of the DDS module: mock vs. live walkthroughs per backend, the QoS mismatch scenario end-to-end, and the composite-adapter routing table.
|
|
202
|
+
- [`TESTING.md`](TESTING.md) — five paths to a working ROS2 environment (WSL2, native Linux, Docker, native Windows), plus MCP client wiring for Claude Desktop, Claude Code, and others.
|
|
203
|
+
- [`TROUBLESHOOTING.md`](TROUBLESHOOTING.md) — the codebase's polished error messages, what they mean, and how to fix them.
|
|
204
|
+
- [`dds-interop-matrix.md`](dds-interop-matrix.md) — why TopicForge sees participants from any OMG-DDS-RTPS-conformant vendor, not just the one it's bound to.
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
TopicForge is **the safety-first read-only MCP for ROS2 robotics**. Where general-purpose ROS-MCP servers let an LLM publish topics, call services, and command robots — useful for demos, untenable for production fleets, defense systems, or anything safety-certified — TopicForge is read-only by **architecture**, not by configuration. There is no write path to misconfigure, no permission system to audit, no liability conversation to have. The MCP client can see the robot stack; it cannot touch it.
|
|
10
10
|
|
|
11
|
-
Concretely, the server exposes
|
|
11
|
+
Concretely, the server exposes **eleven typed read-only tools today** (v0.5.0): the five ROS2-graph tools (`health_check`, `list_topics`, `get_topic_info`, `sample_messages`, `analyze_bag`) plus the six DDS / observability tools shipped across v0.2.0–v0.4.0 (`list_participants`, `detect_qos_mismatches`, `peek_dds_samples`, `participant_events`, `topic_metrics`, `peek_bag_samples`). They are backed by a deterministic mock adapter (no ROS2/DDS required), a `ros2` CLI wrapper, or an OSS DDS participant (Eclipse CycloneDDS / eProsima Fast DDS). Outputs are frozen Pydantic schemas, stable across runtime modes. Telemetry is opt-in, six fields, zero user payload.
|
|
12
12
|
|
|
13
13
|
The ROS-MCP category is no longer empty (see §11 Risk register for the competitive landscape as of 2026-05-13). What TopicForge defends, and the rest of the pack will inherit, is the read-only-by-architecture stance and the production-quality engineering envelope around it — frozen schemas, mock-first development, telemetry contract pinned by tests, Windows-first cross-platform, no shell injection, deterministic outputs.
|
|
14
14
|
|
|
@@ -40,13 +40,13 @@ Three concentric circles, ranked by strategic priority rather than acquisition c
|
|
|
40
40
|
|
|
41
41
|
The strategic bet is **pack breadth via two focused products plus a modular surface inside TopicForge**. Two MCPs, not three to five — solo maintenance cost was the binding constraint and the 2026-05-14 audit collapsed the earlier 3-to-5-MCP plan accordingly.
|
|
42
42
|
|
|
43
|
-
**MCP 01 — TopicForge umbrella.** Covers ROS2 introspection
|
|
43
|
+
**MCP 01 — TopicForge umbrella.** Covers ROS2 introspection (shipped v0.1.2) and DDS observability, which **shipped as a module across v0.2.0–v0.4.0** (see §8) — no longer roadmapped. The `RosAdapter` protocol was generalized into a `MiddlewareAdapter` protocol that supports Eclipse CycloneDDS and eProsima Fast DDS in the OSS core and RTI Connext under the existing `topicforge_pro` license-gated package. One install (`pip install topicforge`, optional extras for DDS), one CLI, one license key — the umbrella keeps the developer ergonomics tight while extending coverage to the DDS-native audience the ROS-MCP competitive set does not reach. Module spec at `docs/projet-file/mcp-02-spec.md`.
|
|
44
44
|
|
|
45
45
|
**MCP 02 — DatasetForge.** Vision Dataset Inspector. Read images + annotations (COCO at MVP; YOLO / HF Datasets on roadmap) and answer structured questions about class balance, split coherence, annotation quality. Targets the ML/CV audience overlapping with TopicForge but distinct enough in domain (training data vs runtime graph) to warrant a separate product, separate repo, separate PyPI name. Full spec at `docs/projet-file/mcp-03-spec.md` (the file is still named `mcp-03-spec.md` for historical continuity ; the slot is MCP 02 of the 2-MCP pack).
|
|
46
46
|
|
|
47
47
|
**Motif of the pivot.** Earlier drafts of this plan sequenced a 3-to-5-MCP pack with a separate DDS observability MCP as MCP 02. The 2026-05-14 audit collapsed that into a 2-product strategy: TopicForge as an umbrella covering both middlewares, DatasetForge as the second standalone product. The binding constraints were (a) solo-maintenance cost of running two repos in parallel and (b) the fact that ROS2 and DDS are the same problem shape — a typed pub/sub graph that needs structured introspection — and the `RosAdapter` protocol already generalizes to a `MiddlewareAdapter` superset with zero rework. Two products instead of three reduces the surface area without losing coverage.
|
|
48
48
|
|
|
49
|
-
The umbrella commits TopicForge to a
|
|
49
|
+
The umbrella commits TopicForge to a broader scope than first drafted: the DDS module shipped **six** DDS / observability tools across v0.2.0–v0.4.0 (not the three originally scoped), taking the surface to **11 tools total**. The original 8-tool ceiling was formally revised — see the re-scope decision in §11. The pack inherits the layer separation, mock-first development, opt-in telemetry, and read-only-by-architecture commitments from TopicForge. Pack-shared infrastructure extraction (telemetry, license, settings resolver into a `pack-template/` repo) becomes a non-decision at 2 products: fork-and-tweak from TopicForge to DatasetForge is acceptable ; revisit only if a third product is ever planned.
|
|
50
50
|
|
|
51
51
|
---
|
|
52
52
|
|
|
@@ -171,7 +171,7 @@ The risks worth tracking explicitly. Updated 2026-05-13 with the competitive lan
|
|
|
171
171
|
- **Cross-platform regressions on Windows.** TopicForge's primary developer environment is Windows. The Makefile uses POSIX shell syntax; users on plain PowerShell need the documented escape hatches. Mitigation: tested directly in CI on `ubuntu-latest` only today; Windows coverage is documented in `docs/TESTING.md` and exercised manually before each release.
|
|
172
172
|
- **Telemetry trust.** Even opt-in telemetry can damage trust if the payload contract drifts. Mitigation: `tests/test_telemetry.py::test_payload_contains_only_whitelisted_keys` pins the six allowed keys. Any change requires a CHANGELOG entry and a README Telemetry section update in the same PR.
|
|
173
173
|
- **Time / focus dilution.** A solo maintainer trying to drive two products (TopicForge umbrella + DatasetForge), a Pro tier inside each, marketing, and the DDS module on top of TopicForge is the realistic risk. The 2026-05-14 pivot from a 3-to-5-MCP pack to a 2-product strategy reduced the surface but did not eliminate the risk. Mitigation: explicit phase gates (do not start Phase 2 until Phase 1 is shipped, do not act on the DDS module marketing until Phase 2 has shipped) — though §8 schedules `MiddlewareAdapter` protocol prep during Phase 1.
|
|
174
|
-
- **Scope creep within the TopicForge umbrella.** Combining ROS2 + DDS introspection in one product risks bloating the tool surface beyond what a focused MCP should expose.
|
|
174
|
+
- **Scope creep within the TopicForge umbrella.** Combining ROS2 + DDS introspection in one product risks bloating the tool surface beyond what a focused MCP should expose. **Re-scope decision (2026-07-08, ratified retroactively).** The register's original ceiling — 5 ROS2 tools + at most 3 DDS tools, any 9th tool gated on a re-scope discussion documented *here* before code lands — was crossed during v0.4.0 **without that discussion being recorded in this register**, a governance gap surfaced by the 2026-07-08 external audit. The three tools that broke it are deliberate and were acknowledged in the CHANGELOG and `docs/projet-file/mcp-02-spec.md §2` at ship time: `participant_events` (9th, v0.4.0 Phase 1), `topic_metrics` (10th, Phase 2), `peek_bag_samples` (11th, Phase 3). They are accepted; the revised ceiling is **11 tools**. A 12th tool now needs an explicit re-scope discussion documented in this register before code lands. Mitigation going forward: the `verify-change` skill's doc-drift step and the `docs-curator` sweep keep this register, `README.md`, and `CLAUDE.md` in sync so a ceiling break cannot ship undocumented again.
|
|
175
175
|
|
|
176
176
|
---
|
|
177
177
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "topicforge"
|
|
7
|
-
version = "0.5.
|
|
7
|
+
version = "0.5.1"
|
|
8
8
|
description = "ROS Topic Inspector & Bag Analyzer MCP server for AI agents"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -22,14 +22,20 @@ classifiers = [
|
|
|
22
22
|
"Programming Language :: Python :: 3.13",
|
|
23
23
|
]
|
|
24
24
|
dependencies = [
|
|
25
|
-
"mcp>=1.0.0",
|
|
25
|
+
"mcp>=1.0.0,<2",
|
|
26
26
|
"pydantic>=2.6",
|
|
27
27
|
]
|
|
28
28
|
|
|
29
29
|
[project.optional-dependencies]
|
|
30
30
|
dev = [
|
|
31
31
|
"pytest>=8.0",
|
|
32
|
+
"pytest-cov>=5.0",
|
|
32
33
|
"ruff>=0.4",
|
|
34
|
+
# rosbags (Apache-2.0, pure-Python) is a *dev* dependency so the
|
|
35
|
+
# bag-analysis I/O tests actually run in CI instead of self-skipping on
|
|
36
|
+
# `requires_rosbags`. It is NOT a runtime dependency — end users opt in
|
|
37
|
+
# via `topicforge[bags]`.
|
|
38
|
+
"rosbags>=0.9",
|
|
33
39
|
]
|
|
34
40
|
# v0.3.0: split DDS extras per vendor. `[dds]` (the union) pulls both
|
|
35
41
|
# OSS Python participants ; granular `[dds-cyclone]` / `[dds-fast]`
|
|
@@ -107,6 +113,33 @@ markers = [
|
|
|
107
113
|
"requires_rosbags: tests needing the `rosbags` Python library; auto-skip without it (v0.4.0 Phase 3+)",
|
|
108
114
|
]
|
|
109
115
|
|
|
116
|
+
[tool.coverage.run]
|
|
117
|
+
branch = true
|
|
118
|
+
source = ["topicforge"]
|
|
119
|
+
omit = [
|
|
120
|
+
# The two real DDS adapters import their vendor binding (cyclonedds /
|
|
121
|
+
# fastdds) at module top level, so they are structurally unreachable
|
|
122
|
+
# without those SDKs installed. Their pure logic was extracted into
|
|
123
|
+
# adapters/common/{dds_introspection,qos_normalize,cdr_decoder}.py
|
|
124
|
+
# (Lot 0) which IS measured here; the thin binding shells are exercised
|
|
125
|
+
# only under the `-m integration` real-bus tier. Excluded so the
|
|
126
|
+
# coverage gate reflects testable code rather than being dragged down
|
|
127
|
+
# by lines no unit run can reach.
|
|
128
|
+
"*/adapters/dds_cyclone/adapter.py",
|
|
129
|
+
"*/adapters/dds_fast/adapter.py",
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
[tool.coverage.report]
|
|
133
|
+
show_missing = true
|
|
134
|
+
skip_covered = false
|
|
135
|
+
# Threshold reflects the unit-testable surface (the two binding shells are
|
|
136
|
+
# omitted above). Baseline at the Lot 0 achieved floor (87% total); the
|
|
137
|
+
# extracted pure modules (qos_normalize, dds_introspection) are ~100%, while
|
|
138
|
+
# factory.py / bag_service.py binding-present branches stay uncovered without
|
|
139
|
+
# the SDKs installed. Ratchet upward as those get tests; do not lower without
|
|
140
|
+
# a note in the changelog.
|
|
141
|
+
fail_under = 85
|
|
142
|
+
|
|
110
143
|
[tool.ruff]
|
|
111
144
|
line-length = 100
|
|
112
145
|
target-version = "py311"
|
|
@@ -13,6 +13,18 @@ from topicforge.adapters.common.dds_helpers import (
|
|
|
13
13
|
VendorTag,
|
|
14
14
|
canonicalize_vendor_id,
|
|
15
15
|
format_guid,
|
|
16
|
+
validate_domain_id,
|
|
17
|
+
)
|
|
18
|
+
from topicforge.adapters.common.dds_introspection import (
|
|
19
|
+
cyclone_extract_guid,
|
|
20
|
+
cyclone_extract_hostname,
|
|
21
|
+
cyclone_extract_topic_name,
|
|
22
|
+
cyclone_extract_vendor_id,
|
|
23
|
+
fast_extract_guid,
|
|
24
|
+
fast_extract_hostname,
|
|
25
|
+
fast_extract_topic_name,
|
|
26
|
+
fast_extract_vendor_id,
|
|
27
|
+
is_removal,
|
|
16
28
|
)
|
|
17
29
|
from topicforge.adapters.common.lifecycle import MAX_EVENTS, LifecycleBuffer
|
|
18
30
|
from topicforge.adapters.common.metrics_buffer import (
|
|
@@ -21,6 +33,11 @@ from topicforge.adapters.common.metrics_buffer import (
|
|
|
21
33
|
MetricsSample,
|
|
22
34
|
)
|
|
23
35
|
from topicforge.adapters.common.qos_analyzer import detect_mismatches
|
|
36
|
+
from topicforge.adapters.common.qos_endpoints import detect_mismatches_across_endpoints
|
|
37
|
+
from topicforge.adapters.common.qos_normalize import (
|
|
38
|
+
cyclone_qos_to_profile,
|
|
39
|
+
fast_qos_to_profile,
|
|
40
|
+
)
|
|
24
41
|
from topicforge.adapters.common.xtypes import (
|
|
25
42
|
DecodeStatus,
|
|
26
43
|
annotate_full,
|
|
@@ -41,12 +58,25 @@ __all__ = [
|
|
|
41
58
|
"annotate_partial",
|
|
42
59
|
"annotate_raw",
|
|
43
60
|
"canonicalize_vendor_id",
|
|
61
|
+
"cyclone_extract_guid",
|
|
62
|
+
"cyclone_extract_hostname",
|
|
63
|
+
"cyclone_extract_topic_name",
|
|
64
|
+
"cyclone_extract_vendor_id",
|
|
65
|
+
"cyclone_qos_to_profile",
|
|
44
66
|
"decode_dynamic_sample",
|
|
45
67
|
"decode_field_value",
|
|
46
68
|
"detect_mismatches",
|
|
69
|
+
"detect_mismatches_across_endpoints",
|
|
47
70
|
"dynamic_type_name",
|
|
48
71
|
"extract_publish_ns_from_payload",
|
|
49
72
|
"extract_seq_from_payload",
|
|
73
|
+
"fast_extract_guid",
|
|
74
|
+
"fast_extract_hostname",
|
|
75
|
+
"fast_extract_topic_name",
|
|
76
|
+
"fast_extract_vendor_id",
|
|
77
|
+
"fast_qos_to_profile",
|
|
50
78
|
"format_guid",
|
|
79
|
+
"is_removal",
|
|
51
80
|
"iter_field_names",
|
|
81
|
+
"validate_domain_id",
|
|
52
82
|
]
|