# wf_sources_mcp SDK Converters 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 SDK conversion helpers into `wf_sources_mcp.sdk.converters` so adapter/runtime code can later move without depending on `wf_mcp.sdk.converters`. **Architecture:** `wf_sources_mcp` owns upstream MCP conversion logic from MCP SDK models into workflow-source DTOs. `wf_mcp.sdk.converters` remains a compatibility shim. This slice moves pure conversion functions only; it does not move `McpSdkAdapter`, runtime sessions, broker discovery, or upstream transport services. **Tech Stack:** Python 3.14, MCP SDK model types, pytest, Ruff, basedpyright, `src/` package layout. --- ## Boundaries Move only: - `tool_to_discovered` - `resource_to_discovered` - `prompt_to_discovered` - `tool_result_to_call_result` - `workflow_output_schema_from_mcp_tool_schema` Do not move: - `McpSdkAdapter` - `PersistentMcpSession` - `PersistentSessionFactory` - `McpRuntimePool` - `discover_connection_capabilities` - `snapshot_from_specs` - `UpstreamTransportService` ## File Map Create: - `src/wf_sources_mcp/sdk/converters.py` — canonical converter helpers. - `tests/wf_sources_mcp/test_sdk_converters.py` — canonical converter tests. Modify: - `src/wf_sources_mcp/sdk/__init__.py` — export converter helpers if convenient. - `src/wf_mcp/sdk/converters.py` — compatibility shim. - `src/wf_mcp/sdk/adapter.py` — import converters from canonical path. - `src/wf_mcp/runtime/session.py` — import `tool_result_to_call_result` from canonical path. - `src/wf_mcp/broker/catalog.py` — import `workflow_output_schema_from_mcp_tool_schema` from canonical path. - `tests/wf_mcp/test_compat_imports.py` — shim identity test. - `tests/wf_sources_mcp/test_import_direction_guard.py` — guard against old converter imports. - `docs/current_roadmap.md` — mark converter slice complete. - `docs/superpowers/specs/2026-06-03-long-lived-workflow-api-boundary.md` — mark converter slice complete. After implementation, move this plan to: - `docs/historical/superpowers/plans/2026-06-07-wf-sources-mcp-sdk-converters-slice.md` --- ### Task 1: Create Canonical Converter Module **Files:** - Create: `src/wf_sources_mcp/sdk/converters.py` - Modify: `src/wf_sources_mcp/sdk/__init__.py` - Test: `tests/wf_sources_mcp/test_sdk_converters.py` - [ ] **Step 1: Create converter module** Create `src/wf_sources_mcp/sdk/converters.py` by moving the current implementation from `src/wf_mcp/sdk/converters.py`: ```python from __future__ import annotations from typing import Any from mcp.types import CallToolResult as McpCallToolResult from mcp.types import Prompt as McpPrompt from mcp.types import Resource as McpResource from mcp.types import Tool as McpTool from wf_sources_mcp.catalog import DiscoveredPrompt, DiscoveredResource, DiscoveredTool from wf_sources_mcp.sdk import ToolCallResult def tool_to_discovered(tool: McpTool) -> DiscoveredTool: """Convert an MCP SDK tool into the source discovery model.""" output_schema = workflow_output_schema_from_mcp_tool_schema(tool.outputSchema) display_name = ( tool.annotations.title if tool.annotations is not None and tool.annotations.title else tool.title ) return DiscoveredTool( name=tool.name, title=display_name, description=tool.description, input_schema=tool.inputSchema, output_schema=output_schema, outcomes=("ok", "error"), metadata=tool.model_dump(by_alias=True, mode="json"), ) def workflow_output_schema_from_mcp_tool_schema( schema: dict[str, Any] | None, ) -> dict[str, Any]: """Return the MCP tool output schema without inventing workflow fields. MCP tools without structured output expose raw content blocks. Those blocks can be text, images, resource links, or mixed results, so this layer must not pretend there is a stable top-level ``text`` field. Workflow authors should add an explicit wrapper/extraction node for the block shape they expect. """ return schema or { "type": "object", "properties": {"content": {"type": "array"}}, } def resource_to_discovered(resource: McpResource) -> DiscoveredResource: """Convert an MCP SDK resource into the source discovery model.""" local_name = resource.name or str(resource.uri) return DiscoveredResource( uri=str(resource.uri), name=local_name, title=resource.title, description=resource.description, mime_type=resource.mimeType, metadata=resource.model_dump(by_alias=True, mode="json"), ) def prompt_to_discovered(prompt: McpPrompt) -> DiscoveredPrompt: """Convert an MCP SDK prompt into the source discovery model.""" arguments = [ argument.model_dump(by_alias=True, mode="json") for argument in prompt.arguments or [] ] return DiscoveredPrompt( name=prompt.name, title=prompt.title, description=prompt.description, arguments=arguments, metadata=prompt.model_dump(by_alias=True, mode="json"), ) def tool_result_to_call_result(result: McpCallToolResult) -> ToolCallResult: """Convert an MCP SDK tool call result into the adapter result model.""" if result.structuredContent is not None: output = result.structuredContent else: output: dict[str, Any] = { "content": [item.model_dump(by_alias=True) for item in result.content] } return ToolCallResult( outcome="error" if result.isError else "ok", output=output, meta=result.meta or {}, ) __all__ = [ "prompt_to_discovered", "resource_to_discovered", "tool_result_to_call_result", "tool_to_discovered", "workflow_output_schema_from_mcp_tool_schema", ] ``` - [ ] **Step 2: Export converter helpers** Update `src/wf_sources_mcp/sdk/__init__.py` to import and export: ```python from .converters import ( prompt_to_discovered, resource_to_discovered, tool_result_to_call_result, tool_to_discovered, workflow_output_schema_from_mcp_tool_schema, ) ``` Add those names to `__all__`. - [ ] **Step 3: Add canonical converter tests** Create `tests/wf_sources_mcp/test_sdk_converters.py` by copying `tests/wf_mcp/test_sdk_converters.py`, but change imports to: ```python from wf_sources_mcp.sdk.converters import ( tool_result_to_call_result, tool_to_discovered, ) ``` Keep the four existing test bodies unchanged. - [ ] **Step 4: Run canonical converter tests** Run: ```bash uv run pytest tests/wf_sources_mcp/test_sdk_converters.py -q ``` Expected: 4 tests pass. --- ### Task 2: Replace Old Converter Module With Shim **Files:** - Modify: `src/wf_mcp/sdk/converters.py` - Modify: `tests/wf_mcp/test_compat_imports.py` - [ ] **Step 1: Replace old converter module** Set `src/wf_mcp/sdk/converters.py` to: ```python """Compatibility shim for MCP SDK converter helpers. Canonical implementation lives in `wf_sources_mcp.sdk.converters`. """ from __future__ import annotations from wf_sources_mcp.sdk.converters import ( prompt_to_discovered, resource_to_discovered, tool_result_to_call_result, tool_to_discovered, workflow_output_schema_from_mcp_tool_schema, ) __all__ = [ "prompt_to_discovered", "resource_to_discovered", "tool_result_to_call_result", "tool_to_discovered", "workflow_output_schema_from_mcp_tool_schema", ] ``` - [ ] **Step 2: Add shim identity test** Append to `tests/wf_mcp/test_compat_imports.py`: ```python def test_wf_mcp_sdk_converter_shim_reexports_wf_sources_mcp_converters() -> None: from wf_mcp.sdk.converters import tool_result_to_call_result as compat_tool_result from wf_mcp.sdk.converters import tool_to_discovered as compat_tool_to_discovered from wf_mcp.sdk.converters import ( workflow_output_schema_from_mcp_tool_schema as compat_output_schema, ) from wf_sources_mcp.sdk.converters import ( tool_result_to_call_result, tool_to_discovered, workflow_output_schema_from_mcp_tool_schema, ) assert compat_tool_result is tool_result_to_call_result assert compat_tool_to_discovered is tool_to_discovered assert compat_output_schema is workflow_output_schema_from_mcp_tool_schema ``` - [ ] **Step 3: Run compatibility tests** Run: ```bash uv run pytest tests/wf_mcp/test_compat_imports.py tests/wf_mcp/test_sdk_converters.py -q ``` Expected: compatibility tests and old converter tests pass. --- ### Task 3: Rewrite Production Imports to Canonical Converter Path **Files:** - Modify: `src/wf_mcp/sdk/adapter.py` - Modify: `src/wf_mcp/runtime/session.py` - Modify: `src/wf_mcp/broker/catalog.py` - [ ] **Step 1: Update adapter imports** In `src/wf_mcp/sdk/adapter.py`, replace: ```python from .converters import ( prompt_to_discovered, resource_to_discovered, tool_result_to_call_result, tool_to_discovered, ) ``` with: ```python from wf_sources_mcp.sdk.converters import ( prompt_to_discovered, resource_to_discovered, tool_result_to_call_result, tool_to_discovered, ) ``` - [ ] **Step 2: Update runtime session import** In `src/wf_mcp/runtime/session.py`, replace: ```python from ..sdk.converters import tool_result_to_call_result ``` with: ```python from wf_sources_mcp.sdk.converters import tool_result_to_call_result ``` - [ ] **Step 3: Update broker catalog import** In `src/wf_mcp/broker/catalog.py`, replace: ```python from ..sdk.converters import workflow_output_schema_from_mcp_tool_schema ``` with: ```python from wf_sources_mcp.sdk.converters import ( workflow_output_schema_from_mcp_tool_schema, ) ``` - [ ] **Step 4: Confirm production imports no longer use old converter path** Run: ```bash rg -n "wf_mcp\\.sdk\\.converters|\\.sdk\\.converters|\\.\\.sdk\\.converters" src ``` Expected: only `src/wf_mcp/sdk/converters.py` shim may mention `wf_sources_mcp.sdk.converters`; no production code imports old converter path. - [ ] **Step 5: Run focused production tests** Run: ```bash uv run pytest 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_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` - [ ] **Step 1: Add old converter module to forbidden guard** Append this test to `tests/wf_sources_mcp/test_import_direction_guard.py`: ```python def test_wf_sources_mcp_does_not_import_old_sdk_converter_module() -> None: root = Path(__file__).resolve().parents[2] / "src" / "wf_sources_mcp" forbidden = { "wf_mcp.sdk.converters", } 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 converter module:\n" + "\n".join(f" {violation}" for violation in violations) ) ``` - [ ] **Step 2: Run guard tests** Run: ```bash 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-converters-slice.md` to `docs/historical/superpowers/plans/2026-06-07-wf-sources-mcp-sdk-converters-slice.md` - [ ] **Step 1: Update roadmap** In `docs/current_roadmap.md`, under the MCP package split section, add: ```markdown Fifth `wf_sources_mcp` slice complete: MCP SDK conversion helpers now live in `wf_sources_mcp.sdk.converters`, with `wf_mcp.sdk.converters` retained as a compatibility shim. ``` - [ ] **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: ```markdown 5. Complete: MCP SDK conversion helpers moved to `wf_sources_mcp.sdk.converters`, with `wf_mcp.sdk.converters` retained as a shim. 6. Upstream transport/discovery/session services. ``` - [ ] **Step 3: Move completed plan to historical** Run: ```bash git mv docs/superpowers/plans/2026-06-07-wf-sources-mcp-sdk-converters-slice.md docs/historical/superpowers/plans/2026-06-07-wf-sources-mcp-sdk-converters-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: ```bash uv run pytest tests/wf_sources_mcp tests/wf_mcp/test_compat_imports.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_catalog.py -q ``` Expected: all focused tests pass. - [ ] **Step 2: Run lint and type checks** Run: ```bash 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: ```bash uv run pytest -q ``` Expected: full suite passes with current skip/xfail counts. If it times out locally, rerun with a longer timeout before reporting. - [ ] **Step 4: Review remaining old converter imports** Run: ```bash rg -n "wf_mcp\\.sdk\\.converters|from wf_mcp\\.sdk\\.converters|from \\.\\.sdk\\.converters|from \\.sdk\\.converters" src tests ``` Expected: remaining occurrences are compatibility shims or tests intentionally exercising old import paths. - [ ] **Step 5: Report** Report: - files created/modified - focused/full verification output - whether `wf_sources_mcp.sdk.converters` owns all converter helpers - whether `wf_mcp.sdk.converters` remains as a shim - deviations from this plan Do not commit unless the user explicitly asks. If committing, use: ```bash git add -A git commit -m "refactor: move mcp sdk converters to wf_sources_mcp" ``` --- ## Self-Review - Spec coverage: moves the pure conversion helpers needed before moving SDK adapter/runtime sessions. - Placeholder scan: no `TODO`, `TBD`, or unspecified test steps. - Type consistency: function names and behavior match current `wf_mcp.sdk.converters` exactly, with only import ownership changing.