Tests necessary for the runtime protocol
This commit is contained in:
@@ -11,6 +11,7 @@ from wf_authoring import NodeSpec
|
||||
|
||||
from ..capabilities import DiscoveredPrompt, DiscoveredResource, DiscoveredTool
|
||||
from ..models import AuthRecord, ConnectionConfig
|
||||
from ..runtime import ToolExecutor
|
||||
from ..sdk import BackendAdapter
|
||||
from ..shared import root_exception
|
||||
from ..workflow import wrap_discovered_tool
|
||||
@@ -72,7 +73,7 @@ def specs_from_discovered_tools(
|
||||
*,
|
||||
connection: ConnectionConfig,
|
||||
auth: AuthRecord | None,
|
||||
adapter: BackendAdapter,
|
||||
executor: ToolExecutor,
|
||||
tools: list[DiscoveredTool],
|
||||
emit_event: Callable[[McpEvent], None] | None = None,
|
||||
) -> list[NodeSpec[Any, Any]]:
|
||||
@@ -80,7 +81,7 @@ def specs_from_discovered_tools(
|
||||
wrap_discovered_tool(
|
||||
connection=connection,
|
||||
auth=auth,
|
||||
adapter=adapter,
|
||||
executor=executor,
|
||||
tool=tool,
|
||||
emit_event=emit_event,
|
||||
)
|
||||
|
||||
@@ -606,7 +606,7 @@ class WfMcpService:
|
||||
specs = specs_from_discovered_tools(
|
||||
connection=connection,
|
||||
auth=auth,
|
||||
adapter=adapter,
|
||||
executor=adapter,
|
||||
tools=capabilities.tools,
|
||||
emit_event=self._record_event,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
from .protocols import ToolExecutor
|
||||
|
||||
__all__ = ["ToolExecutor"]
|
||||
@@ -0,0 +1,23 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Protocol
|
||||
|
||||
from ..models import AuthRecord, 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: ...
|
||||
@@ -12,7 +12,7 @@ from wf_mcp.broker.events import McpEvent, make_event
|
||||
|
||||
from ..capabilities import DiscoveredTool
|
||||
from ..models import AuthRecord, ConnectionConfig
|
||||
from ..sdk import BackendAdapter
|
||||
from ..runtime import ToolExecutor
|
||||
|
||||
|
||||
_JSON_TYPE_MAP: dict[str, object] = {
|
||||
@@ -103,7 +103,7 @@ def wrap_discovered_tool(
|
||||
*,
|
||||
connection: ConnectionConfig,
|
||||
auth: AuthRecord | None,
|
||||
adapter: BackendAdapter,
|
||||
executor: ToolExecutor,
|
||||
tool: DiscoveredTool,
|
||||
emit_event: Callable[[McpEvent], None] | None = None,
|
||||
) -> NodeSpec[BaseModel, BaseModel]:
|
||||
@@ -129,7 +129,7 @@ def wrap_discovered_tool(
|
||||
payload={"input": payload.model_dump(exclude_unset=True)},
|
||||
)
|
||||
)
|
||||
result = await adapter.call_tool(
|
||||
result = await executor.call_tool(
|
||||
connection=connection,
|
||||
auth=auth,
|
||||
tool_name=tool.name,
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
from wf_authoring import build_async_registry
|
||||
from wf_core import RuntimeContext
|
||||
from wf_mcp.capabilities import DiscoveredTool
|
||||
from wf_mcp.models import AuthRecord, ConnectionConfig
|
||||
from wf_mcp.sdk import ToolCallResult
|
||||
from wf_mcp.workflow import wrap_discovered_tool
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class FakeStatefulExecutor:
|
||||
"""Executor fake that exposes why workflow calls need shared MCP runtime."""
|
||||
|
||||
page_open: bool = False
|
||||
calls: list[tuple[str, dict[str, Any]]] = field(default_factory=list)
|
||||
|
||||
async def call_tool(
|
||||
self,
|
||||
connection: ConnectionConfig,
|
||||
auth: AuthRecord | None,
|
||||
tool_name: str,
|
||||
payload: dict[str, Any],
|
||||
) -> ToolCallResult:
|
||||
self.calls.append((tool_name, payload))
|
||||
if tool_name == "browser_navigate":
|
||||
self.page_open = True
|
||||
return ToolCallResult(outcome="ok", output={"content": "opened"})
|
||||
if tool_name == "browser_snapshot":
|
||||
if not self.page_open:
|
||||
return ToolCallResult(outcome="error", output={"content": "no page"})
|
||||
return ToolCallResult(outcome="ok", output={"content": "snapshot"})
|
||||
raise KeyError(tool_name)
|
||||
|
||||
|
||||
def _tool(name: str) -> DiscoveredTool:
|
||||
return DiscoveredTool(
|
||||
name=name,
|
||||
title=None,
|
||||
description=None,
|
||||
input_schema={"type": "object", "properties": {}},
|
||||
output_schema={"type": "object", "properties": {}},
|
||||
outcomes=("ok", "error"),
|
||||
)
|
||||
|
||||
|
||||
def test_generated_workflow_specs_share_injected_tool_executor() -> None:
|
||||
"""Generated NodeSpecs use the injected executor, not a baked-in adapter."""
|
||||
|
||||
connection = ConnectionConfig(
|
||||
id="playwright.default",
|
||||
server="playwright",
|
||||
account="default",
|
||||
)
|
||||
executor = FakeStatefulExecutor()
|
||||
navigate = wrap_discovered_tool(
|
||||
connection=connection,
|
||||
auth=None,
|
||||
executor=executor,
|
||||
tool=_tool("browser_navigate"),
|
||||
)
|
||||
snapshot = wrap_discovered_tool(
|
||||
connection=connection,
|
||||
auth=None,
|
||||
executor=executor,
|
||||
tool=_tool("browser_snapshot"),
|
||||
)
|
||||
handlers = build_async_registry(navigate, snapshot)
|
||||
|
||||
async def run_workflow_calls() -> dict[str, Any]:
|
||||
await handlers["browser_navigate"](
|
||||
{},
|
||||
RuntimeContext(current_node_id="navigate"),
|
||||
)
|
||||
return await handlers["browser_snapshot"](
|
||||
{},
|
||||
RuntimeContext(current_node_id="snapshot"),
|
||||
)
|
||||
|
||||
result = asyncio.run(run_workflow_calls())
|
||||
|
||||
assert result["outcome"] == "ok"
|
||||
assert result["output"]["content"] == "snapshot"
|
||||
assert executor.calls == [("browser_navigate", {}), ("browser_snapshot", {})]
|
||||
@@ -7,7 +7,8 @@ from wf_authoring import build_async_registry
|
||||
from wf_core import RuntimeContext
|
||||
from wf_mcp.capabilities import DiscoveredTool
|
||||
from wf_mcp.models import AuthRecord, ConnectionConfig
|
||||
from wf_mcp.sdk import BackendAdapter, ToolCallResult
|
||||
from wf_mcp.runtime import ToolExecutor
|
||||
from wf_mcp.sdk import ToolCallResult
|
||||
from wf_mcp.workflow import wrap_discovered_tool
|
||||
|
||||
|
||||
@@ -37,7 +38,7 @@ def test_discovered_tool_wrapper_omits_unset_optional_arguments() -> None:
|
||||
account="default",
|
||||
),
|
||||
auth=None,
|
||||
adapter=cast(BackendAdapter, adapter),
|
||||
executor=cast(ToolExecutor, adapter),
|
||||
tool=DiscoveredTool(
|
||||
name="browser_snapshot",
|
||||
title=None,
|
||||
|
||||
Reference in New Issue
Block a user