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

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 as tool_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 use UpstreamTransportService.

Keep now:

  • register_connection.
  • sync_connections_from_config.
  • Connection registry field on WfMcpService.
  • Source catalog field on WfMcpService.
  • Event bus implementation on WfMcpService.
  • Public WfMcpService methods as delegates.
  • Admin handler public behavior.
  • MCP tool schema and CLI behavior.

Do not do in this slice:

  • Do not move ConnectionRegistry out of WfMcpService.
  • 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 ConnectionConfig values supplied by callers, SourceCatalogService for catalog refresh/live checks, Store, optional ToolExecutor, 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 needs tool_executor_for and load_auth.
    • Keep public service methods as delegates.
  • Modify src/wf_mcp/broker/service/source_catalog.py

    • No behavior change; it will receive callbacks from service.upstream instead of private WfMcpService methods.
  • Modify src/wf_mcp/broker/service/workflow_live_checks.py

    • Either move diagnostics into UpstreamTransportService or make the function accept UpstreamTransportService instead of WfMcpService.
    • Preferred for this slice: move live diagnostics as UpstreamTransportService.deployment_diagnostics(...), and leave workflow_live_checks.py as a compatibility shim if imports still reference it.
  • Modify src/wf_mcp/broker/service/workflow_operation_context.py

    • WfMcpWorkflowLiveSourceChecker calls service.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.md if 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.md if 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 despite 0 errors, record the exact output.

Non-Goals and Follow-Up Slices

This plan intentionally leaves these for later:

  1. Connection registry extraction: register_connection, reserved-id policy, and config reconciliation still live on WfMcpService.
  2. Event recorder extraction: event bus and catalog-change event fanout still live on WfMcpService.
  3. Admin surface extraction: admin handlers still call WfMcpService delegate methods.
  4. Final coordinator rename: only consider renaming WfMcpService once 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: UpstreamTransportService owns store, event_sink, adapters, and optional tool_executor; WfMcpService delegates through service.upstream.