refactor: move mcp sdk adapter to wf_sources_mcp
This commit is contained in:
@@ -1,740 +0,0 @@
|
||||
# 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 `McpSdkAdapter` implementation.
|
||||
- Uses `wf_sources_mcp.client.open_mcp_session`.
|
||||
- Uses `wf_sources_mcp.sdk.converters`.
|
||||
- Imports no `wf_mcp` modules.
|
||||
- Modify `src/wf_sources_mcp/sdk/__init__.py`
|
||||
- Export `McpSdkAdapter`.
|
||||
- Replace `src/wf_mcp/sdk/adapter.py`
|
||||
- Compatibility shim re-exporting `McpSdkAdapter`.
|
||||
- Modify `src/wf_mcp/sdk/__init__.py`
|
||||
- Import `McpSdkAdapter` from `wf_sources_mcp.sdk`.
|
||||
- Modify production imports that construct real adapters:
|
||||
- `src/wf_mcp/broker/config.py`
|
||||
- `src/wf_mcp/broker/server.py`
|
||||
- `src/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.py`
|
||||
- `tests/wf_sources_mcp/test_import_direction_guard.py`
|
||||
- Update docs:
|
||||
- `docs/current_roadmap.md`
|
||||
- `docs/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`, or `send_notification` in this slice.
|
||||
- Do not change `BackendAdapter` method signatures.
|
||||
- Do not change `McpSdkAdapter` behavior 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_mcp` from any `src/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`:
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```python
|
||||
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`:
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```python
|
||||
"""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:
|
||||
|
||||
```python
|
||||
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`:
|
||||
|
||||
```python
|
||||
|
||||
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:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```python
|
||||
from ..sdk import McpSdkAdapter
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```python
|
||||
from wf_sources_mcp.sdk import McpSdkAdapter
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Update broker server import**
|
||||
|
||||
In `src/wf_mcp/broker/server.py`, replace:
|
||||
|
||||
```python
|
||||
from ..sdk.adapter import McpSdkAdapter
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```python
|
||||
from wf_sources_mcp.sdk import McpSdkAdapter
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Update legacy server core import**
|
||||
|
||||
In `src/wf_mcp/server/core.py`, replace:
|
||||
|
||||
```python
|
||||
from ..sdk import McpSdkAdapter
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```python
|
||||
from wf_sources_mcp.sdk import McpSdkAdapter
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run focused construction tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```bash
|
||||
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.md` to `docs/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:
|
||||
|
||||
```markdown
|
||||
- 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:
|
||||
|
||||
```markdown
|
||||
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:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```bash
|
||||
uv run basedpyright --level error src
|
||||
```
|
||||
|
||||
Expected: `0 errors, 0 warnings, 0 notes`.
|
||||
|
||||
- [ ] **Step 3: Run focused lint**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```bash
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Expected: no whitespace errors. CRLF warnings are acceptable on Windows.
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
|
||||
- Spec coverage: This plan moves canonical `McpSdkAdapter` ownership to `wf_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 `BackendAdapter` types: `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.
|
||||
Reference in New Issue
Block a user