add call_capability

This commit is contained in:
lda
2026-05-17 01:30:37 +07:00 Verified
parent 3437ec24e8
commit a4910ba46f
6 changed files with 54 additions and 4 deletions
+2 -2
View File
@@ -126,8 +126,8 @@ When the exposed tool catalog grows large or changes often, FastMCP's search
transform is a good mitigation: keep a stable pinned control/workflow spine transform is a good mitigation: keep a stable pinned control/workflow spine
visible, and use `search_tools` plus its synthetic `call_tool` for the changing visible, and use `search_tools` plus its synthetic `call_tool` for the changing
rest of the catalog. Do not confuse that synthetic raw-tool caller with a rest of the catalog. Do not confuse that synthetic raw-tool caller with a
future workflow-capability test tool; testing a normalized `NodeSpec` contract workflow-capability test tool; `wf.workflow.call_capability` tests a normalized
is a separate operation and should also remain pinned once it exists. `NodeSpec` contract and remains pinned for that reason.
## What Is Probably Not Worth Owning Yet ## What Is Probably Not Worth Owning Yet
+2 -2
View File
@@ -266,11 +266,11 @@ Today:
- `wf.mcp` owns workflow-facing MCP runtime helpers - `wf.mcp` owns workflow-facing MCP runtime helpers
- discovered upstream tools can already become workflow node specs - discovered upstream tools can already become workflow node specs
- saved artifacts can be tagged with `kind="workflow"` or `kind="wrapper"` - saved artifacts can be tagged with `kind="workflow"` or `kind="wrapper"`
- `wf.workflow.call_capability` can execute one planner-visible workflow
capability directly and return normalized `outcome` / `output`
Not yet implemented: Not yet implemented:
- a direct public tool for calling arbitrary workflow node specs for authoring
tests
- per-outcome output schemas - per-outcome output schemas
- graph-as-node for saved workflows - graph-as-node for saved workflows
+1
View File
@@ -35,6 +35,7 @@ _SEARCH_ALWAYS_VISIBLE_TOOL_NAMES = [
# Stable workflow control surface. Keep future workflow-capability test # Stable workflow control surface. Keep future workflow-capability test
# tools pinned here too; they are distinct from raw MCP tool execution. # tools pinned here too; they are distinct from raw MCP tool execution.
"wf.workflow.list_artifacts", "wf.workflow.list_artifacts",
"wf.workflow.call_capability",
"wf.workflow.inspect_artifact", "wf.workflow.inspect_artifact",
"wf.workflow.list_deployments", "wf.workflow.list_deployments",
"wf.workflow.validate_deployment", "wf.workflow.validate_deployment",
+21
View File
@@ -14,6 +14,8 @@ from wf_artifacts import (
create_workflow_artifact_from_plan as build_workflow_artifact_from_plan, create_workflow_artifact_from_plan as build_workflow_artifact_from_plan,
validate_deployment_dependencies, validate_deployment_dependencies,
) )
from wf_authoring import build_async_registry
from wf_core import RuntimeContext
from ..events import make_event from ..events import make_event
from ..models import RawWorkflowPlan from ..models import RawWorkflowPlan
@@ -39,6 +41,25 @@ class WorkflowSurfaceHandlers:
] ]
return {"nodes": entries} return {"nodes": entries}
async def call_capability(
self,
*,
qualified_name: str,
payload: dict[str, Any],
) -> dict[str, Any]:
"""Execute one planner-visible workflow capability for authoring tests."""
spec = self.service._get_qualified_spec(qualified_name)
handler = build_async_registry(spec)[spec.name]
result = await handler(
payload,
RuntimeContext(current_node_id=spec.name),
)
return {
"qualified_name": spec.name,
"outcome": result["outcome"],
"output": result["output"],
}
async def save_artifact(self, artifact: dict[str, Any]) -> dict[str, Any]: async def save_artifact(self, artifact: dict[str, Any]) -> dict[str, Any]:
if self.service.artifact_store is None: if self.service.artifact_store is None:
raise KeyError("workflow artifact store is not configured") raise KeyError("workflow artifact store is not configured")
+17
View File
@@ -21,6 +21,23 @@ def register_workflow_tools(server: FastMCP[Any], service: WfMcpService) -> None
async def list_artifacts() -> dict[str, Any]: async def list_artifacts() -> dict[str, Any]:
return await handlers.list_artifacts() return await handlers.list_artifacts()
@server.tool(
name="wf.workflow.call_capability",
title="Call Workflow Capability",
description=(
"Execute one planner-visible workflow capability once and return its "
"normalized outcome and output."
),
)
async def call_capability(
qualified_name: str,
payload: dict[str, Any],
) -> dict[str, Any]:
return await handlers.call_capability(
qualified_name=qualified_name,
payload=payload,
)
@server.tool( @server.tool(
name="wf.workflow.save_artifact", name="wf.workflow.save_artifact",
title="Save Workflow Artifact", title="Save Workflow Artifact",
+11
View File
@@ -51,6 +51,7 @@ def test_server_exposes_upstream_admin_and_workflow_tools() -> None:
assert "wf.admin.call_tool" in names assert "wf.admin.call_tool" in names
assert "wf.admin.get_events" in names assert "wf.admin.get_events" in names
assert "wf.workflow.list_artifacts" in names assert "wf.workflow.list_artifacts" in names
assert "wf.workflow.call_capability" in names
assert "wf.workflow.run_deployment" in names assert "wf.workflow.run_deployment" in names
echo_result = await client.call_tool( echo_result = await client.call_tool(
@@ -58,10 +59,19 @@ def test_server_exposes_upstream_admin_and_workflow_tools() -> None:
{"text": "hello"}, {"text": "hello"},
) )
artifacts_result = await client.call_tool("wf.workflow.list_artifacts") artifacts_result = await client.call_tool("wf.workflow.list_artifacts")
capability_result = await client.call_tool(
"wf.workflow.call_capability",
{
"qualified_name": "wf.std.constant",
"payload": {"value": "hello"},
},
)
sources_result = await client.call_tool("wf.admin.list_sources") sources_result = await client.call_tool("wf.admin.list_sources")
assert _structured(echo_result)["echoed"] == "hello" assert _structured(echo_result)["echoed"] == "hello"
assert _structured(artifacts_result)["nodes"] == [] assert _structured(artifacts_result)["nodes"] == []
assert _structured(capability_result)["outcome"] == "ok"
assert _structured(capability_result)["output"] == {"value": "hello"}
source_ids = { source_ids = {
source["id"] for source in _structured(sources_result)["result"] source["id"] for source in _structured(sources_result)["result"]
} }
@@ -133,6 +143,7 @@ def test_server_search_mode_pins_stable_control_and_workflow_tools() -> None:
assert "wf.admin.list_proxy_tools" in names assert "wf.admin.list_proxy_tools" in names
assert "wf.admin.get_proxy_tool" in names assert "wf.admin.get_proxy_tool" in names
assert "wf.workflow.list_artifacts" in names assert "wf.workflow.list_artifacts" in names
assert "wf.workflow.call_capability" in names
assert "wf.workflow.inspect_artifact" in names assert "wf.workflow.inspect_artifact" in names
assert "wf.workflow.list_deployments" in names assert "wf.workflow.list_deployments" in names
assert "wf.workflow.validate_deployment" in names assert "wf.workflow.validate_deployment" in names