python-sdk/examples/stories/refund_desk
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Marcelo Trylesinski f664db8952
Add resolver dependency injection for MCPServer tools (#2969)
Co-authored-by: Max Isbey <224885523+maxisbey@users.noreply.github.com>
2026-06-29 11:51:46 +01:00
..
__init__.py Add resolver dependency injection for MCPServer tools (#2969) 2026-06-29 11:51:46 +01:00
client.py Add resolver dependency injection for MCPServer tools (#2969) 2026-06-29 11:51:46 +01:00
README.md Add resolver dependency injection for MCPServer tools (#2969) 2026-06-29 11:51:46 +01:00
server.py Add resolver dependency injection for MCPServer tools (#2969) 2026-06-29 11:51:46 +01:00

refund-desk

Resolver dependency injection: a tool parameter annotated Annotated[T, Resolve(fn)] is filled by running the resolver fn before the tool body, instead of from the LLM-supplied arguments. Here refund_order(order_id, reason) refunds what the order record says — cents is resolver-computed and does not appear in the input schema at all, so the model cannot supply or inflate the amount. Resolvers form a DAG (load_order → refund_scope → refund_amount / ask_restock), may return Elicit[...] to ask the human, and run at most once per call. A resolver's own plain parameters are filled from the tool's arguments by name — load_order(order_id) receives the order_id the model passed to refund_order.

Run it

# stdio (default — the client spawns the server as a subprocess)
uv run python -m stories.refund_desk.client

# HTTP — the client self-hosts the server on a free port, runs, then tears it
# down (--legacy: resolver elicitation rides the push request today; the
# manifest pins this era, so bare --http runs the same leg)
uv run python -m stories.refund_desk.client --http --legacy

What to look at

  • server.py refund_order — the signature is the whole story: order_id and reason are model-facing; cents and restock carry Resolve(...) markers and never reach the input schema. client.py asserts properties and required are exactly {order_id, reason}.
  • server.py refund_scope — the no-round-trip fast path: a one-line order returns Scope(full=True) directly; only a multi-line order returns Elicit(...). The ORD-7001 call completes with zero elicitations.
  • server.py _scoped — the elicited SKU is human-typed free text; it is validated against the order (ToolError on a miss) before any amount is computed.
  • The decline contrast: refund_amount takes scope unwrapped, so declining the scope question aborts the whole cents chain with an error containing the framework's Resolver for parameter 'scope' could not resolve: elicitation was decline (the client sees it behind the usual Error executing tool refund_order: prefix); restock keeps the ElicitationResult union, so declining restock still refunds — just with restocked: false.
  • client.py — the scope counter proves memoization from outside: one call consumes refund_scope from two resolvers but the question fires once.

Caveats

  • Decline order. A declined unwrapped dependency aborts resolution in tool-signature order — cents resolves before restock, so ask_restock never runs. Don't rely on a later resolver's side effects after an earlier consumer can abort.
  • Memoization scope. Each resolver runs at most once per tools/call, keyed by function identity; nothing is cached across calls or connections.
  • Validate elicited values. Elicited answers are human-typed; check them against your records (as _scoped does) before acting on them.

Spec

Elicitation — client features

See also

legacy_elicitation/ (the push mechanism resolver elicitation rides on today), mrtr/ (the 2026 input_required carrier; resolver DI will ride it once the SDK wires them together).