refactor: move mcp sdk protocols to wf_sources_mcp

This commit is contained in:
lda
2026-06-07 01:30:40 +07:00 Verified
parent 9bef0b0f63
commit 1385f935ca
21 changed files with 904 additions and 116 deletions
+5 -1
View File
@@ -283,9 +283,13 @@ implementation state.
models, file store, and conversion helpers now live in models, file store, and conversion helpers now live in
`wf_sources_mcp.source_registry`, with `wf_mcp.source_registry` retained `wf_sources_mcp.source_registry`, with `wf_mcp.source_registry` retained
as a compatibility shim. as a compatibility shim.
Third `wf_sources_mcp` slice complete: upstream MCP catalog/discovery DTOs Third `wf_sources_mcp` slice complete: upstream MCP catalog/discovery DTOs
and catalog snapshot dumping now live in `wf_sources_mcp.catalog`, with and catalog snapshot dumping now live in `wf_sources_mcp.catalog`, with
`wf_mcp.capabilities` and `wf_mcp.catalog.models` retained as shims. `wf_mcp.capabilities` and `wf_mcp.catalog.models` retained as shims.
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.
The `wf-mcp` script is now a legacy/special-purpose MCP entrypoint, not the The `wf-mcp` script is now a legacy/special-purpose MCP entrypoint, not the
preferred durable workflow server. New product paths should target preferred durable workflow server. New product paths should target
`wf-rpc-server` plus neutral `wf_config`/`wf_server` composition, then keep `wf-rpc-server` plus neutral `wf_config`/`wf_server` composition, then keep
@@ -0,0 +1,626 @@
# 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:
- `ToolCallResult`
- `BackendAdapter`
- `ToolExecutor`
Do not move:
- `McpSdkAdapter`
- `wf_mcp.sdk.converters`
- `wf_mcp.runtime.session`
- `wf_mcp.runtime.factory`
- `wf_mcp.runtime.pool`
- `wf_mcp.broker.discovery`
- `wf_mcp.broker.service.upstream_transport`
Temporary dependency note:
- `BackendAdapter` and `ToolExecutor` still reference `wf_mcp.models.ConnectionConfig` until the connection runtime DTO moves out of `wf_mcp`.
- They should use `wf_sources_mcp.auth.AuthRecord` and `wf_sources_mcp.catalog` DTOs.
## File Map
Create:
- `src/wf_sources_mcp/sdk/__init__.py` — exports `BackendAdapter`, `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` — import `BackendAdapter` and `ToolCallResult` from canonical module, keep `McpSdkAdapter`.
- `src/wf_mcp/runtime/protocols.py` — replace `ToolExecutor` definition with shim import.
- `src/wf_mcp/runtime/__init__.py` — continue exporting `ToolExecutor`.
- `src/wf_mcp/sdk/adapter.py` — import `BackendAdapter`, `ToolCallResult` from `wf_sources_mcp.sdk`.
- `src/wf_mcp/sdk/converters.py` — import `ToolCallResult` from `wf_sources_mcp.sdk`.
- Production code that imports `BackendAdapter`, `ToolCallResult`, or `ToolExecutor` may be rewritten to `wf_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 old `wf_mcp.sdk` / `wf_mcp.runtime.protocols` from `wf_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`:
```python
"""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`:
```python
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`:
```python
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:
```bash
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.base` with shim**
Set `src/wf_mcp/sdk/base.py` to:
```python
"""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:
```python
from wf_sources_mcp.sdk import BackendAdapter, ToolCallResult
from .adapter import McpSdkAdapter
__all__ = ["BackendAdapter", "McpSdkAdapter", "ToolCallResult"]
```
- [ ] **Step 3: Replace `wf_mcp.runtime.protocols` with shim**
Set `src/wf_mcp/runtime/protocols.py` to:
```python
"""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`:
```python
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`:
```python
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:
```bash
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 consume `BackendAdapter`, `ToolCallResult`, or `ToolExecutor`.
- [ ] **Step 1: Update direct consumers**
Rewrite production imports for moved symbols to:
```python
from wf_sources_mcp.sdk import BackendAdapter, ToolCallResult, ToolExecutor
```
Likely files:
- `src/wf_mcp/broker/discovery.py`
- `src/wf_mcp/broker/service/adapters.py`
- `src/wf_mcp/broker/service/core.py`
- `src/wf_mcp/broker/service/source_catalog.py`
- `src/wf_mcp/broker/service/upstream_transport.py`
- `src/wf_mcp/runtime/session.py`
- `src/wf_mcp/runtime/pool.py`
- `src/wf_mcp/sdk/adapter.py`
- `src/wf_mcp/sdk/converters.py`
- `src/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:
```bash
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:
```bash
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`:
```python
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:
```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-protocols-slice.md` to `docs/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:
```markdown
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:
```markdown
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:
```bash
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:
```bash
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:
```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.
- [ ] **Step 4: Review remaining old protocol imports**
Run:
```bash
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.sdk` owns `BackendAdapter`, `ToolExecutor`, and `ToolCallResult`
- whether compatibility shims remain
- 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 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`, and `ToolCallResult` retain existing signatures and names, with only canonical import ownership changing.
@@ -91,7 +91,8 @@ First slices should move leaf modules only and leave `wf_mcp` re-export shims:
`wf_sources_mcp`, with `wf_mcp` shims preserved. `wf_sources_mcp`, with `wf_mcp` shims preserved.
2. Complete: MCP source registry models/conversion moved to `wf_sources_mcp.source_registry`, with `wf_mcp.source_registry` retained as a shim. 2. Complete: MCP source registry models/conversion moved to `wf_sources_mcp.source_registry`, with `wf_mcp.source_registry` retained as a shim.
3. Complete: upstream MCP catalog/discovery DTOs moved to `wf_sources_mcp.catalog`, with `wf_mcp.capabilities` and `wf_mcp.catalog.models` retained as shims. 3. Complete: upstream MCP catalog/discovery DTOs moved to `wf_sources_mcp.catalog`, with `wf_mcp.capabilities` and `wf_mcp.catalog.models` retained as shims.
4. Upstream transport/discovery/session services. 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.
Each slice should add import-direction tests so the new source-provider package Each slice should add import-direction tests so the new source-provider package
does not depend on `wf_mcp.workflow_surface`, `wf_mcp.admin_surface`, does not depend on `wf_mcp.workflow_surface`, `wf_mcp.admin_surface`,
+1 -2
View File
@@ -9,11 +9,10 @@ from mcp.types import METHOD_NOT_FOUND
from wf_authoring import NodeSpec from wf_authoring import NodeSpec
from wf_sources_mcp.catalog import DiscoveredPrompt, DiscoveredResource, DiscoveredTool from wf_sources_mcp.catalog import DiscoveredPrompt, DiscoveredResource, DiscoveredTool
from wf_sources_mcp.sdk import BackendAdapter, ToolExecutor
from ..auth import AuthRecord from ..auth import AuthRecord
from ..models import ConnectionConfig from ..models import ConnectionConfig
from ..runtime import ToolExecutor
from ..sdk import BackendAdapter
from ..shared import root_exception from ..shared import root_exception
from ..workflow import wrap_discovered_tool from ..workflow import wrap_discovered_tool
from .events import McpEvent from .events import McpEvent
+2 -1
View File
@@ -2,8 +2,9 @@ from __future__ import annotations
from collections.abc import Mapping from collections.abc import Mapping
from wf_sources_mcp.sdk import BackendAdapter
from ...models import ConnectionConfig from ...models import ConnectionConfig
from ...sdk import BackendAdapter
def require_adapter( def require_adapter(
+1 -2
View File
@@ -26,6 +26,7 @@ from wf_sources_mcp.catalog import (
CatalogPromptEntry, CatalogPromptEntry,
CatalogResourceEntry, CatalogResourceEntry,
) )
from wf_sources_mcp.sdk import BackendAdapter, ToolExecutor
from wf_sources_mcp.source_registry import SourceRegistryStore from wf_sources_mcp.source_registry import SourceRegistryStore
from wf_sources_mcp.storage import AuthStore, CatalogStore, Store from wf_sources_mcp.storage import AuthStore, CatalogStore, Store
@@ -36,8 +37,6 @@ from ...models import (
CatalogSnapshot, CatalogSnapshot,
ConnectionConfig, ConnectionConfig,
) )
from ...runtime import ToolExecutor
from ...sdk import BackendAdapter
from ..admin_capabilities import admin_source from ..admin_capabilities import admin_source
from ..catalog import CombinedCatalog from ..catalog import CombinedCatalog
from .builtins import builtin_sources from .builtins import builtin_sources
+1 -1
View File
@@ -23,6 +23,7 @@ from wf_sources_mcp.catalog import (
CatalogPromptEntry, CatalogPromptEntry,
CatalogResourceEntry, CatalogResourceEntry,
) )
from wf_sources_mcp.sdk import ToolExecutor
from wf_sources_mcp.storage import CatalogStore from wf_sources_mcp.storage import CatalogStore
from ...connections import ConnectionConfig, qualify_node_name from ...connections import ConnectionConfig, qualify_node_name
@@ -30,7 +31,6 @@ from ...events import McpEvent, make_event
from ...models import ( from ...models import (
CatalogSnapshot, CatalogSnapshot,
) )
from ...runtime import ToolExecutor
from ...workflow.wrappers import _model_from_schema from ...workflow.wrappers import _model_from_schema
from ..catalog import CombinedCatalog, snapshot_from_specs from ..catalog import CombinedCatalog, snapshot_from_specs
from .specs import get_qualified_spec, qualify_spec from .specs import get_qualified_spec, qualify_spec
@@ -24,11 +24,10 @@ from wf_mcp.broker.discovery import (
) )
from wf_mcp.events import McpEvent, make_event from wf_mcp.events import McpEvent, make_event
from wf_mcp.models import ConnectionConfig from wf_mcp.models import ConnectionConfig
from wf_mcp.runtime import ToolExecutor
from wf_mcp.sdk import BackendAdapter
from wf_mcp.shared.errors import error_payload from wf_mcp.shared.errors import error_payload
from wf_sources_mcp.auth import AuthRecord, connection_auth_diagnostic from wf_sources_mcp.auth import AuthRecord, connection_auth_diagnostic
from wf_sources_mcp.catalog.models import CatalogSnapshot from wf_sources_mcp.catalog.models import CatalogSnapshot
from wf_sources_mcp.sdk import BackendAdapter, ToolExecutor
from wf_sources_mcp.storage import AuthStore, CatalogStore from wf_sources_mcp.storage import AuthStore, CatalogStore
from .adapters import require_adapter from .adapters import require_adapter
+2 -1
View File
@@ -6,9 +6,10 @@ from dataclasses import asdict, dataclass, field
from inspect import isawaitable from inspect import isawaitable
from typing import Any, cast from typing import Any, cast
from wf_sources_mcp.sdk import ToolCallResult
from ..auth import AuthRecord from ..auth import AuthRecord
from ..models import ConnectionConfig from ..models import ConnectionConfig
from ..sdk import ToolCallResult
from .session import PersistentMcpSession from .session import PersistentMcpSession
SessionFactory = Callable[ SessionFactory = Callable[
+9 -21
View File
@@ -1,24 +1,12 @@
"""Compatibility shim for MCP runtime execution protocol.
Canonical implementation lives in `wf_sources_mcp.sdk`.
"""
from __future__ import annotations from __future__ import annotations
from typing import Any, Protocol from wf_sources_mcp.sdk import ToolExecutor
from ..auth import AuthRecord __all__ = [
from ..models import ConnectionConfig "ToolExecutor",
from ..sdk import 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 a future persistent runtime pool can replace the current
adapter without changing generated NodeSpecs.
"""
async def call_tool(
self,
connection: ConnectionConfig,
auth: AuthRecord | None,
tool_name: str,
payload: dict[str, Any],
) -> ToolCallResult: ...
+2 -1
View File
@@ -7,9 +7,10 @@ from typing import Any
from mcp.client.session import ClientSession from mcp.client.session import ClientSession
from mcp.types import CallToolResult from mcp.types import CallToolResult
from wf_sources_mcp.sdk import ToolCallResult
from ..auth import AuthRecord from ..auth import AuthRecord
from ..models import ConnectionConfig from ..models import ConnectionConfig
from ..sdk import ToolCallResult
from ..sdk.converters import tool_result_to_call_result from ..sdk.converters import tool_result_to_call_result
RawToolCaller = Callable[[str, dict[str, Any]], Awaitable[CallToolResult]] RawToolCaller = Callable[[str, dict[str, Any]], Awaitable[CallToolResult]]
+2 -1
View File
@@ -1,4 +1,5 @@
from wf_sources_mcp.sdk import BackendAdapter, ToolCallResult
from .adapter import McpSdkAdapter from .adapter import McpSdkAdapter
from .base import BackendAdapter, ToolCallResult
__all__ = ["BackendAdapter", "McpSdkAdapter", "ToolCallResult"] __all__ = ["BackendAdapter", "McpSdkAdapter", "ToolCallResult"]
+1 -1
View File
@@ -19,9 +19,9 @@ from pydantic import AnyUrl
from wf_sources_mcp.auth import AuthRecord, mcp_auth_env, mcp_auth_headers from wf_sources_mcp.auth import AuthRecord, mcp_auth_env, mcp_auth_headers
from wf_sources_mcp.catalog import DiscoveredPrompt, DiscoveredResource, DiscoveredTool from wf_sources_mcp.catalog import DiscoveredPrompt, DiscoveredResource, DiscoveredTool
from wf_sources_mcp.sdk import BackendAdapter, ToolCallResult
from ..models import ConnectionConfig from ..models import ConnectionConfig
from .base import BackendAdapter, ToolCallResult
from .converters import ( from .converters import (
prompt_to_discovered, prompt_to_discovered,
resource_to_discovered, resource_to_discovered,
+10 -78
View File
@@ -1,81 +1,13 @@
"""Compatibility shim for MCP upstream SDK protocol/result types.
Canonical implementation lives in `wf_sources_mcp.sdk`.
"""
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass, field from wf_sources_mcp.sdk import BackendAdapter, ToolCallResult
from typing import Any, Protocol
from wf_sources_mcp.catalog import DiscoveredPrompt, DiscoveredResource, DiscoveredTool __all__ = [
"BackendAdapter",
from ..auth import AuthRecord "ToolCallResult",
from ..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: ...
+1 -2
View File
@@ -8,8 +8,7 @@ from mcp.types import Resource as McpResource
from mcp.types import Tool as McpTool from mcp.types import Tool as McpTool
from wf_sources_mcp.catalog import DiscoveredPrompt, DiscoveredResource, DiscoveredTool from wf_sources_mcp.catalog import DiscoveredPrompt, DiscoveredResource, DiscoveredTool
from wf_sources_mcp.sdk import ToolCallResult
from .base import ToolCallResult
def tool_to_discovered(tool: McpTool) -> DiscoveredTool: def tool_to_discovered(tool: McpTool) -> DiscoveredTool:
+1 -1
View File
@@ -10,10 +10,10 @@ from wf_authoring import NodeReturn, NodeSpec
from wf_core import RuntimeContext from wf_core import RuntimeContext
from wf_mcp.broker.events import McpEvent, make_event from wf_mcp.broker.events import McpEvent, make_event
from wf_sources_mcp.catalog import DiscoveredTool from wf_sources_mcp.catalog import DiscoveredTool
from wf_sources_mcp.sdk import ToolExecutor
from ..auth import AuthRecord from ..auth import AuthRecord
from ..models import ConnectionConfig from ..models import ConnectionConfig
from ..runtime import ToolExecutor
_JSON_TYPE_MAP: dict[str, object] = { _JSON_TYPE_MAP: dict[str, object] = {
"string": str, "string": str,
+9
View File
@@ -0,0 +1,9 @@
from __future__ import annotations
from .protocols import BackendAdapter, ToolCallResult, ToolExecutor
__all__ = [
"BackendAdapter",
"ToolCallResult",
"ToolExecutor",
]
+112
View File
@@ -0,0 +1,112 @@
"""Protocol/result contracts for MCP upstream source providers.
The temporary `wf_mcp.broker.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.broker.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",
]
+22
View File
@@ -102,3 +102,25 @@ def test_wf_mcp_catalog_models_shim_reexports_wf_sources_mcp_catalog_models() ->
assert CompatCatalogSnapshot is CatalogSnapshot assert CompatCatalogSnapshot is CatalogSnapshot
assert compat_dump is dump_catalog_snapshot assert compat_dump is dump_catalog_snapshot
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
@@ -72,3 +72,31 @@ def test_wf_sources_mcp_does_not_import_wf_mcp_catalog_dtos() -> None:
"wf_sources_mcp still imports old wf_mcp catalog DTO modules:\n" "wf_sources_mcp still imports old wf_mcp catalog DTO modules:\n"
+ "\n".join(f" {violation}" for violation in violations) + "\n".join(f" {violation}" for violation in violations)
) )
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)
)
@@ -0,0 +1,66 @@
from __future__ import annotations
from dataclasses import is_dataclass
from typing import cast
from wf_mcp.broker.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"}}