Problem
\get_mesh_state\ and \list_all_nodes\ should remain separate external MCP tools, but they currently rebuild overlapping node facts in two different places:
- \pipeline/topology.py::build_topology()\ builds graph-oriented node rows plus links and partition summaries.
- \pipeline/nodes.py::list_nodes_enriched()\ builds inventory-oriented node rows for tables and operator inspection.
The overlap is not just conceptual. Both paths independently derive or project many of the same node-level facts:
- identity: \eui64, \riendly_name\
- placement: \�rea_name, \partition_id\
- mesh role:
ole,
outing_role\
- parent relationship: \parent_eui64\
- freshness/lifecycle: \last_seen, \status, \last_referenced_at\
- partition leadership context: leader discovery by
outing_role == leader\
- phantom filtering behavior
At the same time, each tool adds different higher-level projections:
- \get_mesh_state: graph links, partition summaries, split flag, weak/error/asymmetric link tagging, graph diagnostics
- \list_all_nodes: \display_name, \device_kind, HA metadata, availability fields, router peer summaries,
ext_hop_to_otbr, SED-specific mesh-alive fields, optional \signal_strength\
This means the public tools are correctly distinct, but the internal node snapshot logic is not centralized.
Why This Should Not Collapse Into One Public Tool
Do not merge \get_mesh_state\ and \list_all_nodes\ into one MCP tool.
They serve different consumers and payload shapes:
- \get_mesh_state\ is graph-first and should stay optimized for topology reasoning / rendering.
- \list_all_nodes\ is inventory-first and should stay optimized for table views, filtering, and operator inspection.
The refactor goal is shared internal data assembly, not API consolidation.
Current Code Anchors
- \�ddons/thread-observability/app/src/thread_observability/pipeline/topology.py\
- \�ddons/thread-observability/app/src/thread_observability/pipeline/nodes.py\
Relevant current behavior:
- \�uild_topology()\ directly queries node fields, derives \parent_eui64, computes per-node \stale, and then builds partition summaries and links.
- \list_nodes_enriched()\ starts from \store.list_nodes(), derives parent/leader/peer/OTBR-hop context, and projects a richer inventory row.
Proposed Refactor
Introduce one shared internal node snapshot builder that produces a canonical base row per visible node. Then project that base row into:
- a graph-facing node shape for \get_mesh_state\
- an inventory-facing row shape for \list_all_nodes\
Proposed Internal Shape
Create an internal helper or dataclass, e.g. \CanonicalNodeSnapshot, containing only the common base facts needed by both projections.
Suggested fields:
- \eui64\
- \riendly_name\
- \�rea_name\
ole\
outing_role\
- \partition_id\
- \leader_router_id\
outer_id\
- \�endor_id\
- \product_id\
- \serial_number\
- \manufacturer\
- \model\
- \device_id\
- \irst_seen\
- \last_seen\
- \status\
- \status_changed_at\
- \last_referenced_at\
- \�vailable\
- \�vailability_source\
- \�vailability_checked_at\
- \parent_eui64\
- \last_rssi\
- \last_lqi\
- \is_phantom\
- \suppressed_duplicate_euis\
Notes:
- Keep this internal shape free of graph-only artifacts like \links, \split, or edge tags.
- Keep it free of inventory-only projections like \display_name, \device_kind,
outer_peers, or
ext_hop_to_otbr\ unless those are computed in a second projection step.
- \stale\ in topology should likely remain a topology projection derived from \last_seen\ + \reshness_minutes, not a stored base field.
Implementation Outline
- Extract a shared helper in \pipeline/\ for collecting canonical node rows from storage.
- Move duplicate identity / partition / lifecycle / parent assembly into that helper.
- Make \�uild_topology()\ consume the shared canonical rows and then add:
- topology-specific \stale\
- partition summaries
- links
- graph annotations / diagnostics
- Make \list_nodes_enriched()\ consume the same canonical rows and then add:
- \display_name\
- \device_kind\
- partition leader name
- router peer counts / peer names
- \parent_name\
ext_hop_to_otbr\
- SED-specific fields
- optional \signal_strength\
- Preserve existing response schemas for both tools.
Non-Goals
- No MCP tool rename.
- No contract merge between \get_mesh_state\ and \list_all_nodes.
- No dashboard behavior changes required as part of this refactor.
- No broad topology/diagnostics redesign.
Acceptance Criteria
- There is one shared internal node snapshot assembly path used by both \�uild_topology()\ and \list_nodes_enriched().
- \get_mesh_state\ response shape remains unchanged.
- \list_all_nodes\ response shape remains unchanged.
- Existing phantom filtering behavior remains intact.
- Parent resolution remains consistent across both tools.
- Partition leader derivation is not duplicated in both modules.
- Existing tests covering topology and node inventory continue to pass.
- Add focused tests for the shared helper covering:
- phantom filtering
- duplicate hardware collapse passthrough
- parent derivation
- partition/leader consistency across both projections
Problem
\get_mesh_state\ and \list_all_nodes\ should remain separate external MCP tools, but they currently rebuild overlapping node facts in two different places:
The overlap is not just conceptual. Both paths independently derive or project many of the same node-level facts:
ole,
outing_role\
outing_role == leader\
At the same time, each tool adds different higher-level projections:
ext_hop_to_otbr, SED-specific mesh-alive fields, optional \signal_strength\
This means the public tools are correctly distinct, but the internal node snapshot logic is not centralized.
Why This Should Not Collapse Into One Public Tool
Do not merge \get_mesh_state\ and \list_all_nodes\ into one MCP tool.
They serve different consumers and payload shapes:
The refactor goal is shared internal data assembly, not API consolidation.
Current Code Anchors
Relevant current behavior:
Proposed Refactor
Introduce one shared internal node snapshot builder that produces a canonical base row per visible node. Then project that base row into:
Proposed Internal Shape
Create an internal helper or dataclass, e.g. \CanonicalNodeSnapshot, containing only the common base facts needed by both projections.
Suggested fields:
ole\
outing_role\
outer_id\
Notes:
outer_peers, or
ext_hop_to_otbr\ unless those are computed in a second projection step.
Implementation Outline
ext_hop_to_otbr\
Non-Goals
Acceptance Criteria