22 KiB
MCP SDK Adapter Package 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 concrete one-shot MCP SDK adapter from wf_mcp.sdk.adapter to canonical ownership in wf_sources_mcp.sdk.adapter while preserving old imports as compatibility shims.
Architecture: wf_sources_mcp owns MCP-as-upstream-source code: source DTOs, auth, catalog DTOs, SDK protocols/converters, session opener, persistent runtime, and now the concrete one-shot adapter. wf_mcp remains a compatibility/broker/frontend package and should re-export McpSdkAdapter without owning its implementation. This slice must not change runtime behavior or expand persistent sessions beyond tool calls.
Tech Stack: Python 3.14, MCP Python SDK, Pydantic, pytest/pytest-asyncio, ruff, basedpyright.
File Structure
- Create
src/wf_sources_mcp/sdk/adapter.py- Canonical
McpSdkAdapterimplementation. - Uses
wf_sources_mcp.client.open_mcp_session. - Uses
wf_sources_mcp.sdk.converters. - Imports no
wf_mcpmodules.
- Canonical
- Modify
src/wf_sources_mcp/sdk/__init__.py- Export
McpSdkAdapter.
- Export
- Replace
src/wf_mcp/sdk/adapter.py- Compatibility shim re-exporting
McpSdkAdapter.
- Compatibility shim re-exporting
- Modify
src/wf_mcp/sdk/__init__.py- Import
McpSdkAdapterfromwf_sources_mcp.sdk.
- Import
- Modify production imports that construct real adapters:
src/wf_mcp/broker/config.pysrc/wf_mcp/broker/server.pysrc/wf_mcp/server/core.py
- Add canonical tests:
tests/wf_sources_mcp/test_sdk_adapter.py
- Extend compatibility/import tests:
tests/wf_mcp/test_compat_imports.pytests/wf_sources_mcp/test_import_direction_guard.py
- Update docs:
docs/current_roadmap.mddocs/superpowers/specs/2026-06-03-long-lived-workflow-api-boundary.md
Hard Boundaries
- Do not add persistent runtime support for
read_resource,get_prompt,invoke_method, orsend_notificationin this slice. - Do not change
BackendAdaptermethod signatures. - Do not change
McpSdkAdapterbehavior or output payloads. - Do not remove old import paths.
- Do not modify generated MCP protocol models or frontend MCP tool schemas.
- Do not import
wf_mcpfrom anysrc/wf_sources_mcp/**file.
Task 1: Add Canonical Adapter Test
Files:
-
Create:
tests/wf_sources_mcp/test_sdk_adapter.py -
Step 1: Create the canonical adapter test file
Create tests/wf_sources_mcp/test_sdk_adapter.py:
from __future__ import annotations
from typing import Any
import pytest
from mcp import ClientResult
from mcp.types import (
CallToolResult,
ClientNotification,
ClientRequest,
ListPromptsResult,
ListResourcesResult,
ListToolsResult,
Prompt,
Resource,
TextContent,
Tool,
)
from pydantic import AnyUrl
from wf_sources_mcp.connections import McpSourceConnection
from wf_sources_mcp.sdk import BackendAdapter, McpSdkAdapter
from wf_sources_mcp.transports import StdioSourceTransport
def _connection() -> McpSourceConnection:
return McpSourceConnection(
id="demo.personal",
provider="demo",
account="personal",
transport=StdioSourceTransport(command="fake"),
)
class _FakeSession:
def __init__(self) -> None:
self.notifications: list[ClientNotification] = []
self.requests: list[ClientRequest] = []
async def list_tools(self) -> ListToolsResult:
return ListToolsResult(
tools=[
Tool(
name="echo",
title="Echo",
description="Echo text.",
inputSchema={"type": "object", "properties": {}},
)
]
)
async def list_resources(self) -> ListResourcesResult:
return ListResourcesResult(
resources=[
Resource(
uri=AnyUrl("fixture://docs/welcome"),
name="resource.welcome",
title="Welcome",
description="Welcome resource.",
mimeType="text/plain",
)
]
)
async def list_prompts(self) -> ListPromptsResult:
return ListPromptsResult(
prompts=[
Prompt(
name="prompt.summarize",
title="Summarize",
description="Summarize input.",
arguments=[],
)
]
)
async def read_resource(self, uri: AnyUrl) -> Any:
return type(
"ReadResourceResult",
(),
{
"model_dump": lambda _self, **_kwargs: {
"contents": [{"uri": str(uri), "text": "hello"}]
}
},
)()
async def get_prompt(
self,
prompt_name: str,
arguments: dict[str, str] | None = None,
) -> Any:
return type(
"GetPromptResult",
(),
{
"model_dump": lambda _self, **_kwargs: {
"messages": [
{
"role": "user",
"content": {
"type": "text",
"text": f"{prompt_name}:{arguments or {}}",
},
}
]
}
},
)()
async def send_request(
self,
request: ClientRequest,
result_type: type[ClientResult],
) -> Any:
assert result_type is ClientResult
self.requests.append(request)
return type(
"ClientResultModel",
(),
{"model_dump": lambda _self, **_kwargs: {"ok": True}},
)()
async def send_notification(self, notification: ClientNotification) -> None:
self.notifications.append(notification)
async def call_tool(
self,
tool_name: str,
payload: dict[str, Any],
) -> CallToolResult:
return CallToolResult(
content=[TextContent(type="text", text="ok")],
structuredContent={"tool": tool_name, "payload": payload},
)
class _SessionContext:
def __init__(self, session: _FakeSession) -> None:
self.session = session
async def __aenter__(self) -> _FakeSession:
return self.session
async def __aexit__(
self,
exc_type: type[BaseException] | None,
exc: BaseException | None,
tb: object | None,
) -> None:
return None
class _FakeAdapter(McpSdkAdapter):
def __init__(self, session: _FakeSession) -> None:
self.fake_session = session
def _session(self, connection: McpSourceConnection, auth: object | None):
assert connection.id == "demo.personal"
assert auth is None
return _SessionContext(self.fake_session)
def test_mcp_sdk_adapter_implements_backend_protocol() -> None:
adapter: BackendAdapter = McpSdkAdapter()
assert adapter.__class__.__name__ == "McpSdkAdapter"
@pytest.mark.asyncio
async def test_mcp_sdk_adapter_uses_session_for_all_backend_methods() -> None:
session = _FakeSession()
adapter = _FakeAdapter(session)
connection = _connection()
tools = await adapter.list_tools(connection, None)
resources = await adapter.list_resources(connection, None)
prompts = await adapter.list_prompts(connection, None)
metadata = await adapter.get_connection_metadata(connection, None)
resource_payload = await adapter.read_resource(
connection,
None,
"fixture://docs/welcome",
)
prompt_payload = await adapter.get_prompt(
connection,
None,
"prompt.summarize",
{"text": "hello"},
)
method_payload = await adapter.invoke_method(
connection,
None,
"fixture/ping",
{"value": 1},
)
await adapter.send_notification(connection, None, "fixture/notify", {"ok": True})
tool_result = await adapter.call_tool(connection, None, "echo", {"text": "hello"})
assert tools[0].name == "echo"
assert resources[0].uri == "fixture://docs/welcome"
assert prompts[0].name == "prompt.summarize"
assert metadata == {"server": "demo", "transport": "stdio"}
assert resource_payload["contents"][0]["text"] == "hello"
assert prompt_payload["messages"][0]["content"]["text"] == (
"prompt.summarize:{'text': 'hello'}"
)
assert method_payload == {"ok": True}
assert session.requests[0].method == "fixture/ping"
assert session.notifications[0].method == "fixture/notify"
assert tool_result.outcome == "ok"
assert tool_result.output == {
"tool": "echo",
"payload": {"text": "hello"},
}
- Step 2: Run the canonical test and confirm it fails
Run:
uv run pytest tests/wf_sources_mcp/test_sdk_adapter.py -q
Expected: fail with an import error because wf_sources_mcp.sdk.McpSdkAdapter does not exist yet.
Task 2: Move Adapter Implementation to wf_sources_mcp
Files:
-
Create:
src/wf_sources_mcp/sdk/adapter.py -
Modify:
src/wf_sources_mcp/sdk/__init__.py -
Step 1: Create canonical adapter module
Create src/wf_sources_mcp/sdk/adapter.py by moving the implementation from src/wf_mcp/sdk/adapter.py, adjusted to canonical imports:
from __future__ import annotations
from contextlib import asynccontextmanager
from typing import Any
from mcp import ClientResult
from mcp.types import (
ClientNotification,
ClientRequest,
ListPromptsResult,
ListResourcesResult,
ListToolsResult,
)
from pydantic import AnyUrl
from wf_sources_mcp.auth import AuthRecord
from wf_sources_mcp.catalog import DiscoveredPrompt, DiscoveredResource, DiscoveredTool
from wf_sources_mcp.client import open_mcp_session
from wf_sources_mcp.connections import McpSourceConnection
from .converters import (
prompt_to_discovered,
resource_to_discovered,
tool_result_to_call_result,
tool_to_discovered,
)
from .protocols import BackendAdapter, ToolCallResult
class McpSdkAdapter(BackendAdapter):
"""One-shot MCP client adapter for upstream MCP source operations.
This adapter intentionally opens a fresh SDK session per operation. Stateful
workflow tool execution is handled by `wf_sources_mcp.runtime`; discovery
and admin operations use this simpler one-shot path.
"""
@asynccontextmanager
async def _session(
self,
connection: McpSourceConnection,
auth: AuthRecord | None,
):
async with open_mcp_session(connection, auth) as session:
yield session
async def list_tools(
self,
connection: McpSourceConnection,
auth: AuthRecord | None,
) -> list[DiscoveredTool]:
async with self._session(connection, auth) as session:
result: ListToolsResult = await session.list_tools()
return [tool_to_discovered(tool) for tool in result.tools]
async def list_resources(
self,
connection: McpSourceConnection,
auth: AuthRecord | None,
) -> list[DiscoveredResource]:
async with self._session(connection, auth) as session:
result: ListResourcesResult = await session.list_resources()
return [resource_to_discovered(resource) for resource in result.resources]
async def list_prompts(
self,
connection: McpSourceConnection,
auth: AuthRecord | None,
) -> list[DiscoveredPrompt]:
async with self._session(connection, auth) as session:
result: ListPromptsResult = await session.list_prompts()
return [prompt_to_discovered(prompt) for prompt in result.prompts]
async def get_connection_metadata(
self,
connection: McpSourceConnection,
auth: AuthRecord | None,
) -> dict[str, Any]:
transport = connection.transport
return {
"server": connection.provider,
"transport": transport.kind if transport is not None else None,
}
async def read_resource(
self,
connection: McpSourceConnection,
auth: AuthRecord | None,
uri: str,
) -> dict[str, Any]:
async with self._session(connection, auth) as session:
result = await session.read_resource(AnyUrl(uri))
return result.model_dump(by_alias=True, mode="json", exclude_none=True)
async def get_prompt(
self,
connection: McpSourceConnection,
auth: AuthRecord | None,
prompt_name: str,
arguments: dict[str, str] | None = None,
) -> dict[str, Any]:
async with self._session(connection, auth) as session:
result = await session.get_prompt(prompt_name, arguments)
return result.model_dump(by_alias=True, mode="json", exclude_none=True)
async def invoke_method(
self,
connection: McpSourceConnection,
auth: AuthRecord | None,
method: str,
params: dict[str, Any] | None = None,
) -> dict[str, Any]:
async with self._session(connection, auth) as session:
result = await session.send_request(
ClientRequest.model_validate({"method": method, "params": params}),
ClientResult,
)
return result.model_dump(by_alias=True, mode="json", exclude_none=True)
async def send_notification(
self,
connection: McpSourceConnection,
auth: AuthRecord | None,
method: str,
params: dict[str, Any] | None = None,
) -> None:
async with self._session(connection, auth) as session:
await session.send_notification(
ClientNotification.model_validate({"method": method, "params": params})
)
async def call_tool(
self,
connection: McpSourceConnection,
auth: AuthRecord | None,
tool_name: str,
payload: dict[str, Any],
) -> ToolCallResult:
async with self._session(connection, auth) as session:
result = await session.call_tool(tool_name, payload)
return tool_result_to_call_result(result)
__all__ = ["McpSdkAdapter"]
- Step 2: Export adapter from package root
Modify src/wf_sources_mcp/sdk/__init__.py:
from __future__ import annotations
from .adapter import McpSdkAdapter
from .converters import (
prompt_to_discovered,
resource_to_discovered,
tool_result_to_call_result,
tool_to_discovered,
workflow_output_schema_from_mcp_tool_schema,
)
from .protocols import BackendAdapter, ToolCallResult, ToolExecutor
__all__ = [
"BackendAdapter",
"McpSdkAdapter",
"ToolCallResult",
"ToolExecutor",
"prompt_to_discovered",
"resource_to_discovered",
"tool_result_to_call_result",
"tool_to_discovered",
"workflow_output_schema_from_mcp_tool_schema",
]
- Step 3: Run canonical adapter test
Run:
uv run pytest tests/wf_sources_mcp/test_sdk_adapter.py -q
Expected: pass.
Task 3: Turn Old Adapter Module Into Shim
Files:
-
Modify:
src/wf_mcp/sdk/adapter.py -
Modify:
src/wf_mcp/sdk/__init__.py -
Modify:
tests/wf_mcp/test_compat_imports.py -
Step 1: Replace old adapter module with shim
Replace src/wf_mcp/sdk/adapter.py with:
"""Compatibility shim for the canonical MCP SDK adapter."""
from wf_sources_mcp.sdk.adapter import McpSdkAdapter
__all__ = ["McpSdkAdapter"]
- Step 2: Update old SDK package exports
Replace src/wf_mcp/sdk/__init__.py with:
from wf_sources_mcp.sdk import BackendAdapter, McpSdkAdapter, ToolCallResult
__all__ = ["BackendAdapter", "McpSdkAdapter", "ToolCallResult"]
- Step 3: Add shim identity test
Append to tests/wf_mcp/test_compat_imports.py:
def test_wf_mcp_sdk_adapter_shim_reexports_wf_sources_mcp_adapter() -> None:
from wf_mcp.sdk import McpSdkAdapter as CompatPackageAdapter
from wf_mcp.sdk.adapter import McpSdkAdapter as CompatModuleAdapter
from wf_sources_mcp.sdk import McpSdkAdapter
from wf_sources_mcp.sdk.adapter import McpSdkAdapter as CanonicalModuleAdapter
assert CompatPackageAdapter is McpSdkAdapter
assert CompatModuleAdapter is McpSdkAdapter
assert CanonicalModuleAdapter is McpSdkAdapter
- Step 4: Run compatibility tests
Run:
uv run pytest tests/wf_mcp/test_compat_imports.py -q
Expected: pass.
Task 4: Update Production Construction Imports
Files:
-
Modify:
src/wf_mcp/broker/config.py -
Modify:
src/wf_mcp/broker/server.py -
Modify:
src/wf_mcp/server/core.py -
Step 1: Update broker config import
In src/wf_mcp/broker/config.py, replace:
from ..sdk import McpSdkAdapter
with:
from wf_sources_mcp.sdk import McpSdkAdapter
- Step 2: Update broker server import
In src/wf_mcp/broker/server.py, replace:
from ..sdk.adapter import McpSdkAdapter
with:
from wf_sources_mcp.sdk import McpSdkAdapter
- Step 3: Update legacy server core import
In src/wf_mcp/server/core.py, replace:
from ..sdk import McpSdkAdapter
with:
from wf_sources_mcp.sdk import McpSdkAdapter
- Step 4: Run focused construction tests
Run:
uv run pytest tests/wf_mcp/server/test_config.py tests/wf_mcp/test_broker_server.py tests/wf_transport_rpc_http/test_mcp_backed_server_rpc.py -q
Expected: pass.
Task 5: Strengthen Import Direction Guard
Files:
-
Modify:
tests/wf_sources_mcp/test_import_direction_guard.py -
Step 1: Add old adapter module to forbidden imports
In test_wf_sources_mcp_does_not_import_old_sdk_protocol_modules, extend the forbidden set:
forbidden = {
"wf_mcp.sdk",
"wf_mcp.sdk.adapter",
"wf_mcp.sdk.base",
"wf_mcp.runtime",
"wf_mcp.runtime.protocols",
}
- Step 2: Run import direction tests
Run:
uv run pytest tests/wf_sources_mcp/test_import_direction_guard.py -q
Expected: pass.
Task 6: 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 after completion:
docs/superpowers/plans/2026-06-07-mcp-sdk-adapter-package-move.mdtodocs/historical/superpowers/plans/2026-06-07-mcp-sdk-adapter-package-move.md -
Step 1: Update roadmap status
In docs/current_roadmap.md, under the MCP upstream source runtime cleanup bullets near the existing runtime move status, add:
- Completed: one-shot MCP SDK adapter moved to `wf_sources_mcp.sdk.adapter`.
`McpSdkAdapter` is now canonical in `wf_sources_mcp`; `wf_mcp.sdk.*`
remains a compatibility shim for old imports. Persistent runtime is still
tool-call-only.
- Step 2: Update long-lived API boundary status
In docs/superpowers/specs/2026-06-03-long-lived-workflow-api-boundary.md, in the numbered wf_sources_mcp implementation status list after item 7, add:
8. Complete: one-shot MCP SDK adapter moved to
`wf_sources_mcp.sdk.adapter`, with `wf_mcp.sdk.adapter` retained as a
compatibility shim. This does not expand persistent runtime; the next
design slice should unify one-shot and persistent client operation handling
behind a shared source-client facade.
- Step 3: Archive completed plan after implementation
Run only after all code/tests pass:
git mv docs/superpowers/plans/2026-06-07-mcp-sdk-adapter-package-move.md docs/historical/superpowers/plans/2026-06-07-mcp-sdk-adapter-package-move.md
Task 7: Final Verification
Files:
-
All changed files.
-
Step 1: Run focused tests
Run:
uv run pytest tests/wf_sources_mcp tests/wf_mcp/test_sdk_adapter.py tests/wf_mcp/test_compat_imports.py tests/wf_mcp/server/test_config.py tests/wf_mcp/test_broker_server.py tests/wf_transport_rpc_http/test_mcp_backed_server_rpc.py -q
Expected: pass. If a live stdio/server test is skipped because the environment blocks subprocess MCP transports, report the skip exactly.
- Step 2: Run source typecheck
Run:
uv run basedpyright --level error src
Expected: 0 errors, 0 warnings, 0 notes.
- Step 3: Run focused lint
Run:
uv run ruff check src/wf_sources_mcp src/wf_mcp/sdk src/wf_mcp/broker/config.py src/wf_mcp/broker/server.py src/wf_mcp/server/core.py tests/wf_sources_mcp/test_sdk_adapter.py tests/wf_mcp/test_compat_imports.py
Expected: All checks passed!
- Step 4: Check old canonical imports
Run:
rg -n "from wf_mcp\.sdk\.adapter|from wf_mcp\.sdk import McpSdkAdapter|import wf_mcp\.sdk" src tests
Expected: only compatibility tests or intentionally old-public-path tests should remain. Production construction paths should import from wf_sources_mcp.sdk.
- Step 5: Check whitespace
Run:
git diff --check
Expected: no whitespace errors. CRLF warnings are acceptable on Windows.
Self-Review
- Spec coverage: This plan moves canonical
McpSdkAdapterownership towf_sources_mcp, preserves compatibility shims, updates production constructors, strengthens import guards, and updates docs. - Placeholder scan: No placeholder steps remain. All code edits include concrete snippets.
- Type consistency: All adapter signatures use current
BackendAdaptertypes:McpSourceConnection,AuthRecord | None,ToolCallResult. - Intentional non-goal: persistent runtime remains tool-call-only. Shared one-shot/persistent client-operation facade is a future design slice, not part of this move.