18 KiB
wf_sources_mcp SDK Protocols Slice 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 MCP upstream adapter result/protocol types into wf_sources_mcp so runtime and discovery can later move without depending on wf_mcp.sdk shims.
Architecture: wf_sources_mcp owns source-provider contracts. wf_mcp.sdk remains the old compatibility import path and still owns McpSdkAdapter plus MCP SDK converters for now. This slice moves protocol/result shapes only; no live MCP transport/session/converter code moves.
Tech Stack: Python 3.14, dataclasses, typing Protocols, pytest, Ruff, basedpyright, src/ package layout.
Boundaries
Move only:
ToolCallResultBackendAdapterToolExecutor
Do not move:
McpSdkAdapterwf_mcp.sdk.converterswf_mcp.runtime.sessionwf_mcp.runtime.factorywf_mcp.runtime.poolwf_mcp.broker.discoverywf_mcp.broker.service.upstream_transport
Temporary dependency note:
BackendAdapterandToolExecutorstill referencewf_mcp.models.ConnectionConfiguntil the connection runtime DTO moves out ofwf_mcp.- They should use
wf_sources_mcp.auth.AuthRecordandwf_sources_mcp.catalogDTOs.
File Map
Create:
src/wf_sources_mcp/sdk/__init__.py— exportsBackendAdapter,ToolCallResult,ToolExecutor.src/wf_sources_mcp/sdk/protocols.py— canonical protocol/result shapes.tests/wf_sources_mcp/test_sdk_protocols.py— canonical tests.
Modify:
src/wf_sources_mcp/__init__.py— optionally lazy-export the protocol symbols.src/wf_mcp/sdk/base.py— replace with compatibility shim.src/wf_mcp/sdk/__init__.py— importBackendAdapterandToolCallResultfrom canonical module, keepMcpSdkAdapter.src/wf_mcp/runtime/protocols.py— replaceToolExecutordefinition with shim import.src/wf_mcp/runtime/__init__.py— continue exportingToolExecutor.src/wf_mcp/sdk/adapter.py— importBackendAdapter,ToolCallResultfromwf_sources_mcp.sdk.src/wf_mcp/sdk/converters.py— importToolCallResultfromwf_sources_mcp.sdk.- Production code that imports
BackendAdapter,ToolCallResult, orToolExecutormay be rewritten towf_sources_mcp.sdk. tests/wf_mcp/test_compat_imports.py— add shim identity tests.tests/wf_sources_mcp/test_import_direction_guard.py— forbid importing oldwf_mcp.sdk/wf_mcp.runtime.protocolsfromwf_sources_mcp.docs/current_roadmap.md— mark SDK protocol slice complete.docs/superpowers/specs/2026-06-03-long-lived-workflow-api-boundary.md— add the completed slice before runtime/session services.
After implementation, move this plan to:
docs/historical/superpowers/plans/2026-06-07-wf-sources-mcp-sdk-protocols-slice.md
Task 1: Create Canonical SDK Protocol Module
Files:
-
Create:
src/wf_sources_mcp/sdk/protocols.py -
Create:
src/wf_sources_mcp/sdk/__init__.py -
Test:
tests/wf_sources_mcp/test_sdk_protocols.py -
Step 1: Create
protocols.py
Create src/wf_sources_mcp/sdk/protocols.py:
"""Protocol/result contracts for MCP upstream source providers.
The temporary `wf_mcp.models.ConnectionConfig` dependency remains until broker
runtime connection DTOs move to a neutral/source-provider package.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any, Protocol
from wf_sources_mcp.auth import AuthRecord
from wf_sources_mcp.catalog import DiscoveredPrompt, DiscoveredResource, DiscoveredTool
if TYPE_CHECKING:
from wf_mcp.models import ConnectionConfig
@dataclass(slots=True)
class ToolCallResult:
outcome: str
output: dict[str, Any] = field(default_factory=dict)
meta: dict[str, Any] = field(default_factory=dict)
class BackendAdapter(Protocol):
async def list_tools(
self,
connection: ConnectionConfig,
auth: AuthRecord | None,
) -> list[DiscoveredTool]: ...
async def list_resources(
self,
connection: ConnectionConfig,
auth: AuthRecord | None,
) -> list[DiscoveredResource]: ...
async def list_prompts(
self,
connection: ConnectionConfig,
auth: AuthRecord | None,
) -> list[DiscoveredPrompt]: ...
async def get_connection_metadata(
self,
connection: ConnectionConfig,
auth: AuthRecord | None,
) -> dict[str, Any]: ...
async def read_resource(
self,
connection: ConnectionConfig,
auth: AuthRecord | None,
uri: str,
) -> dict[str, Any]: ...
async def get_prompt(
self,
connection: ConnectionConfig,
auth: AuthRecord | None,
prompt_name: str,
arguments: dict[str, str] | None = None,
) -> dict[str, Any]: ...
async def invoke_method(
self,
connection: ConnectionConfig,
auth: AuthRecord | None,
method: str,
params: dict[str, Any] | None = None,
) -> dict[str, Any]: ...
async def send_notification(
self,
connection: ConnectionConfig,
auth: AuthRecord | None,
method: str,
params: dict[str, Any] | None = None,
) -> None: ...
async def call_tool(
self,
connection: ConnectionConfig,
auth: AuthRecord | None,
tool_name: str,
payload: dict[str, Any],
) -> ToolCallResult: ...
class ToolExecutor(Protocol):
"""Runtime boundary for executing MCP tools from workflow nodes.
Discovery can stay one-shot, but workflow execution needs this smaller
protocol so persistent runtime pools can replace one-shot adapters without
changing generated NodeSpecs.
"""
async def call_tool(
self,
connection: ConnectionConfig,
auth: AuthRecord | None,
tool_name: str,
payload: dict[str, Any],
) -> ToolCallResult: ...
__all__ = [
"BackendAdapter",
"ToolCallResult",
"ToolExecutor",
]
- Step 2: Create SDK package exports
Create src/wf_sources_mcp/sdk/__init__.py:
from __future__ import annotations
from .protocols import BackendAdapter, ToolCallResult, ToolExecutor
__all__ = [
"BackendAdapter",
"ToolCallResult",
"ToolExecutor",
]
- Step 3: Add canonical tests
Create tests/wf_sources_mcp/test_sdk_protocols.py:
from __future__ import annotations
from dataclasses import is_dataclass
from typing import cast
from wf_mcp.models import ConnectionConfig
from wf_sources_mcp.auth import AuthRecord
from wf_sources_mcp.catalog import DiscoveredTool
from wf_sources_mcp.sdk import BackendAdapter, ToolCallResult, ToolExecutor
class EchoAdapter:
async def list_tools(
self,
connection: ConnectionConfig,
auth: AuthRecord | None,
) -> list[DiscoveredTool]:
return [
DiscoveredTool(
name="echo",
title=None,
description="Echo",
input_schema={"type": "object"},
output_schema={"type": "object"},
)
]
async def call_tool(
self,
connection: ConnectionConfig,
auth: AuthRecord | None,
tool_name: str,
payload: dict[str, object],
) -> ToolCallResult:
return ToolCallResult(outcome="ok", output={"echoed": payload})
def test_tool_call_result_is_slots_dataclass_with_empty_defaults() -> None:
result = ToolCallResult(outcome="ok")
assert is_dataclass(result)
assert result.output == {}
assert result.meta == {}
async def test_backend_adapter_protocol_can_describe_tool_listing() -> None:
adapter = cast(BackendAdapter, EchoAdapter())
tools = await adapter.list_tools(
ConnectionConfig(id="demo.default", server="demo", account="default"),
None,
)
assert tools[0].name == "echo"
async def test_tool_executor_protocol_can_describe_tool_calls() -> None:
executor = cast(ToolExecutor, EchoAdapter())
result = await executor.call_tool(
ConnectionConfig(id="demo.default", server="demo", account="default"),
None,
"echo",
{"message": "hello"},
)
assert result.outcome == "ok"
assert result.output == {"echoed": {"message": "hello"}}
- Step 4: Run canonical tests
Run:
uv run pytest tests/wf_sources_mcp/test_sdk_protocols.py -q
Expected: 3 tests pass.
Task 2: Replace Old Protocol Modules With Shims
Files:
-
Modify:
src/wf_mcp/sdk/base.py -
Modify:
src/wf_mcp/sdk/__init__.py -
Modify:
src/wf_mcp/runtime/protocols.py -
Modify:
src/wf_mcp/runtime/__init__.py -
Modify:
tests/wf_mcp/test_compat_imports.py -
Step 1: Replace
wf_mcp.sdk.basewith shim
Set src/wf_mcp/sdk/base.py to:
"""Compatibility shim for MCP upstream SDK protocol/result types.
Canonical implementation lives in `wf_sources_mcp.sdk`.
"""
from __future__ import annotations
from wf_sources_mcp.sdk import BackendAdapter, ToolCallResult
__all__ = [
"BackendAdapter",
"ToolCallResult",
]
- Step 2: Update
wf_mcp.sdk.__init__
Set src/wf_mcp/sdk/__init__.py to:
from wf_sources_mcp.sdk import BackendAdapter, ToolCallResult
from .adapter import McpSdkAdapter
__all__ = ["BackendAdapter", "McpSdkAdapter", "ToolCallResult"]
- Step 3: Replace
wf_mcp.runtime.protocolswith shim
Set src/wf_mcp/runtime/protocols.py to:
"""Compatibility shim for MCP runtime execution protocol.
Canonical implementation lives in `wf_sources_mcp.sdk`.
"""
from __future__ import annotations
from wf_sources_mcp.sdk import ToolExecutor
__all__ = [
"ToolExecutor",
]
- Step 4: Keep
wf_mcp.runtime.__init__facade
src/wf_mcp/runtime/__init__.py should still export ToolExecutor:
from .factory import PersistentSessionFactory
from .pool import McpRuntimePool, connection_runtime_fingerprint
from .protocols import ToolExecutor
from .session import PersistentMcpSession
__all__ = [
"McpRuntimePool",
"PersistentMcpSession",
"PersistentSessionFactory",
"ToolExecutor",
"connection_runtime_fingerprint",
]
- Step 5: Add shim identity tests
Append to tests/wf_mcp/test_compat_imports.py:
def test_wf_mcp_sdk_protocol_shims_reexport_wf_sources_mcp_sdk() -> None:
from wf_mcp.sdk import BackendAdapter as CompatBackendAdapter
from wf_mcp.sdk import ToolCallResult as CompatToolCallResult
from wf_mcp.sdk.base import BackendAdapter as CompatBaseBackendAdapter
from wf_mcp.sdk.base import ToolCallResult as CompatBaseToolCallResult
from wf_sources_mcp.sdk import BackendAdapter, ToolCallResult
assert CompatBackendAdapter is BackendAdapter
assert CompatToolCallResult is ToolCallResult
assert CompatBaseBackendAdapter is BackendAdapter
assert CompatBaseToolCallResult is ToolCallResult
def test_wf_mcp_runtime_protocol_shim_reexports_wf_sources_mcp_tool_executor() -> None:
from wf_mcp.runtime import ToolExecutor as CompatRuntimeToolExecutor
from wf_mcp.runtime.protocols import ToolExecutor as CompatProtocolToolExecutor
from wf_sources_mcp.sdk import ToolExecutor
assert CompatRuntimeToolExecutor is ToolExecutor
assert CompatProtocolToolExecutor is ToolExecutor
- Step 6: Run compatibility tests
Run:
uv run pytest tests/wf_mcp/test_compat_imports.py tests/wf_mcp/test_workflow_wrappers.py -q
Expected: compatibility tests pass.
Task 3: Rewrite Production Imports to Canonical Protocols
Files:
-
Modify: production files under
src/wf_mcp/that consumeBackendAdapter,ToolCallResult, orToolExecutor. -
Step 1: Update direct consumers
Rewrite production imports for moved symbols to:
from wf_sources_mcp.sdk import BackendAdapter, ToolCallResult, ToolExecutor
Likely files:
src/wf_mcp/broker/discovery.pysrc/wf_mcp/broker/service/adapters.pysrc/wf_mcp/broker/service/core.pysrc/wf_mcp/broker/service/source_catalog.pysrc/wf_mcp/broker/service/upstream_transport.pysrc/wf_mcp/runtime/session.pysrc/wf_mcp/runtime/pool.pysrc/wf_mcp/sdk/adapter.pysrc/wf_mcp/sdk/converters.pysrc/wf_mcp/workflow/wrappers.py
Do not move or rewrite imports for:
-
McpSdkAdapter -
McpRuntimePool -
PersistentSessionFactory -
PersistentMcpSession -
tool_result_to_call_result -
workflow_output_schema_from_mcp_tool_schema -
Step 2: Confirm production imports no longer depend on old protocol shims
Run:
rg -n "from wf_mcp\\.sdk import (BackendAdapter|ToolCallResult)|from wf_mcp\\.sdk\\.base|from wf_mcp\\.runtime import ToolExecutor|from wf_mcp\\.runtime\\.protocols|from \\.\\.sdk import (BackendAdapter|ToolCallResult)|from \\.sdk import (BackendAdapter|ToolCallResult)|from \\.\\.runtime import ToolExecutor|from \\.runtime import ToolExecutor" src
Expected: old protocol/result imports remain only in shim/facade files or imports of non-moved runtime/adapter classes.
- Step 3: Run focused production tests
Run:
uv run pytest tests/wf_mcp/test_workflow_wrappers.py tests/wf_mcp/test_sdk_adapter.py tests/wf_mcp/test_sdk_converters.py tests/wf_mcp/test_stateful_runtime.py tests/wf_mcp/service/test_upstream_transport.py tests/wf_mcp/service/test_catalog.py -q
Expected: all focused tests pass.
Task 4: Strengthen Import-Direction Guard
Files:
-
Modify:
tests/wf_sources_mcp/test_import_direction_guard.py -
Test:
tests/wf_sources_mcp/test_import_direction_guard.py -
Step 1: Add old SDK/runtime protocol modules to forbidden set
Append this test to tests/wf_sources_mcp/test_import_direction_guard.py:
def test_wf_sources_mcp_does_not_import_old_sdk_protocol_modules() -> None:
root = Path(__file__).resolve().parents[2] / "src" / "wf_sources_mcp"
forbidden = {
"wf_mcp.sdk",
"wf_mcp.sdk.base",
"wf_mcp.runtime",
"wf_mcp.runtime.protocols",
}
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 SDK/runtime protocol modules:\n"
+ "\n".join(f" {violation}" for violation in violations)
)
- Step 2: Run guard tests
Run:
uv run pytest tests/wf_sources_mcp/test_import_direction_guard.py -q
Expected: guard tests pass.
Task 5: Docs Status and Plan Archival
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-07-wf-sources-mcp-sdk-protocols-slice.mdtodocs/historical/superpowers/plans/2026-06-07-wf-sources-mcp-sdk-protocols-slice.md -
Step 1: Update roadmap
In docs/current_roadmap.md, under the MCP package split section, add:
Fourth `wf_sources_mcp` slice complete: upstream SDK protocol/result
types (`BackendAdapter`, `ToolExecutor`, `ToolCallResult`) now live in
`wf_sources_mcp.sdk`, with `wf_mcp.sdk` and `wf_mcp.runtime.protocols`
retained as compatibility shims.
- Step 2: Update long-lived API boundary spec
In docs/superpowers/specs/2026-06-03-long-lived-workflow-api-boundary.md, update the wf_sources_mcp status list so it includes:
4. Complete: upstream SDK protocol/result types moved to `wf_sources_mcp.sdk`, with `wf_mcp.sdk` and `wf_mcp.runtime.protocols` retained as shims.
5. Upstream transport/discovery/session services.
- Step 3: Move completed plan to historical
Run:
git mv docs/superpowers/plans/2026-06-07-wf-sources-mcp-sdk-protocols-slice.md docs/historical/superpowers/plans/2026-06-07-wf-sources-mcp-sdk-protocols-slice.md
Expected: git status --short shows an R rename for this plan.
Task 6: Final Verification
Files:
-
All changed files
-
Step 1: Run focused extraction tests
Run:
uv run pytest tests/wf_sources_mcp tests/wf_mcp/test_compat_imports.py tests/wf_mcp/test_workflow_wrappers.py tests/wf_mcp/test_sdk_adapter.py tests/wf_mcp/test_sdk_converters.py tests/wf_mcp/test_stateful_runtime.py tests/wf_mcp/service/test_upstream_transport.py tests/wf_mcp/service/test_catalog.py -q
Expected: all focused tests pass.
- Step 2: Run lint and type checks
Run:
uv run ruff check src tests
uv run basedpyright --level error src
Expected: Ruff reports All checks passed!; basedpyright reports 0 errors.
- Step 3: Run full suite
Run:
uv run pytest -q
Expected: full suite passes with current skip/xfail counts.
- Step 4: Review remaining old protocol imports
Run:
rg -n "from wf_mcp\\.sdk import (BackendAdapter|ToolCallResult)|from wf_mcp\\.sdk\\.base|from wf_mcp\\.runtime import ToolExecutor|from wf_mcp\\.runtime\\.protocols" src tests
Expected: remaining occurrences are compatibility shims, facade exports, or tests intentionally exercising old import paths.
- Step 5: Report
Report:
- files created/modified
- focused/full verification output
- whether
wf_sources_mcp.sdkownsBackendAdapter,ToolExecutor, andToolCallResult - whether compatibility shims remain
- deviations from this plan
Do not commit unless the user explicitly asks. If committing, use:
git add -A
git commit -m "refactor: move mcp sdk protocols to wf_sources_mcp"
Self-Review
- Spec coverage: moves source-provider protocol/result seams needed before runtime/session migration.
- Placeholder scan: no
TODO,TBD, or unspecified test steps. - Type consistency:
BackendAdapter,ToolExecutor, andToolCallResultretain existing signatures and names, with only canonical import ownership changing.