# 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: ```python 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: ```bash 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: ```python 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: ```bash 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: ```bash 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: ```python from ...connections import ConnectionRegistry, parse_connection_id from ...shared.names import RESERVED_CONNECTION_IDS ``` Replace with: ```python from ...connections import ConnectionRegistry ``` Add: ```python from .connection_service import ConnectionService ``` In `WfMcpService`, remove the dataclass field: ```python connections: ConnectionRegistry = field(default_factory=ConnectionRegistry) ``` Add this init-false field near the other service fields: ```python 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: ```python 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: ```python @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: ```python def register_connection(self, connection: ConnectionConfig) -> None: self.connection_service.register_connection(connection) ``` Replace `sync_connections_from_config` with: ```python def sync_connections_from_config(self, config: BrokerConfig) -> None: self.connection_service.sync_connections_from_config(config) ``` - [ ] **Step 5: Run focused service tests** Run: ```bash 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: ```python from wf_mcp.broker import WfMcpService ``` Append these tests: ```python 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: ```bash 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: ```bash 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: ```bash 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: ```bash 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: ```markdown - 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: ```markdown - 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: ```bash 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: ```markdown - 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: ```bash 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: ```bash 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: ```bash uv run pytest -q ``` Expected: full suite passes with the existing skipped/xfailed counts only. - [ ] **Step 3: Run final static checks** Run: ```bash 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.