34 KiB
WfMcpService Upstream Transport 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: Extract upstream MCP transport/auth/adapter operations from WfMcpService into a focused implementation service while preserving existing admin, MCP, CLI, and workflow behavior.
Architecture: Add UpstreamTransportService under wf_mcp.broker.service. It owns adapter registration, auth save/load, tool executor selection, catalog refresh I/O, broker resource reads, prompt rendering, raw method calls, raw notifications, and live-source probes. WfMcpService remains the coordinator and compatibility façade; connection registration and config reconciliation stay on WfMcpService because they mutate source registration and reserved-id policy.
Tech Stack: Python 3.14, dataclasses, BackendAdapter, ConnectionRegistry, MCP SDK adapters, SourceCatalogService, pytest, ruff, basedpyright.
Scope
Move now:
register_adapter._tool_executor_for, public on the new service astool_executor_for.save_auth.load_auth.read_resource.render_prompt.invoke_method.send_notification.refresh_connection_catalog.- Live source diagnostics from
workflow_live_checks.py, adapted to useUpstreamTransportService.
Keep now:
register_connection.sync_connections_from_config.- Connection registry field on
WfMcpService. - Source catalog field on
WfMcpService. - Event bus implementation on
WfMcpService. - Public
WfMcpServicemethods as delegates. - Admin handler public behavior.
- MCP tool schema and CLI behavior.
Do not do in this slice:
- Do not move
ConnectionRegistryout ofWfMcpService. - Do not rename
WfMcpService. - Do not move event recorder implementation.
- Do not change adapter implementations.
- Do not change live-check failure semantics.
Target File Structure
-
Create
src/wf_mcp/broker/service/upstream_transport.py- Owns adapter/auth/upstream I/O operations.
- Depends on
ConnectionConfigvalues supplied by callers,SourceCatalogServicefor catalog refresh/live checks,Store, optionalToolExecutor, and event emitter callbacks. - Contains docstrings for why generated workflow NodeSpecs use
tool_executor_for.
-
Modify
src/wf_mcp/broker/service/core.py- Add
upstream: UpstreamTransportService = field(init=False). - Construct it before
SourceCatalogService, because source hydration needstool_executor_forandload_auth. - Keep public service methods as delegates.
- Add
-
Modify
src/wf_mcp/broker/service/source_catalog.py- No behavior change; it will receive callbacks from
service.upstreaminstead of privateWfMcpServicemethods.
- No behavior change; it will receive callbacks from
-
Modify
src/wf_mcp/broker/service/workflow_live_checks.py- Either move diagnostics into
UpstreamTransportServiceor make the function acceptUpstreamTransportServiceinstead ofWfMcpService. - Preferred for this slice: move live diagnostics as
UpstreamTransportService.deployment_diagnostics(...), and leaveworkflow_live_checks.pyas a compatibility shim if imports still reference it.
- Either move diagnostics into
-
Modify
src/wf_mcp/broker/service/workflow_operation_context.pyWfMcpWorkflowLiveSourceCheckercallsservice.upstream.deployment_diagnostics(...).
-
Add tests in
tests/wf_mcp/service/test_upstream_transport.py. -
Update docs:
docs/current_roadmap.md.docs/superpowers/research/2026-06-01-wf-api-extraction-map.mdif stale.
Task 1: Add UpstreamTransportService Skeleton
Files:
-
Create:
src/wf_mcp/broker/service/upstream_transport.py -
Create:
tests/wf_mcp/service/test_upstream_transport.py -
Modify:
src/wf_mcp/broker/service/core.py -
Step 1: Write adapter registration and auth tests
Create tests/wf_mcp/service/test_upstream_transport.py:
from __future__ import annotations
from wf_mcp.broker.service.upstream_transport import UpstreamTransportService
from wf_mcp.events import McpEvent
from wf_mcp.models import AuthRecord
from wf_mcp.storage import FileStore
from ..test_support import FakeAdapter, local_temp_root
def test_upstream_transport_registers_adapter() -> None:
events: list[McpEvent] = []
transport = UpstreamTransportService(
store=FileStore(local_temp_root() / "upstream_adapter"),
event_sink=events.append,
)
adapter = FakeAdapter()
transport.register_adapter("demo", adapter)
assert transport.adapters["demo"] is adapter
def test_upstream_transport_saves_and_loads_auth_with_event() -> None:
events: list[McpEvent] = []
transport = UpstreamTransportService(
store=FileStore(local_temp_root() / "upstream_auth"),
event_sink=events.append,
)
record = AuthRecord(connection_id="demo.personal", scheme="bearer")
transport.save_auth(record)
loaded = transport.load_auth("demo.personal")
assert loaded is not None
assert loaded.connection_id == "demo.personal"
assert events[-1].kind == "auth_saved"
assert events[-1].connection_id == "demo.personal"
- Step 2: Run the tests and verify they fail
Run:
uv run pytest tests/wf_mcp/service/test_upstream_transport.py::test_upstream_transport_registers_adapter tests/wf_mcp/service/test_upstream_transport.py::test_upstream_transport_saves_and_loads_auth_with_event -q
Expected: import failure because upstream_transport.py does not exist.
- Step 3: Create the skeleton service
Create src/wf_mcp/broker/service/upstream_transport.py:
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass, field
from wf_mcp.events import McpEvent, make_event
from wf_mcp.models import AuthRecord
from wf_mcp.runtime import ToolExecutor
from wf_mcp.sdk import BackendAdapter
from wf_mcp.storage import Store
EventSink = Callable[[McpEvent], None]
@dataclass(slots=True)
class UpstreamTransportService:
"""Own upstream MCP adapter/auth operations for the broker service.
This is not protocol-neutral. It is the MCP transport implementation used by
admin calls, discovery, generated workflow NodeSpecs, and live source checks.
"""
store: Store
event_sink: EventSink
adapters: dict[str, BackendAdapter] = field(default_factory=dict)
tool_executor: ToolExecutor | None = None
def register_adapter(self, server: str, adapter: BackendAdapter) -> None:
self.adapters[server] = adapter
def save_auth(self, record: AuthRecord) -> None:
self.store.save_auth(record)
self.event_sink(
make_event(
"auth_saved",
connection_id=record.connection_id,
payload={"scheme": record.scheme},
)
)
def load_auth(self, connection_id: str) -> AuthRecord | None:
return self.store.load_auth(connection_id)
- Step 4: Run the tests
Run:
uv run pytest tests/wf_mcp/service/test_upstream_transport.py::test_upstream_transport_registers_adapter tests/wf_mcp/service/test_upstream_transport.py::test_upstream_transport_saves_and_loads_auth_with_event -q
Expected: pass.
- Step 5: Run ruff
Run:
uv run ruff check src/wf_mcp/broker/service/upstream_transport.py tests/wf_mcp/service/test_upstream_transport.py
Expected: pass.
Task 2: Wire UpstreamTransportService Into WfMcpService
Files:
-
Modify:
src/wf_mcp/broker/service/core.py -
Test:
tests/wf_mcp/service/test_upstream_transport.py -
Step 1: Add service delegate tests
Append to tests/wf_mcp/service/test_upstream_transport.py:
from wf_mcp.broker import WfMcpService
def test_wfmcpservice_uses_upstream_transport_for_adapters_and_auth() -> None:
service = WfMcpService(store=FileStore(local_temp_root() / "service_upstream"))
adapter = FakeAdapter()
service.register_adapter("demo", adapter)
service.save_auth(AuthRecord(connection_id="demo.personal", scheme="bearer"))
assert service.upstream.adapters["demo"] is adapter
assert service.adapters is service.upstream.adapters
assert service.load_auth("demo.personal") is not None
assert service.list_events()[-1].kind == "auth_saved"
- Step 2: Run the delegate test and verify it fails
Run:
uv run pytest tests/wf_mcp/service/test_upstream_transport.py::test_wfmcpservice_uses_upstream_transport_for_adapters_and_auth -q
Expected: fail because service.upstream does not exist.
- Step 3: Add upstream field and compatibility adapters property
In src/wf_mcp/broker/service/core.py, import:
from .upstream_transport import UpstreamTransportService
Replace the dataclass field:
adapters: dict[str, BackendAdapter] = field(default_factory=dict)
with:
upstream: UpstreamTransportService = field(init=False)
Keep tool_executor: ToolExecutor | None = None on WfMcpService for construction compatibility.
Add property:
@property
def adapters(self) -> dict[str, BackendAdapter]:
"""Compatibility view of upstream adapter registry."""
return self.upstream.adapters
In __post_init__, before constructing SourceCatalogService, add:
self.upstream = UpstreamTransportService(
store=self.store,
event_sink=self._record_event,
tool_executor=self.tool_executor,
)
Then update SourceCatalogService(...) construction:
tool_executor_for=self.upstream.tool_executor_for,
load_auth=self.upstream.load_auth,
- Step 4: Delegate adapter/auth methods
Replace these WfMcpService method bodies:
def register_adapter(self, server: str, adapter: BackendAdapter) -> None:
self.upstream.register_adapter(server, adapter)
def _tool_executor_for(self, connection: ConnectionConfig) -> ToolExecutor:
return self.upstream.tool_executor_for(connection)
def save_auth(self, record: AuthRecord) -> None:
self.upstream.save_auth(record)
def load_auth(self, connection_id: str) -> AuthRecord | None:
return self.upstream.load_auth(connection_id)
The private _tool_executor_for delegate stays temporarily because existing internal call sites may still reference it during this slice.
- Step 5: Add tool_executor_for implementation
In src/wf_mcp/broker/service/upstream_transport.py, add imports:
from wf_mcp.models import ConnectionConfig
from wf_mcp.broker.service.adapters import require_adapter
Add:
def tool_executor_for(self, connection: ConnectionConfig) -> ToolExecutor:
"""Return the executor used by generated workflow NodeSpecs.
Discovery uses short-lived adapters. Generated workflow nodes use this
hook so config-built services can swap in a persistent runtime pool for
stateful MCP servers.
"""
if self.tool_executor is not None:
return self.tool_executor
return require_adapter(connection, self.adapters)
- Step 6: Run focused tests
Run:
uv run pytest tests/wf_mcp/service/test_upstream_transport.py tests/wf_mcp/service/test_catalog.py::test_source_catalog_hydrates_connection_source_from_snapshot_directly -q
Expected: pass.
- Step 7: Run ruff
Run:
uv run ruff check src/wf_mcp/broker/service/core.py src/wf_mcp/broker/service/upstream_transport.py tests/wf_mcp/service/test_upstream_transport.py
Expected: pass.
Task 3: Move Resource, Prompt, Raw Method, and Notification Calls
Files:
-
Modify:
src/wf_mcp/broker/service/upstream_transport.py -
Modify:
src/wf_mcp/broker/service/core.py -
Test:
tests/wf_mcp/service/test_upstream_transport.py -
Test:
tests/wf_mcp/service/test_events.py -
Step 1: Add a direct raw method test
Append to tests/wf_mcp/service/test_upstream_transport.py:
from wf_mcp.connections import ConnectionRegistry
from wf_mcp.models import ConnectionConfig
def test_upstream_transport_invokes_raw_method_and_records_events() -> None:
events: list[McpEvent] = []
connections = ConnectionRegistry()
connections.register(
ConnectionConfig(id="demo.personal", server="demo", account="personal")
)
transport = UpstreamTransportService(
store=FileStore(local_temp_root() / "upstream_raw_method"),
event_sink=events.append,
)
transport.register_adapter("demo", FakeAdapter())
result = asyncio.run(
transport.invoke_method(
connections.get("demo.personal"),
"demo.echo",
params={"text": "hello"},
)
)
assert result["echoed"] == {"text": "hello"}
assert [event.kind for event in events] == [
"raw_method_started",
"raw_method_completed",
]
Add import asyncio at the top of the test file.
- Step 2: Run the direct raw method test and verify it fails
Run:
uv run pytest tests/wf_mcp/service/test_upstream_transport.py::test_upstream_transport_invokes_raw_method_and_records_events -q
Expected: fail because invoke_method does not exist on UpstreamTransportService.
- Step 3: Move upstream call methods
In src/wf_mcp/broker/service/upstream_transport.py, add imports:
from typing import Any
Add methods:
async def read_resource(
self,
connection: ConnectionConfig,
qualified_name: str,
uri: str,
) -> dict[str, Any]:
adapter = require_adapter(connection, self.adapters)
auth = self.load_auth(connection.id)
self.event_sink(
make_event(
"resource_read_started",
connection_id=connection.id,
capability_id=qualified_name,
payload={"uri": uri},
)
)
result = await adapter.read_resource(connection, auth, uri)
self.event_sink(
make_event(
"resource_read_completed",
connection_id=connection.id,
capability_id=qualified_name,
payload={"uri": uri},
)
)
return result
async def render_prompt(
self,
connection: ConnectionConfig,
qualified_name: str,
local_name: str,
arguments: dict[str, str] | None = None,
) -> dict[str, Any]:
adapter = require_adapter(connection, self.adapters)
auth = self.load_auth(connection.id)
self.event_sink(
make_event(
"prompt_get_started",
connection_id=connection.id,
capability_id=qualified_name,
payload={"argument_keys": sorted((arguments or {}).keys())},
)
)
result = await adapter.get_prompt(connection, auth, local_name, arguments)
self.event_sink(
make_event(
"prompt_get_completed",
connection_id=connection.id,
capability_id=qualified_name,
payload={"argument_keys": sorted((arguments or {}).keys())},
)
)
return result
async def invoke_method(
self,
connection: ConnectionConfig,
method: str,
*,
params: dict[str, Any] | None = None,
) -> dict[str, Any]:
adapter = require_adapter(connection, self.adapters)
auth = self.load_auth(connection.id)
self.event_sink(
make_event(
"raw_method_started",
connection_id=connection.id,
capability_id=method,
payload={"params": params or {}},
)
)
result = await adapter.invoke_method(connection, auth, method, params)
self.event_sink(
make_event(
"raw_method_completed",
connection_id=connection.id,
capability_id=method,
payload={"result_keys": sorted(result.keys())},
)
)
return result
async def send_notification(
self,
connection: ConnectionConfig,
method: str,
*,
params: dict[str, Any] | None = None,
) -> None:
adapter = require_adapter(connection, self.adapters)
auth = self.load_auth(connection.id)
self.event_sink(
make_event(
"raw_notification_started",
connection_id=connection.id,
capability_id=method,
payload={"params": params or {}},
)
)
await adapter.send_notification(connection, auth, method, params)
self.event_sink(
make_event(
"raw_notification_completed",
connection_id=connection.id,
capability_id=method,
payload={},
)
)
- Step 4: Delegate WfMcpService upstream calls
In src/wf_mcp/broker/service/core.py, keep local docs handling in WfMcpService.read_resource and render_prompt, but delegate remote calls:
resource = self.get_resource(qualified_name)
connection = self.connections.get(resource.connection_id)
return await self.upstream.read_resource(
connection,
qualified_name,
resource.uri,
)
For prompts:
prompt = self.get_prompt(qualified_name)
connection = self.connections.get(prompt.connection_id)
return await self.upstream.render_prompt(
connection,
qualified_name,
prompt.local_name,
arguments,
)
For raw method:
connection = self.connections.get(connection_id)
return await self.upstream.invoke_method(
connection,
method,
params=params,
)
For notification:
connection = self.connections.get(connection_id)
await self.upstream.send_notification(connection, method, params=params)
- Step 5: Run service event tests
Run:
uv run pytest tests/wf_mcp/service/test_upstream_transport.py::test_upstream_transport_invokes_raw_method_and_records_events tests/wf_mcp/service/test_events.py::test_service_records_resource_prompt_and_raw_method_events -q
If the exact event test name differs, run:
uv run pytest tests/wf_mcp/service/test_events.py -q
Expected: pass.
- Step 6: Run ruff
Run:
uv run ruff check src/wf_mcp/broker/service/core.py src/wf_mcp/broker/service/upstream_transport.py tests/wf_mcp/service/test_upstream_transport.py
Expected: pass.
Task 4: Move Catalog Refresh Upstream I/O
Files:
-
Modify:
src/wf_mcp/broker/service/upstream_transport.py -
Modify:
src/wf_mcp/broker/service/core.py -
Test:
tests/wf_mcp/service/test_upstream_transport.py -
Test:
tests/wf_mcp/service/test_catalog.py -
Test:
tests/wf_mcp/service/test_events.py -
Step 1: Add a direct refresh test
Append:
from wf_mcp.broker.service.source_catalog import SourceCatalogService
def test_upstream_transport_refreshes_catalog_directly() -> None:
events: list[McpEvent] = []
store = FileStore(local_temp_root() / "upstream_refresh")
connections = ConnectionRegistry()
connection = ConnectionConfig(id="demo.personal", server="demo", account="personal")
connections.register(connection)
transport = UpstreamTransportService(store=store, event_sink=events.append)
transport.register_adapter("demo", FakeAdapter())
source_catalog = SourceCatalogService(
store=store,
connection_lookup=connections.get,
connection_list_enabled=connections.list_enabled,
connection_list_all=connections.list_all,
tool_executor_for=transport.tool_executor_for,
load_auth=transport.load_auth,
emit_event=events.append,
)
source_catalog.hydrate_connection_source_from_snapshot(connection)
asyncio.run(
transport.refresh_connection_catalog(
connection,
source_catalog=source_catalog,
record_catalog_change_events=lambda source_id, snapshot, reason: None,
)
)
snapshot = store.load_catalog("demo.personal")
assert snapshot is not None
assert len(snapshot.nodes) >= 1
assert "catalog_refresh_started" in [event.kind for event in events]
assert "catalog_refresh_completed" in [event.kind for event in events]
- Step 2: Run the direct refresh test and verify it fails
Run:
uv run pytest tests/wf_mcp/service/test_upstream_transport.py::test_upstream_transport_refreshes_catalog_directly -q
Expected: fail because refresh_connection_catalog does not exist on UpstreamTransportService.
- Step 3: Move refresh I/O into UpstreamTransportService
In src/wf_mcp/broker/service/upstream_transport.py, add imports:
import time
from wf_mcp.broker.catalog import snapshot_from_specs
from wf_mcp.broker.discovery import discover_connection_capabilities, specs_from_discovered_tools
from wf_mcp.models import CatalogSnapshot
from wf_mcp.shared.errors import error_payload
from .source_catalog import SourceCatalogService
Add:
async def refresh_connection_catalog(
self,
connection: ConnectionConfig,
*,
source_catalog: SourceCatalogService,
max_age_seconds: int | None = None,
default_catalog_max_age_seconds: int = 300,
record_catalog_change_events: Callable[[str, CatalogSnapshot, str], None],
) -> None:
auth = self.load_auth(connection.id)
self.event_sink(
make_event(
"catalog_refresh_started",
connection_id=connection.id,
payload={"server": connection.server},
)
)
try:
adapter = require_adapter(connection, self.adapters)
capabilities = await discover_connection_capabilities(
connection=connection,
auth=auth,
adapter=adapter,
)
specs = specs_from_discovered_tools(
connection=connection,
auth=auth,
executor=self.tool_executor_for(connection),
tools=capabilities.tools,
emit_event=self.event_sink,
)
source_catalog.register_specs(
connection.id,
*specs,
max_age_seconds=max_age_seconds,
emit_change_events=False,
)
snapshot = snapshot_from_specs(
connection.id,
specs=source_catalog.capability_sources[
connection.id
].capabilities.node_specs,
tool_display_names={tool.name: tool.title for tool in capabilities.tools},
resources=capabilities.resources,
prompts=capabilities.prompts,
metadata=capabilities.metadata,
fetched_at_epoch_ms=int(time.time() * 1000),
max_age_seconds=max_age_seconds or default_catalog_max_age_seconds,
)
self.store.save_catalog(snapshot)
record_catalog_change_events(connection.id, snapshot, "catalog_refresh")
self.event_sink(
make_event(
"catalog_refresh_completed",
connection_id=connection.id,
payload={
"node_count": len(snapshot.nodes),
"resource_count": len(snapshot.resources),
"prompt_count": len(snapshot.prompts),
},
)
)
except Exception as exc:
self.event_sink(
make_event(
"catalog_refresh_failed",
connection_id=connection.id,
payload=error_payload(exc),
)
)
raise
- Step 4: Delegate WfMcpService refresh
Replace WfMcpService.refresh_connection_catalog body with:
connection = self.connections.get(connection_id)
await self.upstream.refresh_connection_catalog(
connection,
source_catalog=self.source_catalog,
max_age_seconds=max_age_seconds,
default_catalog_max_age_seconds=self.default_catalog_max_age_seconds,
record_catalog_change_events=lambda source_id, snapshot, reason: (
self._record_catalog_change_events(
source_id,
snapshot,
reason=reason,
)
),
)
- Step 5: Run refresh and catalog regression tests
Run:
uv run pytest tests/wf_mcp/service/test_upstream_transport.py::test_upstream_transport_refreshes_catalog_directly tests/wf_mcp/service/test_catalog.py::test_service_hydrates_planner_specs_from_stored_catalog tests/wf_mcp/service/test_events.py::test_service_records_catalog_refresh_failure_event -q
If the exact failure-event test name differs, run:
uv run pytest tests/wf_mcp/service/test_events.py -q
Expected: pass.
- Step 6: Run ruff
Run:
uv run ruff check src/wf_mcp/broker/service/core.py src/wf_mcp/broker/service/upstream_transport.py tests/wf_mcp/service/test_upstream_transport.py
Expected: pass.
Task 5: Move Live Source Diagnostics to UpstreamTransportService
Files:
-
Modify:
src/wf_mcp/broker/service/upstream_transport.py -
Modify:
src/wf_mcp/broker/service/workflow_live_checks.py -
Modify:
src/wf_mcp/broker/service/workflow_operation_context.py -
Test:
tests/wf_mcp/workflow_surface/test_deployments.py -
Step 1: Add a direct live-check missing connection test
Append to tests/wf_mcp/service/test_upstream_transport.py:
def test_upstream_transport_live_diagnostics_report_missing_connection() -> None:
transport = UpstreamTransportService(
store=FileStore(local_temp_root() / "upstream_live_missing"),
event_sink=lambda event: None,
)
source_catalog = SourceCatalogService(
store=transport.store,
connection_lookup=lambda connection_id: (_ for _in ()).throw(
KeyError(connection_id)
),
connection_list_enabled=lambda: [],
connection_list_all=lambda: [],
tool_executor_for=transport.tool_executor_for,
load_auth=transport.load_auth,
emit_event=lambda event: None,
)
source_catalog.register_capability_source(
CapabilitySource(
id="demo.personal",
kind="connection",
permissions=SourcePermissions(calls_upstream=True),
capabilities=CapabilityBuckets(),
)
)
artifact = echo_artifact()
deployment = WorkflowDeployment(
id="echo.personal",
artifact_id="echo",
artifact_version=1,
bindings=[{"logical_source": "demo", "concrete_source": "demo.personal"}],
)
diagnostics = asyncio.run(
transport.deployment_diagnostics(
deployment=deployment,
artifacts=[artifact],
source_catalog=source_catalog,
)
)
assert diagnostics[0].code == "source_unreachable"
assert diagnostics[0].bound_source == "demo.personal"
Add this import with the other test helpers:
from wf_artifacts import WorkflowDeployment
from wf_platform import SourcePermissions
from ..workflow_surface.conftest import echo_artifact
Keep this test focused on UpstreamTransportService; do not call
WorkflowSurfaceHandlers.validate_deployment here.
- Step 2: Run the live-check test and verify it fails
Run:
uv run pytest tests/wf_mcp/service/test_upstream_transport.py::test_upstream_transport_live_diagnostics_report_missing_connection -q
Expected: fail because deployment_diagnostics does not exist.
- Step 3: Move live diagnostics implementation
Move the implementation from src/wf_mcp/broker/service/workflow_live_checks.py into UpstreamTransportService as:
async def deployment_diagnostics(
self,
*,
deployment: WorkflowDeployment,
artifacts: Sequence[WorkflowArtifact],
source_catalog: SourceCatalogService,
) -> list[DependencyDiagnostic]:
...
Use:
source = source_catalog.capability_sources.get(source_id)
connection = source_catalog.connection_lookup(source_id)
adapter = require_adapter(connection, self.adapters)
auth = self.load_auth(source_id)
Keep the same timeout constant and same caught exception tuple.
- Step 4: Leave workflow_live_checks.py as a shim or delete if unused
Search:
rg -n "live_source_diagnostics|LIVE_SOURCE_CHECK_TIMEOUT_SECONDS|workflow_live_checks" src tests
If only workflow_operation_context.py uses it, update that file and delete workflow_live_checks.py.
If tests import it directly, keep workflow_live_checks.py as a compatibility shim:
"""Compatibility wrappers for live source diagnostics.
Canonical live-check implementation now lives on UpstreamTransportService.
"""
and delegate through service.upstream.deployment_diagnostics(...).
- Step 5: Update workflow operation context
In src/wf_mcp/broker/service/workflow_operation_context.py, remove the import:
from .workflow_live_checks import live_source_diagnostics
Change WfMcpWorkflowLiveSourceChecker.deployment_diagnostics to:
return await self.service.upstream.deployment_diagnostics(
deployment=deployment,
artifacts=artifacts,
source_catalog=self.service.source_catalog,
)
- Step 6: Run deployment live-check tests
Run:
uv run pytest tests/wf_mcp/service/test_upstream_transport.py::test_upstream_transport_live_diagnostics_report_missing_connection tests/wf_mcp/workflow_surface/test_deployments.py -q
Expected: pass.
- Step 7: Run ruff
Run:
uv run ruff check src/wf_mcp/broker/service/upstream_transport.py src/wf_mcp/broker/service/workflow_live_checks.py src/wf_mcp/broker/service/workflow_operation_context.py tests/wf_mcp/service/test_upstream_transport.py
If workflow_live_checks.py was deleted, remove it from the ruff command.
Expected: pass.
Task 6: Clean Imports, Docs, and Verify
Files:
-
Modify:
src/wf_mcp/broker/service/core.py -
Modify:
src/wf_mcp/broker/service/upstream_transport.py -
Modify:
docs/current_roadmap.md -
Modify:
docs/superpowers/research/2026-06-01-wf-api-extraction-map.mdif stale. -
Step 1: Remove stale imports from core.py
After the move, src/wf_mcp/broker/service/core.py should no longer import items used only by upstream transport, such as:
import time
from ...shared.errors import error_payload
from ..discovery import discover_connection_capabilities, specs_from_discovered_tools
from .adapters import require_adapter
from ..catalog import snapshot_from_specs
Keep imports still used by source catalog delegates, runtime delegates, public signatures, or event catalog-change methods.
- Step 2: Add roadmap note
In docs/current_roadmap.md, under the service extraction bullets, add:
- Upstream MCP transport is being separated from broker coordination.
`UpstreamTransportService` now owns adapter registration, auth persistence,
catalog refresh I/O, resource/prompt reads, raw method/notification calls,
generated-tool executor selection, and live source diagnostics.
- Step 3: Update extraction map if stale
If docs/superpowers/research/2026-06-01-wf-api-extraction-map.md says WfMcpService directly owns upstream transport/auth/adapter behavior, add:
Upstream MCP transport ownership is now split: `UpstreamTransportService` owns
adapter registry, auth persistence, catalog refresh I/O, resource/prompt reads,
raw method/notification calls, generated-tool executor selection, and live
source diagnostics. `WfMcpService` remains the coordinator and compatibility
façade.
- Step 4: Run focused verification
Run:
uv run pytest tests/wf_mcp/service/test_upstream_transport.py tests/wf_mcp/service/test_events.py tests/wf_mcp/service/test_catalog.py::test_service_hydrates_planner_specs_from_stored_catalog tests/wf_mcp/service/test_sources.py tests/wf_mcp/workflow_surface/test_deployments.py tests/wf_mcp/workflow_surface/test_runs.py -q
Expected: all selected tests pass.
- Step 5: Run full verification
Run:
uv run pytest -q
uv run ruff check src/wf_mcp/broker/service src/wf_api tests/wf_mcp/service tests/wf_api
uv run ruff format --check src/wf_mcp/broker/service src/wf_api tests/wf_mcp/service tests/wf_api docs/current_roadmap.md
uv run basedpyright --level error
Expected:
- pytest passes.
- ruff check passes.
- ruff format check passes.
- basedpyright reports
0 errors. If the known workspace enumeration warning causes a nonzero exit despite0 errors, record the exact output.
Non-Goals and Follow-Up Slices
This plan intentionally leaves these for later:
- Connection registry extraction:
register_connection, reserved-id policy, and config reconciliation still live onWfMcpService. - Event recorder extraction: event bus and catalog-change event fanout still live on
WfMcpService. - Admin surface extraction: admin handlers still call
WfMcpServicedelegate methods. - Final coordinator rename: only consider renaming
WfMcpServiceonce most implementation services are extracted.
Self-Review
- Spec coverage: The plan extracts upstream adapter/auth/I/O/live-check responsibilities while preserving current public service methods and admin behavior.
- Placeholder scan: No placeholder implementation tasks are left. Test snippets use existing artifact/deployment constructor shapes from the workflow surface tests.
- Type consistency:
UpstreamTransportServiceownsstore,event_sink,adapters, and optionaltool_executor;WfMcpServicedelegates throughservice.upstream.