Files
lda-wf/docs/historical/superpowers/plans/2026-06-02-wfmcpservice-connection-service-extraction.md
T

19 KiB

WfMcpService ConnectionService Extraction 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: Move connection registration and config reconciliation out of WfMcpService into a focused ConnectionService.

Architecture: ConnectionService owns the broker-local ConnectionRegistry and emits connection lifecycle events. SourceCatalogService still owns capability sources, so ConnectionService binds to it after both services are constructed. WfMcpService remains a compatibility facade with .connections, register_connection(), and sync_connections_from_config() delegating to the new service.

Tech Stack: Python 3.14, dataclasses, pytest, ruff, basedpyright, existing wf_mcp broker service modules.


File Structure

  • Create src/wf_mcp/broker/service/connection_service.py
    • Owns ConnectionRegistry.
    • Validates connection IDs and reserved IDs.
    • Registers connections and hydrates source catalog snapshots.
    • Reconciles config reload changes.
  • Modify src/wf_mcp/broker/service/core.py
    • Removes direct ConnectionRegistry field from WfMcpService.
    • Constructs ConnectionService, passes its lookup/list callbacks into SourceCatalogService, then binds the source catalog back to ConnectionService.
    • Keeps compatibility property/method delegates.
  • Create tests/wf_mcp/service/test_connection_service.py
    • Direct tests for ConnectionService.
    • Service facade smoke test for .connections compatibility.
  • Modify docs/current_roadmap.md
    • Mark the connection-service extraction as the current/complete slice after implementation.
  • Optionally modify docs/superpowers/research/2026-06-01-wf-api-extraction-map.md
    • Add one ownership note if the file still tracks WfMcpService decomposition.

Task 1: Add Direct ConnectionService Tests

Files:

  • Create: tests/wf_mcp/service/test_connection_service.py

  • Step 1: Create direct tests for the new service boundary

Create tests/wf_mcp/service/test_connection_service.py with:

from __future__ import annotations

from wf_mcp.broker.service.connection_service import ConnectionService
from wf_mcp.broker.service.events import BrokerEventRecorder
from wf_mcp.broker.service.source_catalog import SourceCatalogService
from wf_mcp.events import EventBus
from wf_mcp.models import BrokerConfig, ConnectionConfig
from wf_mcp.runtime import ToolExecutor
from wf_mcp.storage import FileStore

from ..test_support import local_temp_root


def _source_catalog(service: ConnectionService) -> SourceCatalogService:
    store = FileStore(local_temp_root() / "connection_service_catalog")

    def _tool_executor_for(_connection: ConnectionConfig) -> ToolExecutor:
        raise AssertionError("tool executor should not be needed in these tests")

    catalog = SourceCatalogService(
        store=store,
        connection_lookup=service.get,
        connection_list_enabled=service.list_enabled,
        connection_list_all=service.list_all,
        tool_executor_for=_tool_executor_for,
        load_auth=lambda _connection_id: None,
        emit_event=service.events.record_event,
    )
    service.bind_source_catalog(catalog)
    return catalog


def test_connection_service_rejects_reserved_connection_ids() -> None:
    service = ConnectionService(events=BrokerEventRecorder(EventBus()))
    _source_catalog(service)

    for connection_id in ("wf.admin", "wf.mcp"):
        try:
            service.register_connection(
                ConnectionConfig(id=connection_id, server="wf", account="reserved")
            )
        except ValueError as exc:
            assert connection_id in str(exc)
            assert "reserved by wf-mcp" in str(exc)
        else:
            raise AssertionError(f"expected {connection_id!r} to be rejected")


def test_connection_service_registers_connection_and_empty_source() -> None:
    service = ConnectionService(events=BrokerEventRecorder(EventBus()))
    catalog = _source_catalog(service)

    service.register_connection(
        ConnectionConfig(id="demo.personal", server="demo", account="personal")
    )

    assert service.get("demo.personal").server == "demo"
    assert [connection.id for connection in service.list_enabled()] == ["demo.personal"]
    source = catalog.capability_sources["demo.personal"]
    assert source.enabled is True
    assert source.description == "No catalog loaded for demo.personal."
    assert service.events.list_events()[0].kind == "connection_registered"
    assert service.events.list_events()[0].connection_id == "demo.personal"


def test_connection_service_sync_removes_retired_connections_and_sources() -> None:
    service = ConnectionService(events=BrokerEventRecorder(EventBus()))
    catalog = _source_catalog(service)
    service.register_connection(
        ConnectionConfig(id="demo.personal", server="demo", account="personal")
    )

    service.sync_connections_from_config(
        BrokerConfig(store_root=local_temp_root(), connections=[])
    )

    assert service.list_all() == []
    assert "demo.personal" not in catalog.capability_sources


def test_connection_service_sync_updates_existing_source_enabled_flag() -> None:
    service = ConnectionService(events=BrokerEventRecorder(EventBus()))
    catalog = _source_catalog(service)
    service.register_connection(
        ConnectionConfig(id="demo.personal", server="demo", account="personal")
    )

    service.sync_connections_from_config(
        BrokerConfig(
            store_root=local_temp_root(),
            connections=[
                ConnectionConfig(
                    id="demo.personal",
                    server="demo",
                    account="personal",
                    enabled=False,
                )
            ],
        )
    )

    assert service.get("demo.personal").enabled is False
    assert catalog.capability_sources["demo.personal"].enabled is False
  • Step 2: Run the direct test and confirm it fails before implementation

Run:

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

Expected: import failure for wf_mcp.broker.service.connection_service.


Task 2: Implement ConnectionService

Files:

  • Create: src/wf_mcp/broker/service/connection_service.py

  • Step 1: Add the service implementation

Create src/wf_mcp/broker/service/connection_service.py with:

from __future__ import annotations

from dataclasses import dataclass, field

from ...connections import ConnectionRegistry, parse_connection_id
from ...models import BrokerConfig, ConnectionConfig
from ...shared.names import RESERVED_CONNECTION_IDS
from .events import BrokerEventRecorder
from .source_catalog import SourceCatalogService


@dataclass(slots=True)
class ConnectionService:
    """Own broker connection registration and config reconciliation.

    SourceCatalogService needs connection lookup callbacks during construction,
    while registering a connection needs source-catalog hydration. The catalog is
    therefore bound after both services exist; `_source_catalog()` makes that
    construction cycle explicit and fail-fast.
    """

    events: BrokerEventRecorder
    connections: ConnectionRegistry = field(default_factory=ConnectionRegistry)
    source_catalog: SourceCatalogService | None = None

    def bind_source_catalog(self, source_catalog: SourceCatalogService) -> None:
        self.source_catalog = source_catalog

    def get(self, connection_id: str) -> ConnectionConfig:
        return self.connections.get(connection_id)

    def list_all(self) -> list[ConnectionConfig]:
        return self.connections.list_all()

    def list_enabled(self) -> list[ConnectionConfig]:
        return self.connections.list_enabled()

    def register_connection(self, connection: ConnectionConfig) -> None:
        self._validate_connection_id(connection.id)
        self.connections.register(connection)
        self._source_catalog().hydrate_connection_source_from_snapshot(connection)
        self.events.record_kind(
            "connection_registered",
            connection_id=connection.id,
            payload={"server": connection.server, "account": connection.account},
        )

    def sync_connections_from_config(self, config: BrokerConfig) -> None:
        """Reconcile registry/source state after the public server reloads config."""
        source_catalog = self._source_catalog()
        next_ids = {connection.id for connection in config.connections}
        previous_ids = set(self.connections.connections)
        for connection_id in previous_ids - next_ids:
            del self.connections.connections[connection_id]
            source_catalog.capability_sources.pop(connection_id, None)

        for connection in config.connections:
            self._validate_connection_id(connection.id)
            self.connections.register(connection)
            source = source_catalog.capability_sources.get(connection.id)
            if source is None:
                source_catalog.hydrate_connection_source_from_snapshot(connection)
            else:
                source.enabled = connection.enabled

    def _source_catalog(self) -> SourceCatalogService:
        if self.source_catalog is None:
            raise RuntimeError("ConnectionService requires a bound SourceCatalogService")
        return self.source_catalog

    @staticmethod
    def _validate_connection_id(connection_id: str) -> None:
        parse_connection_id(connection_id)
        if connection_id in RESERVED_CONNECTION_IDS:
            raise ValueError(f"connection id {connection_id!r} is reserved by wf-mcp")
  • Step 2: Run the direct tests

Run:

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

Expected: all tests pass.

  • Step 3: Run ruff on the new files

Run:

uv run ruff check src/wf_mcp/broker/service/connection_service.py tests/wf_mcp/service/test_connection_service.py

Expected: all checks pass.


Task 3: Wire WfMcpService Through ConnectionService

Files:

  • Modify: src/wf_mcp/broker/service/core.py

  • Step 1: Update imports and dataclass fields

In src/wf_mcp/broker/service/core.py:

Remove:

from ...connections import ConnectionRegistry, parse_connection_id
from ...shared.names import RESERVED_CONNECTION_IDS

Replace with:

from ...connections import ConnectionRegistry

Add:

from .connection_service import ConnectionService

In WfMcpService, remove the dataclass field:

connections: ConnectionRegistry = field(default_factory=ConnectionRegistry)

Add this init-false field near the other service fields:

connection_service: ConnectionService = field(init=False)
  • Step 2: Construct and bind the connection service

In __post_init__, replace the source-catalog construction block with this shape:

self.events = BrokerEventRecorder(self.event_bus)
self.connection_service = ConnectionService(events=self.events)
self.upstream = UpstreamTransportService(
    store=self.store,
    event_sink=self.events.record_event,
    tool_executor=self.tool_executor,
)
self.source_catalog = SourceCatalogService(
    store=self.store,
    connection_lookup=self.connection_service.get,
    connection_list_enabled=self.connection_service.list_enabled,
    connection_list_all=self.connection_service.list_all,
    tool_executor_for=self.upstream.tool_executor_for,
    load_auth=self.upstream.load_auth,
    emit_event=self.events.record_event,
    default_catalog_max_age_seconds=self.default_catalog_max_age_seconds,
)
self.connection_service.bind_source_catalog(self.source_catalog)
  • Step 3: Preserve .connections compatibility as a property

Add this property below capability_sources or above it:

@property
def connections(self) -> ConnectionRegistry:
    """Compatibility view of the broker connection registry.

    Connection lifecycle ownership has moved to ConnectionService. Keep this
    property because admin handlers, CLI helpers, and tests still inspect the
    registry through the service facade.
    """
    return self.connection_service.connections
  • Step 4: Replace connection lifecycle method bodies with delegates

Replace register_connection with:

def register_connection(self, connection: ConnectionConfig) -> None:
    self.connection_service.register_connection(connection)

Replace sync_connections_from_config with:

def sync_connections_from_config(self, config: BrokerConfig) -> None:
    self.connection_service.sync_connections_from_config(config)
  • Step 5: Run focused service tests

Run:

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

Expected: all selected tests pass.


Task 4: Add Facade Compatibility Tests

Files:

  • Modify: tests/wf_mcp/service/test_connection_service.py

  • Step 1: Add WfMcpService compatibility coverage

Append these imports:

from wf_mcp.broker import WfMcpService

Append these tests:

def test_wfmcpservice_exposes_connection_registry_from_connection_service() -> None:
    service = WfMcpService(store=FileStore(local_temp_root() / "connection_facade"))

    service.register_connection(
        ConnectionConfig(id="demo.personal", server="demo", account="personal")
    )

    assert service.connections is service.connection_service.connections
    assert service.connections.get("demo.personal").account == "personal"
    assert "demo.personal" in service.capability_sources


def test_wfmcpservice_sync_connections_delegates_to_connection_service() -> None:
    service = WfMcpService(store=FileStore(local_temp_root() / "connection_sync"))
    service.register_connection(
        ConnectionConfig(id="demo.personal", server="demo", account="personal")
    )

    service.sync_connections_from_config(
        BrokerConfig(
            store_root=local_temp_root(),
            connections=[
                ConnectionConfig(
                    id="demo.work",
                    server="demo",
                    account="work",
                    enabled=True,
                )
            ],
        )
    )

    assert [connection.id for connection in service.connections.list_all()] == [
        "demo.work"
    ]
    assert "demo.personal" not in service.capability_sources
    assert "demo.work" in service.capability_sources
  • Step 2: Run the compatibility tests

Run:

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

Expected: all tests pass.


Task 5: Clean Imports and Verify Call Sites

Files:

  • Modify: src/wf_mcp/broker/service/core.py

  • Possibly modify files only if ruff reports stale imports.

  • Step 1: Search for stale direct ownership assumptions

Run:

rg -n 'parse_connection_id|RESERVED_CONNECTION_IDS|connection_service|ConnectionRegistry|connections: ConnectionRegistry' src/wf_mcp/broker/service tests/wf_mcp/service

Expected:

  • parse_connection_id and RESERVED_CONNECTION_IDS appear in connection_service.py, not core.py.

  • connections: ConnectionRegistry appears in connection_service.py, not core.py.

  • connection_service appears in core.py and direct tests.

  • Step 2: Run ruff on modified service files

Run:

uv run ruff check src/wf_mcp/broker/service/core.py src/wf_mcp/broker/service/connection_service.py tests/wf_mcp/service/test_connection_service.py

Expected: all checks pass. If ruff reports unused imports in core.py, remove only those imports.

  • Step 3: Run basedpyright on modified source

Run:

uv run basedpyright --level error

Expected: 0 errors.


Task 6: Update Roadmap and Extraction Map

Files:

  • Modify: docs/current_roadmap.md

  • Modify if present/relevant: docs/superpowers/research/2026-06-01-wf-api-extraction-map.md

  • Step 1: Update docs/current_roadmap.md

Find the bullet that says:

- Next planned service extraction: move connection registration/config reconciliation
  into a `ConnectionService`. That slice should own reserved connection-id
  rejection, `register_connection`, `sync_connections_from_config`, and source
  hydration coordination while leaving `WfMcpService` as a compatibility
  coordinator.

Replace it with:

- Connection ownership now lives in `ConnectionService`: it owns the broker
  `ConnectionRegistry`, reserved connection-id rejection, `register_connection`,
  and `sync_connections_from_config`. `WfMcpService.connections` remains a
  compatibility property while source hydration still belongs to
  `SourceCatalogService`.
  • Step 2: Update the extraction map if it contains the WfMcpService split notes

Run:

rg -n 'ConnectionService|connection registration|sync_connections_from_config|WfMcpService' docs/superpowers/research/2026-06-01-wf-api-extraction-map.md

If the file exists and contains the service split section, add this short note near the other extracted-service bullets:

- Connection registration/config reload reconciliation is now owned by
  `wf_mcp.broker.service.connection_service.ConnectionService`. The service owns
  the `ConnectionRegistry`; `WfMcpService.connections` is only a compatibility
  property.
  • Step 3: Run docs grep to verify roadmap wording

Run:

rg -n 'ConnectionService|Connection ownership|Next planned service extraction' docs/current_roadmap.md docs/superpowers/research/2026-06-01-wf-api-extraction-map.md

Expected:

  • docs/current_roadmap.md mentions completed ConnectionService ownership.
  • No stale "Next planned service extraction" wording for this same slice remains.

Task 7: Final Verification

Files:

  • No new files.

  • Step 1: Run focused tests

Run:

uv run pytest tests/wf_mcp/service/test_connection_service.py tests/wf_mcp/service/test_catalog.py tests/wf_mcp/service/test_sources.py tests/wf_mcp/service/test_events.py tests/wf_mcp/test_broker_server.py tests/wf_mcp/test_admin_surface.py -q

Expected: all selected tests pass.

  • Step 2: Run full test suite

Run:

uv run pytest -q

Expected: full suite passes with the existing skipped/xfailed counts only.

  • Step 3: Run final static checks

Run:

uv run ruff check
uv run ruff format --check
uv run basedpyright --level error

Expected:

  • ruff check passes.
  • ruff format check passes for Python files.
  • basedpyright reports 0 errors.

If markdown format checks complain about preview-only markdown behavior, do not rewrite unrelated markdown. Report it as a formatting-tool limitation and keep the code checks green.


Self-Review

  • Spec coverage: The plan moves reserved ID validation, register_connection, and sync_connections_from_config into ConnectionService; preserves .connections compatibility; keeps source hydration coordination explicit through a post-construction bind.
  • Placeholder scan: No TBD, generic "add tests", or unfilled implementation steps remain.
  • Type consistency: ConnectionService exposes get, list_all, and list_enabled so SourceCatalogService can use bound methods without depending on WfMcpService.
  • Risk: The construction cycle between connection lookup and source hydration is intentionally represented by bind_source_catalog(). The fail-fast _source_catalog() guard prevents silent use before binding.