Files
lda-wf/docs/historical/superpowers/plans/2026-06-08-wf-sources-mcp-adapter-helper.md
T

16 KiB

MCP Adapter Helper Move 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 the upstream MCP adapter lookup helper from wf_mcp.broker.service.adapters into canonical wf_sources_mcp.adapters.

Architecture: wf_sources_mcp.adapters.require_adapter will accept a structural source reference so both legacy ConnectionConfig(server=...) and typed McpSourceConnection(provider=...) can resolve adapters. The type seam should model "either server or provider", not "both". The old wf_mcp.broker.service.adapters module remains a compatibility shim. UpstreamTransportService imports the canonical helper.

Tech Stack: Python 3.14, structural Protocol, Mapping, wf_sources_mcp.sdk.BackendAdapter, pytest, ruff, basedpyright.


Hard Boundaries

  • Do not move UpstreamTransportService in this slice.
  • Do not change adapter registry shape: it remains dict[str, BackendAdapter].
  • Do not change error type: missing adapter should still raise KeyError.
  • Do not import wf_mcp from src/wf_sources_mcp/adapters.py.
  • Keep wf_mcp.broker.service.adapters.require_adapter import-compatible via a shim.
  • Do not commit unless the caller explicitly asks for a commit.

File Map

  • Create src/wf_sources_mcp/adapters.py: canonical adapter-ref protocols and require_adapter.
  • Modify src/wf_sources_mcp/__init__.py: export adapter-ref protocols and require_adapter.
  • Replace src/wf_mcp/broker/service/adapters.py: compatibility shim.
  • Modify src/wf_mcp/broker/service/upstream_transport.py: import require_adapter from wf_sources_mcp.adapters.
  • Create tests/wf_sources_mcp/test_adapters.py: canonical helper tests.
  • Modify tests/wf_mcp/test_compat_imports.py: shim identity test.
  • Modify tests/wf_sources_mcp/test_import_direction_guard.py: forbid old broker service adapter imports.
  • Modify docs: docs/current_roadmap.md and docs/superpowers/specs/2026-06-03-long-lived-workflow-api-boundary.md.
  • Move this plan to docs/historical/superpowers/plans/ after implementation is verified.

Task 1: Add Canonical Adapter Helper Tests

Files:

  • Create: tests/wf_sources_mcp/test_adapters.py

  • Step 1: Write tests for legacy and typed source refs

Create tests/wf_sources_mcp/test_adapters.py:

from __future__ import annotations

from dataclasses import dataclass
from typing import Any

import pytest

from wf_sources_mcp.adapters import require_adapter
from wf_sources_mcp.auth import AuthRecord
from wf_sources_mcp.catalog import DiscoveredPrompt, DiscoveredResource, DiscoveredTool
from wf_sources_mcp.connections import McpSourceConnection
from wf_sources_mcp.sdk import BackendAdapter, ToolCallResult
from wf_sources_mcp.transports import StdioSourceTransport


class _Adapter:
    async def list_tools(
        self,
        connection: McpSourceConnection,
        auth: AuthRecord | None,
    ) -> list[DiscoveredTool]:
        return []

    async def list_resources(
        self,
        connection: McpSourceConnection,
        auth: AuthRecord | None,
    ) -> list[DiscoveredResource]:
        return []

    async def list_prompts(
        self,
        connection: McpSourceConnection,
        auth: AuthRecord | None,
    ) -> list[DiscoveredPrompt]:
        return []

    async def get_connection_metadata(
        self,
        connection: McpSourceConnection,
        auth: AuthRecord | None,
    ) -> dict[str, Any]:
        return {}

    async def read_resource(
        self,
        connection: McpSourceConnection,
        auth: AuthRecord | None,
        uri: str,
    ) -> dict[str, Any]:
        raise NotImplementedError

    async def get_prompt(
        self,
        connection: McpSourceConnection,
        auth: AuthRecord | None,
        prompt_name: str,
        arguments: dict[str, str] | None = None,
    ) -> dict[str, Any]:
        raise NotImplementedError

    async def invoke_method(
        self,
        connection: McpSourceConnection,
        auth: AuthRecord | None,
        method: str,
        params: dict[str, Any] | None = None,
    ) -> dict[str, Any]:
        raise NotImplementedError

    async def send_notification(
        self,
        connection: McpSourceConnection,
        auth: AuthRecord | None,
        method: str,
        params: dict[str, Any] | None = None,
    ) -> None:
        raise NotImplementedError

    async def call_tool(
        self,
        connection: McpSourceConnection,
        auth: AuthRecord | None,
        tool_name: str,
        payload: dict[str, Any],
    ) -> ToolCallResult:
        raise NotImplementedError


@dataclass(slots=True)
class _LegacyConnection:
    server: str


def test_require_adapter_uses_legacy_server_field() -> None:
    adapter = _Adapter()

    result = require_adapter(
        _LegacyConnection(server="demo"),
        {"demo": adapter},
    )

    assert result is adapter


def test_require_adapter_uses_typed_source_provider_field() -> None:
    adapter = _Adapter()
    connection = McpSourceConnection(
        id="demo.default",
        provider="demo",
        account="default",
        transport=StdioSourceTransport(command="demo-mcp"),
    )

    result = require_adapter(connection, {"demo": adapter})

    assert result is adapter


def test_require_adapter_raises_useful_key_error() -> None:
    with pytest.raises(KeyError, match="no adapter registered for source 'missing'"):
        require_adapter(_LegacyConnection(server="missing"), {})


def test_require_adapter_has_backend_adapter_static_shape() -> None:
    adapter: BackendAdapter = _Adapter()

    assert adapter is not None
  • Step 2: Run tests and verify failure before implementation

Run:

uv run pytest tests/wf_sources_mcp/test_adapters.py -q

Expected: fail with ModuleNotFoundError or import error for wf_sources_mcp.adapters.


Task 2: Create Canonical wf_sources_mcp.adapters

Files:

  • Create: src/wf_sources_mcp/adapters.py

  • Step 1: Add structural helper implementation

Create src/wf_sources_mcp/adapters.py:

from __future__ import annotations

from collections.abc import Mapping
from typing import Protocol

from wf_sources_mcp.sdk import BackendAdapter


class SourceAdapterRef(Protocol):
    """Typed source identity used by `McpSourceConnection`."""

    provider: str


class LegacyAdapterRef(Protocol):
    """Legacy broker source identity used by `ConnectionConfig`."""

    server: str


type AdapterLookupRef = SourceAdapterRef | LegacyAdapterRef


def _adapter_key(source: object) -> str:
    server = getattr(source, "server", None)
    if isinstance(server, str):
        return server
    provider = getattr(source, "provider", None)
    if isinstance(provider, str):
        return provider
    raise TypeError("source must expose a string 'server' or 'provider' attribute")


def require_adapter(
    source: AdapterLookupRef,
    adapters: Mapping[str, BackendAdapter],
) -> BackendAdapter:
    """Return the adapter for a source or raise a useful lookup error."""
    key = _adapter_key(source)
    adapter = adapters.get(key)
    if adapter is None:
        raise KeyError(f"no adapter registered for source {key!r}")
    return adapter


__all__ = ["AdapterLookupRef", "LegacyAdapterRef", "SourceAdapterRef", "require_adapter"]

Note: _adapter_key still uses runtime attribute checks because this helper is a compatibility boundary. The public type alias must express "legacy server ref OR typed provider ref"; do not require both attributes.

  • Step 2: Run canonical tests

Run:

uv run pytest tests/wf_sources_mcp/test_adapters.py -q

Expected: pass.


Task 3: Export Adapter Helper From wf_sources_mcp

Files:

  • Modify: src/wf_sources_mcp/__init__.py

  • Modify: tests/wf_sources_mcp/test_adapters.py

  • Step 1: Add package-root exports

Update src/wf_sources_mcp/__init__.py so this works:

from wf_sources_mcp import AdapterLookupRef, LegacyAdapterRef, SourceAdapterRef, require_adapter

If the package uses lazy __getattr__, add these names to __all__ and route them to .adapters:

    if name in {"AdapterLookupRef", "LegacyAdapterRef", "SourceAdapterRef", "require_adapter"}:
        from . import adapters

        return getattr(adapters, name)
  • Step 2: Add package-root export test

Append to tests/wf_sources_mcp/test_adapters.py:

def test_adapter_helper_exports_from_package_root() -> None:
    from wf_sources_mcp import require_adapter as root_require_adapter
    from wf_sources_mcp.adapters import require_adapter

    assert root_require_adapter is require_adapter
  • Step 3: Run canonical tests

Run:

uv run pytest tests/wf_sources_mcp/test_adapters.py -q

Expected: pass.


Task 4: Replace Broker Service Adapter Module With Shim

Files:

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

  • Modify: tests/wf_mcp/test_compat_imports.py

  • Step 1: Replace old implementation with shim

Replace src/wf_mcp/broker/service/adapters.py with:

"""Compatibility shim for upstream MCP adapter lookup.

Canonical implementation lives in `wf_sources_mcp.adapters`.
"""

from __future__ import annotations

from wf_sources_mcp.adapters import (
    AdapterLookupRef,
    LegacyAdapterRef,
    SourceAdapterRef,
    require_adapter,
)

__all__ = ["AdapterLookupRef", "LegacyAdapterRef", "SourceAdapterRef", "require_adapter"]
  • Step 2: Add shim identity test

Append to tests/wf_mcp/test_compat_imports.py:

def test_wf_mcp_broker_service_adapter_shim_reexports_wf_sources_mcp_adapter_helper() -> None:
    from wf_mcp.broker.service.adapters import require_adapter as compat_require_adapter
    from wf_sources_mcp.adapters import require_adapter

    assert compat_require_adapter is require_adapter
  • Step 3: Run compatibility test

Run:

uv run pytest tests/wf_mcp/test_compat_imports.py::test_wf_mcp_broker_service_adapter_shim_reexports_wf_sources_mcp_adapter_helper -q

Expected: pass.


Task 5: Update Upstream Transport Import

Files:

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

  • Step 1: Replace import

Change:

from .adapters import require_adapter

to:

from wf_sources_mcp.adapters import require_adapter
  • Step 2: Run upstream transport tests

Run:

uv run pytest tests/wf_mcp/service/test_upstream_transport.py tests/wf_mcp/service/test_catalog.py tests/wf_mcp/service/test_events.py::test_service_records_tool_call_events -q

Expected: pass.


Task 6: Add Import Guard for Old Adapter Helper

Files:

  • Modify: tests/wf_sources_mcp/test_import_direction_guard.py

  • Step 1: Add forbidden old adapter import test

Append to tests/wf_sources_mcp/test_import_direction_guard.py:

def test_wf_sources_mcp_does_not_import_old_broker_service_adapter_module() -> None:
    root = Path(__file__).resolve().parents[2] / "src" / "wf_sources_mcp"
    forbidden = {"wf_mcp.broker.service.adapters"}
    violations: list[str] = []

    for py_file in sorted(root.rglob("*.py")):
        rel = py_file.relative_to(root.parent)
        module = str(rel.with_suffix("")).replace("/", ".").replace("\\", ".")
        tree = ast.parse(py_file.read_text(encoding="utf-8"), filename=str(py_file))
        for node in ast.walk(tree):
            if isinstance(node, ast.ImportFrom) and node.module in forbidden:
                violations.append(f"{module}:{node.lineno}: from {node.module} import ...")
            elif isinstance(node, ast.Import):
                for alias in node.names:
                    if alias.name in forbidden:
                        violations.append(f"{module}:{node.lineno}: import {alias.name}")

    assert violations == [], (
        "wf_sources_mcp still imports old wf_mcp broker service adapter module:\n"
        + "\n".join(f"  {violation}" for violation in violations)
    )
  • Step 2: Run import guard

Run:

uv run pytest tests/wf_sources_mcp/test_import_direction_guard.py -q

Expected: pass.


Task 7: Update Docs and Archive Plan

Files:

  • Modify: docs/current_roadmap.md

  • Modify: docs/superpowers/specs/2026-06-03-long-lived-workflow-api-boundary.md

  • Move: docs/superpowers/plans/2026-06-08-wf-sources-mcp-adapter-helper.md to docs/historical/superpowers/plans/2026-06-08-wf-sources-mcp-adapter-helper.md

  • Step 1: Update docs/current_roadmap.md

Under the wf_sources_mcp cleanup section, add:

    - Completed: upstream MCP adapter lookup (`require_adapter`) now lives in
      `wf_sources_mcp.adapters`, with `wf_mcp.broker.service.adapters` retained
      as a compatibility shim.
  • Step 2: Update docs/superpowers/specs/2026-06-03-long-lived-workflow-api-boundary.md

Add a completed numbered item before the pending upstream transport/discovery/session services item:

21. Complete: upstream MCP adapter lookup (`require_adapter`) moved to
    `wf_sources_mcp.adapters`, with `wf_mcp.broker.service.adapters` retained
    as a compatibility shim.

If numbering differs because new items landed meanwhile, keep the completed item before the broad pending item and renumber.

  • Step 3: Archive the plan

Run:

git mv docs/superpowers/plans/2026-06-08-wf-sources-mcp-adapter-helper.md docs/historical/superpowers/plans/2026-06-08-wf-sources-mcp-adapter-helper.md

Expected: git status --short shows an R rename for the plan.


Task 8: Final Verification

Files:

  • No code edits unless verification finds a real issue.

  • Step 1: Run focused tests

Run:

uv run pytest tests/wf_sources_mcp/test_adapters.py tests/wf_sources_mcp/test_import_direction_guard.py tests/wf_mcp/test_compat_imports.py tests/wf_mcp/service/test_upstream_transport.py tests/wf_mcp/service/test_catalog.py tests/wf_mcp/service/test_events.py::test_service_records_tool_call_events -q

Expected: all selected tests pass.

  • Step 2: Run source-provider tests

Run:

uv run pytest tests/wf_sources_mcp -q

Expected: all wf_sources_mcp tests pass.

  • Step 3: Run lint

Run:

uv run ruff check src/wf_sources_mcp/adapters.py src/wf_mcp/broker/service/adapters.py src/wf_mcp/broker/service/upstream_transport.py tests/wf_sources_mcp/test_adapters.py tests/wf_sources_mcp/test_import_direction_guard.py tests/wf_mcp/test_compat_imports.py

Expected: All checks passed!

  • Step 4: Run typecheck

Run:

uv run basedpyright --level error src/wf_sources_mcp/adapters.py src/wf_mcp/broker/service/adapters.py src/wf_mcp/broker/service/upstream_transport.py tests/wf_sources_mcp/test_adapters.py tests/wf_mcp/test_compat_imports.py

Expected: 0 errors, 0 warnings, 0 notes

  • Step 5: Check old adapter helper import usage

Run:

rg -n "require_adapter|wf_mcp\\.broker\\.service\\.adapters|from \\.adapters import" src tests

Expected:

  • src/wf_sources_mcp/adapters.py owns canonical require_adapter.

  • src/wf_mcp/broker/service/adapters.py remains as a shim.

  • src/wf_mcp/broker/service/upstream_transport.py imports from wf_sources_mcp.adapters.

  • tests/wf_mcp/test_compat_imports.py may reference the old shim path.

  • src/wf_sources_mcp must not import wf_mcp.broker.service.adapters.

  • Step 6: Check whitespace

Run:

git diff --check

Expected: no whitespace errors. CRLF warnings on Windows are acceptable.


Expected Final Report

The implementer should report:

  • Files created, modified, and moved.
  • Exact verification commands and pass/fail output.
  • Confirmation that canonical require_adapter lives in wf_sources_mcp.adapters.
  • Confirmation that wf_mcp.broker.service.adapters is a compatibility shim.
  • Confirmation that UpstreamTransportService imports canonical require_adapter.
  • Any deviations from this plan.

Do not claim "full suite passed" unless the full suite was actually run.