refactor: move mcp sdk protocols to wf_sources_mcp
This commit is contained in:
@@ -283,9 +283,13 @@ implementation state.
|
||||
models, file store, and conversion helpers now live in
|
||||
`wf_sources_mcp.source_registry`, with `wf_mcp.source_registry` retained
|
||||
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
|
||||
`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
|
||||
preferred durable workflow server. New product paths should target
|
||||
`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.
|
||||
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.
|
||||
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
|
||||
does not depend on `wf_mcp.workflow_surface`, `wf_mcp.admin_surface`,
|
||||
|
||||
@@ -9,11 +9,10 @@ from mcp.types import METHOD_NOT_FOUND
|
||||
|
||||
from wf_authoring import NodeSpec
|
||||
from wf_sources_mcp.catalog import DiscoveredPrompt, DiscoveredResource, DiscoveredTool
|
||||
from wf_sources_mcp.sdk import BackendAdapter, ToolExecutor
|
||||
|
||||
from ..auth import AuthRecord
|
||||
from ..models import ConnectionConfig
|
||||
from ..runtime import ToolExecutor
|
||||
from ..sdk import BackendAdapter
|
||||
from ..shared import root_exception
|
||||
from ..workflow import wrap_discovered_tool
|
||||
from .events import McpEvent
|
||||
|
||||
@@ -2,8 +2,9 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
|
||||
from wf_sources_mcp.sdk import BackendAdapter
|
||||
|
||||
from ...models import ConnectionConfig
|
||||
from ...sdk import BackendAdapter
|
||||
|
||||
|
||||
def require_adapter(
|
||||
|
||||
@@ -26,6 +26,7 @@ from wf_sources_mcp.catalog import (
|
||||
CatalogPromptEntry,
|
||||
CatalogResourceEntry,
|
||||
)
|
||||
from wf_sources_mcp.sdk import BackendAdapter, ToolExecutor
|
||||
from wf_sources_mcp.source_registry import SourceRegistryStore
|
||||
from wf_sources_mcp.storage import AuthStore, CatalogStore, Store
|
||||
|
||||
@@ -36,8 +37,6 @@ from ...models import (
|
||||
CatalogSnapshot,
|
||||
ConnectionConfig,
|
||||
)
|
||||
from ...runtime import ToolExecutor
|
||||
from ...sdk import BackendAdapter
|
||||
from ..admin_capabilities import admin_source
|
||||
from ..catalog import CombinedCatalog
|
||||
from .builtins import builtin_sources
|
||||
|
||||
@@ -23,6 +23,7 @@ from wf_sources_mcp.catalog import (
|
||||
CatalogPromptEntry,
|
||||
CatalogResourceEntry,
|
||||
)
|
||||
from wf_sources_mcp.sdk import ToolExecutor
|
||||
from wf_sources_mcp.storage import CatalogStore
|
||||
|
||||
from ...connections import ConnectionConfig, qualify_node_name
|
||||
@@ -30,7 +31,6 @@ from ...events import McpEvent, make_event
|
||||
from ...models import (
|
||||
CatalogSnapshot,
|
||||
)
|
||||
from ...runtime import ToolExecutor
|
||||
from ...workflow.wrappers import _model_from_schema
|
||||
from ..catalog import CombinedCatalog, snapshot_from_specs
|
||||
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.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_sources_mcp.auth import AuthRecord, connection_auth_diagnostic
|
||||
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 .adapters import require_adapter
|
||||
|
||||
@@ -6,9 +6,10 @@ from dataclasses import asdict, dataclass, field
|
||||
from inspect import isawaitable
|
||||
from typing import Any, cast
|
||||
|
||||
from wf_sources_mcp.sdk import ToolCallResult
|
||||
|
||||
from ..auth import AuthRecord
|
||||
from ..models import ConnectionConfig
|
||||
from ..sdk import ToolCallResult
|
||||
from .session import PersistentMcpSession
|
||||
|
||||
SessionFactory = Callable[
|
||||
|
||||
@@ -1,24 +1,12 @@
|
||||
"""Compatibility shim for MCP runtime execution protocol.
|
||||
|
||||
Canonical implementation lives in `wf_sources_mcp.sdk`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Protocol
|
||||
from wf_sources_mcp.sdk import ToolExecutor
|
||||
|
||||
from ..auth import AuthRecord
|
||||
from ..models import ConnectionConfig
|
||||
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: ...
|
||||
__all__ = [
|
||||
"ToolExecutor",
|
||||
]
|
||||
|
||||
@@ -7,9 +7,10 @@ from typing import Any
|
||||
from mcp.client.session import ClientSession
|
||||
from mcp.types import CallToolResult
|
||||
|
||||
from wf_sources_mcp.sdk import ToolCallResult
|
||||
|
||||
from ..auth import AuthRecord
|
||||
from ..models import ConnectionConfig
|
||||
from ..sdk import ToolCallResult
|
||||
from ..sdk.converters import tool_result_to_call_result
|
||||
|
||||
RawToolCaller = Callable[[str, dict[str, Any]], Awaitable[CallToolResult]]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from wf_sources_mcp.sdk import BackendAdapter, ToolCallResult
|
||||
|
||||
from .adapter import McpSdkAdapter
|
||||
from .base import BackendAdapter, ToolCallResult
|
||||
|
||||
__all__ = ["BackendAdapter", "McpSdkAdapter", "ToolCallResult"]
|
||||
|
||||
@@ -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.catalog import DiscoveredPrompt, DiscoveredResource, DiscoveredTool
|
||||
from wf_sources_mcp.sdk import BackendAdapter, ToolCallResult
|
||||
|
||||
from ..models import ConnectionConfig
|
||||
from .base import BackendAdapter, ToolCallResult
|
||||
from .converters import (
|
||||
prompt_to_discovered,
|
||||
resource_to_discovered,
|
||||
|
||||
+10
-78
@@ -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 dataclasses import dataclass, field
|
||||
from typing import Any, Protocol
|
||||
from wf_sources_mcp.sdk import BackendAdapter, ToolCallResult
|
||||
|
||||
from wf_sources_mcp.catalog import DiscoveredPrompt, DiscoveredResource, DiscoveredTool
|
||||
|
||||
from ..auth import AuthRecord
|
||||
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: ...
|
||||
__all__ = [
|
||||
"BackendAdapter",
|
||||
"ToolCallResult",
|
||||
]
|
||||
|
||||
@@ -8,8 +8,7 @@ from mcp.types import Resource as McpResource
|
||||
from mcp.types import Tool as McpTool
|
||||
|
||||
from wf_sources_mcp.catalog import DiscoveredPrompt, DiscoveredResource, DiscoveredTool
|
||||
|
||||
from .base import ToolCallResult
|
||||
from wf_sources_mcp.sdk import ToolCallResult
|
||||
|
||||
|
||||
def tool_to_discovered(tool: McpTool) -> DiscoveredTool:
|
||||
|
||||
@@ -10,10 +10,10 @@ from wf_authoring import NodeReturn, NodeSpec
|
||||
from wf_core import RuntimeContext
|
||||
from wf_mcp.broker.events import McpEvent, make_event
|
||||
from wf_sources_mcp.catalog import DiscoveredTool
|
||||
from wf_sources_mcp.sdk import ToolExecutor
|
||||
|
||||
from ..auth import AuthRecord
|
||||
from ..models import ConnectionConfig
|
||||
from ..runtime import ToolExecutor
|
||||
|
||||
_JSON_TYPE_MAP: dict[str, object] = {
|
||||
"string": str,
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .protocols import BackendAdapter, ToolCallResult, ToolExecutor
|
||||
|
||||
__all__ = [
|
||||
"BackendAdapter",
|
||||
"ToolCallResult",
|
||||
"ToolExecutor",
|
||||
]
|
||||
@@ -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",
|
||||
]
|
||||
@@ -102,3 +102,25 @@ def test_wf_mcp_catalog_models_shim_reexports_wf_sources_mcp_catalog_models() ->
|
||||
|
||||
assert CompatCatalogSnapshot is CatalogSnapshot
|
||||
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"
|
||||
+ "\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"}}
|
||||
Reference in New Issue
Block a user