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 ..capabilities import DiscoveredPrompt, DiscoveredResource, DiscoveredTool
|
||||||
from ..models import AuthRecord, ConnectionConfig
|
from ..models import AuthRecord, ConnectionConfig
|
||||||
|
from ..runtime import ToolExecutor
|
||||||
from ..sdk import BackendAdapter
|
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
|
||||||
@@ -72,7 +73,7 @@ def specs_from_discovered_tools(
|
|||||||
*,
|
*,
|
||||||
connection: ConnectionConfig,
|
connection: ConnectionConfig,
|
||||||
auth: AuthRecord | None,
|
auth: AuthRecord | None,
|
||||||
adapter: BackendAdapter,
|
executor: ToolExecutor,
|
||||||
tools: list[DiscoveredTool],
|
tools: list[DiscoveredTool],
|
||||||
emit_event: Callable[[McpEvent], None] | None = None,
|
emit_event: Callable[[McpEvent], None] | None = None,
|
||||||
) -> list[NodeSpec[Any, Any]]:
|
) -> list[NodeSpec[Any, Any]]:
|
||||||
@@ -80,7 +81,7 @@ def specs_from_discovered_tools(
|
|||||||
wrap_discovered_tool(
|
wrap_discovered_tool(
|
||||||
connection=connection,
|
connection=connection,
|
||||||
auth=auth,
|
auth=auth,
|
||||||
adapter=adapter,
|
executor=executor,
|
||||||
tool=tool,
|
tool=tool,
|
||||||
emit_event=emit_event,
|
emit_event=emit_event,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -606,7 +606,7 @@ class WfMcpService:
|
|||||||
specs = specs_from_discovered_tools(
|
specs = specs_from_discovered_tools(
|
||||||
connection=connection,
|
connection=connection,
|
||||||
auth=auth,
|
auth=auth,
|
||||||
adapter=adapter,
|
executor=adapter,
|
||||||
tools=capabilities.tools,
|
tools=capabilities.tools,
|
||||||
emit_event=self._record_event,
|
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 ..capabilities import DiscoveredTool
|
||||||
from ..models import AuthRecord, ConnectionConfig
|
from ..models import AuthRecord, ConnectionConfig
|
||||||
from ..sdk import BackendAdapter
|
from ..runtime import ToolExecutor
|
||||||
|
|
||||||
|
|
||||||
_JSON_TYPE_MAP: dict[str, object] = {
|
_JSON_TYPE_MAP: dict[str, object] = {
|
||||||
@@ -103,7 +103,7 @@ def wrap_discovered_tool(
|
|||||||
*,
|
*,
|
||||||
connection: ConnectionConfig,
|
connection: ConnectionConfig,
|
||||||
auth: AuthRecord | None,
|
auth: AuthRecord | None,
|
||||||
adapter: BackendAdapter,
|
executor: ToolExecutor,
|
||||||
tool: DiscoveredTool,
|
tool: DiscoveredTool,
|
||||||
emit_event: Callable[[McpEvent], None] | None = None,
|
emit_event: Callable[[McpEvent], None] | None = None,
|
||||||
) -> NodeSpec[BaseModel, BaseModel]:
|
) -> NodeSpec[BaseModel, BaseModel]:
|
||||||
@@ -129,7 +129,7 @@ def wrap_discovered_tool(
|
|||||||
payload={"input": payload.model_dump(exclude_unset=True)},
|
payload={"input": payload.model_dump(exclude_unset=True)},
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
result = await adapter.call_tool(
|
result = await executor.call_tool(
|
||||||
connection=connection,
|
connection=connection,
|
||||||
auth=auth,
|
auth=auth,
|
||||||
tool_name=tool.name,
|
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_core import RuntimeContext
|
||||||
from wf_mcp.capabilities import DiscoveredTool
|
from wf_mcp.capabilities import DiscoveredTool
|
||||||
from wf_mcp.models import AuthRecord, ConnectionConfig
|
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
|
from wf_mcp.workflow import wrap_discovered_tool
|
||||||
|
|
||||||
|
|
||||||
@@ -37,7 +38,7 @@ def test_discovered_tool_wrapper_omits_unset_optional_arguments() -> None:
|
|||||||
account="default",
|
account="default",
|
||||||
),
|
),
|
||||||
auth=None,
|
auth=None,
|
||||||
adapter=cast(BackendAdapter, adapter),
|
executor=cast(ToolExecutor, adapter),
|
||||||
tool=DiscoveredTool(
|
tool=DiscoveredTool(
|
||||||
name="browser_snapshot",
|
name="browser_snapshot",
|
||||||
title=None,
|
title=None,
|
||||||
|
|||||||
Reference in New Issue
Block a user