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.
- Owns
- Modify
src/wf_mcp/broker/service/core.py- Removes direct
ConnectionRegistryfield fromWfMcpService. - Constructs
ConnectionService, passes its lookup/list callbacks intoSourceCatalogService, then binds the source catalog back toConnectionService. - Keeps compatibility property/method delegates.
- Removes direct
- Create
tests/wf_mcp/service/test_connection_service.py- Direct tests for
ConnectionService. - Service facade smoke test for
.connectionscompatibility.
- Direct tests for
- 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
WfMcpServicedecomposition.
- Add one ownership note if the file still tracks
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
.connectionscompatibility 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_idandRESERVED_CONNECTION_IDSappear inconnection_service.py, notcore.py. -
connections: ConnectionRegistryappears inconnection_service.py, notcore.py. -
connection_serviceappears incore.pyand 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.mdmentions completedConnectionServiceownership.- 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, andsync_connections_from_configintoConnectionService; preserves.connectionscompatibility; keeps source hydration coordination explicit through a post-construction bind. - Placeholder scan: No
TBD, generic "add tests", or unfilled implementation steps remain. - Type consistency:
ConnectionServiceexposesget,list_all, andlist_enabledsoSourceCatalogServicecan use bound methods without depending onWfMcpService. - 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.