Repository navigation
tech-debt(mcp): four hand-maintained bridge lists can drift — adding manual_mcp touched three of four (#14586) #14631
Description
Summary
Registering manual_mcp as the twelfth MCP bridge (#14586) required editing
three separately hand-maintained lists of "every governed bridge", and a
fourth — a test asserting an exact set — caught the omission only because it
happened to use == rather than a subset/superset check. This is the same
canonical-source-duplication shape umbrella #13916 tracks (see also its
worked example #13512, and the more recently filed #14584).
The four lists
autobot_shared/auth/mcp_bridge_scan.py—bridge_files()globs
autobot-backend/api/*_mcp.py+*_mcp/tools.pydirectly off the
filesystem, filtered byEXCLUDED_BRIDGE_STEMS(a table, post-security(mcp): manual_mcp exposes three MCP tools with no permission check and is excluded from the bridge scan by name #14586).autobot-backend/api/mcp_registry.py—_BRIDGE_MODULE_REGISTRY
(module path, name,/mcp/toolsendpoint, feature-tag list per bridge),
consulted bydiscover_bridges()'s module-scan fallback and mirrored into
MCP_BRIDGESat import time.tools/lint/check_mcp_tool_permission_coverage.py—
PER_BRIDGE_DISCOVERY_FLOOR, a bridge-name-keyed dict of expected
per-bridge tool counts.autobot-backend/tests/unit/api/test_mcp_plugin_discovery.py—
test_discover_bridges_known_names'sexpectedset, a literal list of
bridge names compared with==againstdiscover_bridges()'s output.
Adding manual_mcp required edits to #1 (remove exclusion + make its tool
schemas parseable), #2 (add the registry tuple — without it discover_bridges()
never sees the bridge, regardless of #1), and #3 (add its floor, bump the
aggregate). #4 was not touched proactively; CI caught it because the test
happens to assert exact equality rather than "every bridge in the scan is a
subset of expected" — a shard failure, not a design decision, is what
surfaced it (see PR #14627 discussion on #14586).
Recommendation: mcp_bridge_scan.py's filesystem glob is the natural SSOT
It is the only one of the four that cannot silently disagree with
reality — it reads the actual files on disk rather than a name someone
remembered to type into a second place. The other three should assert
against it or derive from it, not duplicate its membership by hand:
- List Dev new gui #4 (test literal) is pure duplication and should be deleted.
test_discover_bridges_known_namesdoesn't need a hardcoded set at all —
it can assertnames == {bridge_name(p) for p in mcp_bridge_scan.bridge_files()}directly. This removes the fourth
copy entirely rather than requiring it to be kept in sync. - List Docs improvements #2 (
_BRIDGE_MODULE_REGISTRY) carries genuine extra metadata
(endpoint URL, feature tags) the glob cannot supply, so it cannot be
deleted outright — but every governed bridge that has aMANIFEST(all
twelve do, after security(mcp): manual_mcp exposes three MCP tools with no permission check and is excluded from the bridge scan by name #14586) already self-describes name, description and
endpoint.discover_bridges()'s module-scan fallback could iterate
mcp_bridge_scan.bridge_files()directly, import each module, and read
itsMANIFEST, rather than walking a separately hand-listed
(module_path, name, endpoint, features)tuple per bridge. That would
shrink_BRIDGE_MODULE_REGISTRYto a manually curatedfeatureslist
keyed by bridge name (metadata the glob genuinely cannot derive), with
membership and endpoint both sourced from the scan + each bridge's own
MANIFEST. - List Fix frontend errors and remove unused code #3 (
PER_BRIDGE_DISCOVERY_FLOOR) is a deliberate snapshot, not a
live count — floors exist specifically to lag reality so a regression is
detectable, so its values should stay hand-set. But its key set should
be asserted against the glob: today, a new bridge with no floor entry is
invisible tobridge_discovery_gaps()(which only iterates
PER_BRIDGE_DISCOVERY_FLOOR.items()), and would likely still pass the
aggregateDISCOVERY_FLOORcheck since new tools only ever raise the sum.
That is exactly the kind of silent gapmanual_mcpsat in for as long as
it did. A guard assertingset(PER_BRIDGE_DISCOVERY_FLOOR) == {bridge_name(p) for p in bridge_files()}would have failed loudly the
momentmanual_mcpwas un-excluded from the scan, before its floor was
added by hand.
Suggested scope (separate PR, per #14586's review)
- Replace
test_mcp_plugin_discovery.py's literalexpectedset with a
comparison againstmcp_bridge_scan.bridge_files(). - Add a guard (pytest + the required-check script) asserting
set(PER_BRIDGE_DISCOVERY_FLOOR) == {bridge_name(p) for p in bridge_files()}— every governed bridge has a floor, no orphaned floor
entries either. - Investigate deriving
_BRIDGE_MODULE_REGISTRY's module discovery from
mcp_bridge_scan.bridge_files()+ each bridge'sMANIFEST, keeping
only the feature-tag metadata hand-maintained. - Land each change with its own guard test, per Umbrella: canonical-source duplication — concepts with no single definition, where the copies have already drifted #13916's stated
acceptance criterion ("every consolidation lands with a guard test...
without it the count regrows").
References
- security(mcp): manual_mcp exposes three MCP tools with no permission check and is excluded from the bridge scan by name #14586 / PR security(mcp): govern manual_mcp as the twelfth bridge, fix double mount (#14586) #14627 — added
manual_mcpas the twelfth bridge; this issue
is the follow-up "governance completeness" gap that PR's review surfaced. - Umbrella: canonical-source duplication — concepts with no single definition, where the copies have already drifted #13916 — canonical-source-duplication umbrella; this is the MCP-bridge-list
instance of the same pattern. - tech-debt(slm-tests): no guard test keeps health_collector_state_change_test.py's ansible mirror in sync #14584 — the most recently filed sibling instance (ansible test-file
mirror drift), same shape, filed the same way (own issue, cross-linked,
not folded into a PR already carrying an unrelated change).
Activity
refactor(mcp): derive the bridge lists from disk instead of hand-main…
chore(ratchets): re-pin audio-extension-allowlist, red at zero headro…
fix(guards,ratchets): route the new guard through the one enumeration…
refactor(mcp): derive the bridge lists from disk instead of hand-main…
chore(ratchets): re-pin audio-extension-allowlist, red at zero headro…
fix(guards,ratchets): route the new guard through the one enumeration…
chore(ratchets): record that this branch adopted main's floors, not i…
7 remaining items
fix(docs): correct a floor comment that narrated a re-pin this PR doe…
fix(docs): delete the correction block too -- two voices on one floor…
refactor(mcp): derive the bridge lists from disk instead of hand-main…
fix(guards,ratchets): route the new guard through the one enumeration…
chore(ratchets): record that this branch adopted main's floors, not i…
fix(guards): migrate the enum-census floor to declare(), re-measured …
chore(ratchets): re-pin two floors mid-window, including one I pinned…
chore(ratchets): re-pin import-hermeticity, and remove every measurem…
refactor(mcp): derive the bridge lists from disk instead of hand-main…
fix(guards,ratchets): route the new guard through the one enumeration…
chore(ratchets): record that this branch adopted main's floors, not i…
fix(guards): migrate the enum-census floor to declare(), re-measured …
chore(ratchets): re-pin two floors mid-window, including one I pinned…
chore(ratchets): re-pin import-hermeticity, and remove every measurem…