example use, live MCP

told chat i dont want to use the fake ahh server we have (FakeAdapter). switch over, boom, errors.
This commit is contained in:
lda
2026-05-09 01:01:05 +07:00 Verified
parent 4d8e1e29ee
commit 47b337a9c3
6 changed files with 141 additions and 10 deletions
+82
View File
@@ -0,0 +1,82 @@
from __future__ import annotations
import asyncio
from pathlib import Path
import sys
from wf_core import END
from wf_mcp.broker import WfMcpService
from wf_mcp.models import ConnectionConfig, RawWorkflowPlan
from wf_mcp.sdk import McpSdkAdapter
from wf_mcp.storage import FileStore
FIXTURE_SERVER = (
Path(__file__).resolve().parents[1] / "tests" / "fixtures" / "mcp_echo_server.py"
)
async def run_example() -> dict[str, object]:
"""Discover an MCP tool, compile it into a workflow, and execute it."""
service = WfMcpService(store=FileStore(Path(".wf_mcp_store") / "example"))
service.register_connection(
ConnectionConfig(
id="fixture.personal",
server="fixture",
account="personal",
metadata={
"transport": "stdio",
"command": sys.executable,
"args": [str(FIXTURE_SERVER)],
},
)
)
service.register_adapter("fixture", McpSdkAdapter())
await service.refresh_connection_catalog("fixture.personal")
plan = RawWorkflowPlan(
name="mcp_echo_workflow",
input_schema={
"type": "object",
"properties": {"text": {"type": "string"}},
"required": ["text"],
},
state_schema={"fields": {"echoed": {"type": "string"}}},
output_schema={
"type": "object",
"properties": {"echoed": {"type": "string"}},
"required": ["echoed"],
},
start="echo",
nodes=[
{
"id": "echo",
"type": "node",
"node": "fixture.personal.echo_tool",
"in_map": {"input.text": "text"},
"out_map": {"echoed": "state.echoed"},
}
],
edges=[
{"from": "echo", "outcome": "ok", "to": END},
{"from": "echo", "outcome": "error", "to": END}, # workaround for now; i need wf_authoring.ops::runtime_error
],
)
run = await service.run_workflow_from_plan(plan, {"text": "hello from MCP"})
return {
"status": run.status.value,
"output": run.output,
"catalog": service.get_catalog().as_payload(),
}
def main() -> None:
"""Run the MCP tool workflow example and print the result."""
import json
print(json.dumps(asyncio.run(run_example()), indent=2))
if __name__ == "__main__":
main()
+2 -4
View File
@@ -177,11 +177,9 @@ def test_pydantic_field_descriptions_survive_node_catalog_schema() -> None:
assert entry.description == "Echoes a documented message." assert entry.description == "Echoes a documented message."
assert entry.input_schema["type"] == "object" assert entry.input_schema["type"] == "object"
assert ( assert (
entry.input_schema["properties"]["message"]["description"] entry.input_schema["properties"]["message"]["description"] == "Message to echo"
== "Message to echo"
) )
assert entry.output_schema["type"] == "object" assert entry.output_schema["type"] == "object"
assert ( assert (
entry.output_schema["properties"]["echoed"]["description"] entry.output_schema["properties"]["echoed"]["description"] == "Echoed message"
== "Echoed message"
) )
+8 -1
View File
@@ -1,13 +1,20 @@
from __future__ import annotations from __future__ import annotations
from typing import Annotated, TypedDict
from mcp.server.fastmcp import FastMCP from mcp.server.fastmcp import FastMCP
from pydantic import Field
server = FastMCP("echo-fixture") server = FastMCP("echo-fixture")
class EchoToolResult(TypedDict):
echoed: str
@server.tool(title="Echo tool") @server.tool(title="Echo tool")
async def echo_tool(text: str) -> dict[str, str]: async def echo_tool(
text: Annotated[str, Field(description="Text to echo")],
) -> EchoToolResult:
return {"echoed": text} return {"echoed": text}
+23
View File
@@ -47,6 +47,29 @@ class Context(TypedDict): # dataclass support? no. i mean langgraph doesnt.
# class Input(TypedDict, total=False): # class Input(TypedDict, total=False):
## context is a hard thing
# According to https://docs.langchain.com/oss/python/concepts/context, there are three types:
#
# | type | mut | lifetime |
# | --- | --- | --- |
# |static runtime (context) | static | single run |
# |dynamic runtime (state) | mut | single run |
# |dynamic cross-convo (store) | mut | cross-conversation |
#
# now what the hell is store
### store
#
# store is used in langgraph-demo for debugging. but it can be used for more things.
# it saves every turn. every graph nodes. I use InMemoryStore, you can use psql store!
# This allows for picking the work up again after a while for example.
# a lot more versatility there.
#
### what about us? how should we handle context?
#
# in the future if wed like, we could handle context. This could be useful for lda.chat!
# i want lda.chat to be/have a meta-agent. So i could spin up ideas! a
class ContextInput(BaseModel): class ContextInput(BaseModel):
context: Context # final! context: Context # final!
@@ -0,0 +1,22 @@
from __future__ import annotations
import asyncio
from typing import Any, cast
import pytest
from examples.mcp_tool_workflow import run_example
def test_mcp_tool_workflow_example_runs_discovered_tool() -> None:
try:
result = asyncio.run(run_example())
except PermissionError as exc:
pytest.skip(f"stdio MCP transport is not permitted in this environment: {exc}")
assert result["status"] == "completed"
assert result["output"] == {"echoed": "hello from MCP"}
catalog = cast(dict[str, Any], result["catalog"])
node = catalog["nodes"][0]
assert node["qualified_name"] == "fixture.personal.echo_tool"
assert node["input_schema"]["properties"]["text"]["description"] == "Text to echo"
+4 -5
View File
@@ -196,10 +196,7 @@ def test_service_catalog_preserves_json_schema_description_metadata() -> None:
node = service.get_catalog().as_payload()["nodes"][0] node = service.get_catalog().as_payload()["nodes"][0]
assert node["input_schema"]["type"] == "object" assert node["input_schema"]["type"] == "object"
assert isinstance(node["input_schema"]["properties"], dict) assert isinstance(node["input_schema"]["properties"], dict)
assert ( assert node["input_schema"]["properties"]["text"]["description"] == "Text to echo"
node["input_schema"]["properties"]["text"]["description"]
== "Text to echo"
)
assert node["output_schema"]["type"] == "object" assert node["output_schema"]["type"] == "object"
assert isinstance(node["output_schema"]["properties"], dict) assert isinstance(node["output_schema"]["properties"], dict)
@@ -226,7 +223,9 @@ def test_service_wrapped_tool_adapter_model_validates_simple_schema_types() -> N
except ValueError as exc: except ValueError as exc:
assert "text" in str(exc) assert "text" in str(exc)
else: else:
raise AssertionError("expected generated adapter model to reject non-string text") raise AssertionError(
"expected generated adapter model to reject non-string text"
)
def test_service_records_tool_call_events() -> None: def test_service_records_tool_call_events() -> None: