Files
lda-wf/docs/historical/superpowers/plans/2026-06-03-source-registry-startup-merge.md
T

11 KiB

Source Registry Startup Merge Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Load persisted dynamic MCP source registry entries during broker/server construction and merge them with config-defined connections without changing behavior when the registry file is absent.

Architecture: Keep parsing/loading config separate from runtime hydration. wf_mcp.source_registry owns MCP registry entries and conversion to ConnectionConfig; ConnectionService owns connection reconciliation and source-catalog hydration; build_service_from_config() wires the default file store from BrokerConfig.store_root.

Tech Stack: Python 3.14, dataclasses, Pydantic v2, wf_mcp.source_registry, ConnectionService, WfMcpService, build_service_from_config, pytest, ruff, basedpyright.


Preconditions

Completed before this plan:

  • Generic registry mechanics exist in wf_api.source_registry.
  • MCP-specific registry models remain in wf_mcp.source_registry.
  • registry_entry_to_connection_config() exists and is tested.
  • No startup/runtime merge uses the registry yet.

Merge Rules

  1. Config-defined connections win over registry entries with the same id.
  2. Registry entries fill ids not present in config.
  3. Reserved ids remain rejected by existing ConnectionService validation.
  4. Shadowed registry entries must be visible through an event/diagnostic, not silently ignored.
  5. Missing source_registry.json must preserve current config-only behavior.

Do not mutate config files. Do not delete auth/catalog files.


Task 1: Add Merge Helper Tests First

  • Add focused tests in tests/wf_mcp/service/test_connection_service.py.
  • Import:
from wf_mcp.source_registry import (
    FileSourceRegistryStore,
    McpSourceRegistryEntry,
    SourceRegistryFile,
    StdioSourceTransport,
)
  • Add helper:
def _registry_entry(
    source_id: str = "demo.registry",
    *,
    enabled: bool = True,
) -> McpSourceRegistryEntry:
    return McpSourceRegistryEntry(
        id=source_id,
        kind="mcp",
        enabled=enabled,
        provider="demo",
        account=source_id.rsplit(".", 1)[-1],
        transport=StdioSourceTransport(command="demo-server"),
    )
  • Add test_connection_service_sync_merges_registry_entries.

Expected behavior:

store = FileSourceRegistryStore(tmp_path)
store.save_registry(SourceRegistryFile(sources=[_registry_entry()]))

service.sync_connections_from_config(
    BrokerConfig(store_root=tmp_path, connections=[]),
    source_registry_store=store,
)

assert [connection.id for connection in service.list_all()] == ["demo.registry"]
assert "demo.registry" in catalog.capability_sources
  • Add test_connection_service_sync_config_shadows_registry_entry.

Expected behavior:

store.save_registry(SourceRegistryFile(sources=[_registry_entry("demo.same")]))
service.sync_connections_from_config(
    BrokerConfig(
        store_root=tmp_path,
        connections=[
            ConnectionConfig(id="demo.same", server="demo", account="config"),
        ],
    ),
    source_registry_store=store,
)

assert service.get("demo.same").account == "config"
assert service.events.list_events()[-1].kind == "source_registry_ignored_config_shadow"
assert service.events.list_events()[-1].connection_id == "demo.same"
  • Add test_connection_service_sync_registry_disabled_entry_hydrates_disabled_source.

Expected behavior:

store.save_registry(SourceRegistryFile(sources=[_registry_entry(enabled=False)]))
service.sync_connections_from_config(
    BrokerConfig(store_root=tmp_path, connections=[]),
    source_registry_store=store,
)

assert service.get("demo.registry").enabled is False
assert catalog.capability_sources["demo.registry"].enabled is False
  • Run:
uv run pytest tests/wf_mcp/service/test_connection_service.py -q

Expected: new tests fail until Task 2.


Task 2: Implement Registry-Aware Connection Reconciliation

  • Update src/wf_mcp/broker/service/connection_service.py.
  • Import:
from ...source_registry import (
    SourceRegistryStore,
    registry_entry_to_connection_config,
)
  • Change sync_connections_from_config signature:
def sync_connections_from_config(
    self,
    config: BrokerConfig,
    *,
    source_registry_store: SourceRegistryStore | None = None,
) -> None:
  • Build a merged connection list before existing remove/update/register logic:
connections = list(config.connections)
config_ids = {connection.id for connection in connections}
if source_registry_store is not None:
    registry = source_registry_store.load_registry()
    for entry in registry.sources:
        if entry.id in config_ids:
            self.events.record_kind(
                "source_registry_ignored_config_shadow",
                connection_id=entry.id,
                payload={
                    "server": entry.provider,
                    "account": entry.account,
                    "reason": "config_connection_takes_precedence",
                },
            )
            continue
        connections.append(registry_entry_to_connection_config(entry))
  • Reuse the existing reconciliation logic against the merged connections list.

  • Keep BrokerConfig immutable; do not mutate config.connections.

  • Keep existing event behavior for registered/updated/removed connections.

  • Add a short comment near the merge explaining config precedence.

  • Run:

uv run pytest tests/wf_mcp/service/test_connection_service.py -q

Expected: all connection service tests pass.


Task 3: Wire WfMcpService Facade

  • Update src/wf_mcp/broker/service/core.py.
  • Import SourceRegistryStore from wf_mcp.source_registry.
  • Change facade method signature:
def sync_connections_from_config(
    self,
    config: BrokerConfig,
    *,
    source_registry_store: SourceRegistryStore | None = None,
) -> None:
    self.connection_service.sync_connections_from_config(
        config,
        source_registry_store=source_registry_store,
    )
  • Add/adjust a facade test in tests/wf_mcp/service/test_connection_service.py proving WfMcpService.sync_connections_from_config(..., source_registry_store=store) delegates and hydrates a registry connection.

Task 4: Wire Build From Config

  • Update src/wf_mcp/broker/config.py.
  • Import FileSourceRegistryStore.
  • Construct the store in build_service_from_config(config):
source_registry_store = FileSourceRegistryStore(config.store_root)
  • Replace the manual connection loop with one registry-aware sync call:
service.sync_connections_from_config(
    config,
    source_registry_store=source_registry_store,
)
for connection in service.connections.list_all():
    if connection.server not in service.adapters:
        service.register_adapter(connection.server, McpSdkAdapter())

Important:

  • Preserve adapter registration for config-defined and registry-defined connections.
  • Do not load the registry in load_broker_config; it should only parse config files.
  • Do not save the registry during startup.

Task 5: Add Build-Service Integration Tests

  • Update tests/wf_mcp/test_broker_server.py or tests/wf_mcp/server/test_config.py.
  • Add test_build_service_from_config_loads_source_registry_entries.

Setup:

config = BrokerConfig(store_root=tmp_path, connections=[])
FileSourceRegistryStore(tmp_path).save_registry(
    SourceRegistryFile(sources=[_registry_entry("fixture.registry")])
)
service = build_service_from_config(config)

Assertions:

assert service.connections.get("fixture.registry").server == "fixture"
assert "fixture" in service.adapters
assert "fixture.registry" in service.capability_sources
  • Add test_build_service_from_config_config_shadows_registry.

Assertions:

assert service.connections.get("fixture.same").account == "config"
assert any(
    event.kind == "source_registry_ignored_config_shadow"
    and event.connection_id == "fixture.same"
    for event in service.list_events()
)
  • Add test_build_service_from_config_absent_registry_preserves_existing_behavior if no existing test already covers this. It can extend the current test_build_service_from_config_registers_connections.

Task 6: Update Docs

  • Update docs/superpowers/specs/2026-06-03-store-backed-source-registry-design.md.

Mark startup merge as complete or in-progress, depending on final status:

### Slice 3: Startup Merge

Status: complete. Broker/service construction now loads `source_registry.json`,
merges config-defined connections with dynamic registry entries, preserves config
precedence, and emits `source_registry_ignored_config_shadow` for shadowed
registry entries.
  • Update docs/current_roadmap.md.

Replace the source-registry note with:

- Source registry startup merge is implemented: absent registry preserves
  config-only behavior, registry-only entries hydrate as dynamic connections,
  and config entries shadow same-id registry entries with an event.
  • Update docs/superpowers/plans/2026-06-03-source-registry-next-slices.md.

Mark Slice 2A and Slice 2B complete if not already done, and mark Slice 3 complete after implementation.


Task 7: Verify

  • Run focused tests:
uv run pytest tests/wf_mcp/service/test_connection_service.py tests/wf_mcp/test_broker_server.py tests/wf_mcp/server/test_config.py -q
  • Run source registry tests:
uv run pytest tests/wf_api/test_source_registry.py tests/wf_mcp/test_source_registry.py -q
  • Run quality checks:
uv run ruff check src/wf_mcp/source_registry.py src/wf_mcp/broker/config.py src/wf_mcp/broker/service/connection_service.py src/wf_mcp/broker/service/core.py tests/wf_mcp/service/test_connection_service.py tests/wf_mcp/test_broker_server.py tests/wf_mcp/server/test_config.py
uv run basedpyright --level error src/wf_mcp/source_registry.py src/wf_mcp/broker/config.py src/wf_mcp/broker/service/connection_service.py src/wf_mcp/broker/service/core.py tests/wf_mcp/service/test_connection_service.py tests/wf_mcp/test_broker_server.py tests/wf_mcp/server/test_config.py
  • If focused tests pass, run the full suite if time permits:
uv run pytest -q

Acceptance Criteria

  • Missing registry file keeps existing config-only behavior.
  • Registry-only entries become registered connections and hydrated source catalog entries.
  • Config entries shadow same-id registry entries.
  • Shadowing emits source_registry_ignored_config_shadow.
  • Disabled registry entries hydrate disabled connection/source state.
  • load_broker_config() remains config-only parsing.
  • No registry mutation commands are added in this slice.
  • wf_api still imports no wf_mcp.