refactor: move mcp sdk converters to wf_sources_mcp
This commit is contained in:
@@ -0,0 +1,517 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user