4. Look up an account and draft a reply¶
Use a direct mcp step when policy already knows which tool to call. Here
extraction supplies an account reference, a trusted guard checks that one is
present, and a local read-only MCP server returns synthetic account facts.
First, add the mcp.account_records transport and reviewed lookup_account
catalog to my_support/config/settings.yaml. Copy the complete
mcp block because
its input and output schemas must match the server's discovered schemas
exactly. Set supports_tools: true only if you will also try chapter 5; the
direct call itself does not ask the model to choose a tool. Create
my_support/server.py from the small synthetic server,
then replace the copied transport with:
transport:
type: stdio
command: $MCP_PYTHON
args:
- -m
- my_support.server
cwd: $PROJECT_ROOT
env:
PYTHONUNBUFFERED: "1"
The host below supplies the Python executable and repository root. Keep the
catalog from the example unchanged: its schemas describe this server's exact
signature. The server exposes one operation, lookup_account, with effect: read.
Add these steps to each branch flow.yaml after extract:
steps:
- extract
- require_reference
- lookup
- draft
- assemble
output:
pointer: /steps/assemble/result
optional: true
default: null
Create require_reference.step.yaml in each branch:
type: handler
handler: require_reference
input:
account_reference:
pointer: /steps/extract/result/account_reference
Register require_reference in your host with frozen input/output schemas;
the example handler returns
needs_review=True when the extracted reference is null. It projects a
non-null string only for a usable reference. prepare_application receives
that registration before compilation. This guard is a business policy check,
not proof that the model extracted the correct customer's reference.
Create lookup.step.yaml in each branch:
type: mcp
server: account_records
tool: lookup_account
arguments:
account_reference:
pointer: /steps/require_reference/result/account_reference
The runtime validates both the argument and tool result against the reviewed
catalog. The model cannot substitute another tool. An empty reference stops
before lookup; the synthetic gold checks lookup.status: skipped in that case.
Next create draft.step.md and draft.schema.json beside it, using the
billing draft
as the concrete shape. Bind only /payload/message and the validated
/steps/lookup/result; require a JSON object with one reply string. Tell the
model to acknowledge the request without claiming a refund or cancellation
has happened.
The final snapshot adds assemble.step.yaml to pair the draft with the trusted
route and guarded reference, and routes either branch to finalize. Its
select_reply handler
projects {queue, account_reference, reply} to the workflow payload. On a
review route, the configured output default is {disposition: needs_review}.
Use the final workflow
and finalize flow
for exact bindings. This last projection makes the host's result useful
without parsing internal step records.
Open your application with its handlers¶
Put the handler implementations and registrations
in my_support/handlers.py. Create my_support/run.py to connect your
configuration, handlers, and client lifecycle:
import asyncio
import os
import sys
from pathlib import Path
from foliqant import Envelope, open_application, prepare_application
from my_support.handlers import HANDLERS
async def main() -> None:
root = Path(__file__).resolve().parent.parent
prepared = prepare_application(root / "my_support/config/settings.yaml", handlers=HANDLERS)
environment = {
**os.environ,
"MCP_PYTHON": sys.executable,
"PROJECT_ROOT": str(root),
}
async with open_application(prepared, environment=environment) as app:
result = await app.run(
"support_email",
Envelope(
payload={
"message": "Please review the duplicate charge on invoice INV-7 for account A-100."
}
),
)
print(result.model_dump_json(indent=2))
asyncio.run(main())
After setting MODEL_ID and MODEL_BASE_URL in my_support/config/.env,
run uv run --no-sync python -m my_support.run from the repository root.
This calls your configured model and the local synthetic MCP server. Handler
registrations belong in Python; YAML never imports arbitrary application code.
For an offline comparison without a model endpoint, run the finished snapshot:
Expect payload.queue: billing, payload.account_reference: A-100, and a
reviewable payload.reply. Continue with a model tool loop.