chore: import upstream snapshot with attribution
Deploy Docs / deploy-docs (push) Failing after 1s
Conformance Tests / client-conformance (push) Failing after 3s
Conformance Tests / server-conformance (push) Failing after 1s
GitHub Actions Security Analysis / zizmor (push) Failing after 1s
CI / checks (push) Failing after 59m20s
CI / all-green (push) Waiting to run

This commit is contained in:
wehub-resource-sync
2026-07-13 12:10:27 +08:00
commit 49b9bb6724
992 changed files with 161690 additions and 0 deletions
+88
View File
@@ -0,0 +1,88 @@
# 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 ask each question 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
```bash
# 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 (2026 protocol: the questions ride embedded input_required round-trips;
# add --legacy to ride synchronous push elicitation instead)
uv run python -m stories.refund_desk.client --http
```
## 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}`. At 2026 the resolver's elicited
answers ride between rounds inside a `requestState` the SDK seals by default;
see `mrtr/` for the full security walk-through.
- `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
- **Transport per era.** The framework picks the elicitation transport from
the negotiated protocol: at >= 2026-07-28 the questions ride embedded
`input_required` round-trips (a resolver that depends on another's answer is
asked in a later round); at <= 2025-11-25 each is a synchronous
`elicitation/create` push request mid-call. Author code is identical on
both — this client runs unchanged on either era.
- **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 question is asked at most once per call, and
within a round each resolver runs at most once, keyed by function identity.
Across 2026 rounds only *elicited* outcomes persist (in `requestState`); any
resolver's body may run again on each round the call passes through. A
recorded answer is consulted only when the resolver asks its question again:
it satisfies the question without re-prompting the user, and it never stands
in for a value the resolver computes itself.
An answer is matched back to its question when the call resumes, so an
eliciting resolver must derive its question deterministically from the
tool's arguments and earlier answers; a per-call generated value (a
`default_factory` id, a timestamp) is re-derived each round and must not
appear in a question the answer is meant to bind to. 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](https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation),
[Input required tool results — server features](https://modelcontextprotocol.io/specification/draft/server/tools#input-required-tool-results)
## See also
`mrtr/` (the 2026 `input_required` carrier these questions ride at
>= 2026-07-28), `legacy_elicitation/` (the push mechanism they ride on
handshake-era connections).
+105
View File
@@ -0,0 +1,105 @@
"""Prove the refund amount is schema-hidden, resolvers memoize per call, and decline semantics differ per consumer."""
import mcp_types as types
from mcp.client import Client, ClientRequestContext
from stories._harness import Target, run_client
async def main(target: Target, *, mode: str = "auto") -> None:
# Scripted answers + per-topic counters; topics in `declines` are refused.
counts = {"scope": 0, "restock": 0}
answers: dict[str, dict[str, str | int | float | bool | list[str] | None]] = {
"scope": {"full": True},
"restock": {"restock": True},
}
declines: set[str] = set()
async def on_elicit(context: ClientRequestContext, params: types.ElicitRequestParams) -> types.ElicitResult:
assert isinstance(params, types.ElicitRequestFormParams)
topic = "scope" if "full" in params.requested_schema["properties"] else "restock"
counts[topic] += 1
if topic in declines:
return types.ElicitResult(action="decline")
return types.ElicitResult(action="accept", content=answers[topic])
async with Client(target, mode=mode, elicitation_callback=on_elicit) as client:
# The model-facing contract is order_id + reason only; cents and restock are resolver-filled.
listed = await client.list_tools()
(tool,) = listed.tools
assert set(tool.input_schema["properties"]) == {"order_id", "reason"}, tool.input_schema
assert set(tool.input_schema.get("required", ())) == {"order_id", "reason"}, tool.input_schema
# One digital line: scope auto-fills (full), restock auto-fills (False) — zero round-trips.
receipt = await client.call_tool("refund_order", {"order_id": "ORD-7001", "reason": "download corrupted"})
assert receipt.structured_content == {
"order_id": "ORD-7001",
"refunded_cents": 1500,
"restocked": False,
"reason": "download corrupted",
}, receipt.structured_content
assert counts == {"scope": 0, "restock": 0}, counts
# Full refund of a three-line order. The scope question fires exactly ONCE even though
# both refund_amount and ask_restock consume it — asked at most once per call on either
# era. ask_restock needs the scope ANSWER, so at 2026 the two questions land in
# successive rounds, never one concurrent batch: counts and order are era-independent.
receipt = await client.call_tool("refund_order", {"order_id": "ORD-7002", "reason": "arrived broken"})
assert receipt.structured_content == {
"order_id": "ORD-7002",
"refunded_cents": 4800,
"restocked": True,
"reason": "arrived broken",
}, receipt.structured_content
assert counts == {"scope": 1, "restock": 1}, counts
# Declining restock still refunds: the tool keeps the ElicitationResult union for
# `restock`, sees the decline, and just skips the restock. The scope counter moves
# again — questions are deduped per call, not per connection.
declines.add("restock")
answers["scope"] = {"full": False, "sku": "canvas-tote"}
receipt = await client.call_tool("refund_order", {"order_id": "ORD-7002", "reason": "wrong colour"})
assert receipt.structured_content == {
"order_id": "ORD-7002",
"refunded_cents": 2400,
"restocked": False,
"reason": "wrong colour",
}, receipt.structured_content
assert counts == {"scope": 2, "restock": 2}, counts
declines.clear()
# An elicited SKU is human-typed: the server validates it against the order before
# any money is computed.
answers["scope"] = {"full": False, "sku": "mystery-hat"}
result = await client.call_tool("refund_order", {"order_id": "ORD-7002", "reason": "lost parcel"})
assert result.is_error, result
assert isinstance(result.content[0], types.TextContent)
assert "order has no item 'mystery-hat'" in result.content[0].text, result.content[0].text
# Declining scope aborts the whole call: refund_amount and ask_restock both consume scope
# unwrapped, so whichever resolves first (`cents`, in signature order) aborts, and
# ask_restock never runs under any order.
declines.add("scope")
restock_before = counts["restock"]
result = await client.call_tool("refund_order", {"order_id": "ORD-7002", "reason": "changed mind"})
assert result.is_error, result
assert isinstance(result.content[0], types.TextContent)
assert "Resolver for parameter 'scope' could not resolve: elicitation was decline" in result.content[0].text, (
result.content[0].text
)
assert counts["restock"] == restock_before, counts
declines.clear()
# A ToolError raised inside a resolver surfaces exactly like one from the tool body.
result = await client.call_tool("refund_order", {"order_id": "ORD-9999", "reason": "typo"})
assert result.is_error, result
assert isinstance(result.content[0], types.TextContent)
assert "unknown order 'ORD-9999'" in result.content[0].text, result.content[0].text
# Full elicitation trajectory: scope fired in legs 2-5 (memoized within each call),
# restock only in the two calls that reached it.
assert counts == {"scope": 4, "restock": 2}, counts
if __name__ == "__main__":
run_client(main)
+127
View File
@@ -0,0 +1,127 @@
"""Resolver DI: the refund amount is computed by resolvers from the order record — `cents` never appears in the
tool's input schema, so the model cannot supply or inflate it."""
from dataclasses import dataclass
from typing import Annotated
from pydantic import BaseModel
from mcp.server.mcpserver import (
AcceptedElicitation,
Elicit,
ElicitationResult,
MCPServer,
Resolve,
)
from mcp.server.mcpserver.exceptions import ToolError
from stories._hosting import run_server_from_args
@dataclass(frozen=True)
class Line:
sku: str
cents: int
physical: bool
@dataclass(frozen=True)
class Order:
order_id: str
lines: tuple[Line, ...]
ORDERS: dict[str, Order] = {
"ORD-7001": Order("ORD-7001", (Line("ebook-fieldnotes", 1500, physical=False),)),
"ORD-7002": Order(
"ORD-7002",
(
Line("enamel-mug", 1800, physical=True),
Line("canvas-tote", 2400, physical=True),
Line("sticker-pack", 600, physical=False),
),
),
}
class Scope(BaseModel):
"""Which items to refund: the whole order, or a single SKU."""
full: bool
sku: str = ""
class RestockChoice(BaseModel):
restock: bool
class Receipt(BaseModel):
order_id: str
refunded_cents: int
restocked: bool
reason: str
def load_order(order_id: str) -> Order:
order = ORDERS.get(order_id)
if order is None:
raise ToolError(f"unknown order {order_id!r}")
return order
def refund_scope(order_id: str, order: Annotated[Order, Resolve(load_order)]) -> Scope | Elicit[Scope]:
if len(order.lines) == 1:
return Scope(full=True)
skus = ", ".join(line.sku for line in order.lines)
return Elicit(f"{order_id} has several items ({skus}). Refund the whole order, or one SKU?", Scope)
def _scoped(order: Order, scope: Scope) -> tuple[Line, ...]:
"""The lines a scope covers. The SKU was typed by a human — validate it against the order."""
if scope.full:
return order.lines
lines = tuple(line for line in order.lines if line.sku == scope.sku)
if not lines:
raise ToolError(f"order has no item {scope.sku!r}")
return lines
def refund_amount(
order: Annotated[Order, Resolve(load_order)],
scope: Annotated[Scope, Resolve(refund_scope)],
) -> int:
return sum(line.cents for line in _scoped(order, scope))
def ask_restock(
order: Annotated[Order, Resolve(load_order)],
scope: Annotated[Scope, Resolve(refund_scope)],
) -> RestockChoice | Elicit[RestockChoice]:
physical = [line.sku for line in _scoped(order, scope) if line.physical]
if not physical:
return RestockChoice(restock=False)
return Elicit(f"The refund includes physical items ({', '.join(physical)}). Return them to stock?", RestockChoice)
def build_server() -> MCPServer:
# Elicited answers ride between rounds in a requestState the SDK seals by default;
# see mrtr/ for the full security walk-through.
mcp = MCPServer("refund-desk")
@mcp.tool(description="Refund an order. The amount comes from the order record, not from the caller.")
def refund_order(
order_id: str,
reason: str,
cents: Annotated[int, Resolve(refund_amount)],
restock: Annotated[ElicitationResult[RestockChoice], Resolve(ask_restock)],
) -> Receipt:
# `restock` keeps the full elicitation outcome: a declined restock still refunds. A plain
# (non-Elicit) resolver return arrives wrapped as an accepted outcome, so the fast path
# lands in the same `AcceptedElicitation` branch.
restocked = isinstance(restock, AcceptedElicitation) and restock.data.restock
return Receipt(order_id=order_id, refunded_cents=cents, restocked=restocked, reason=reason)
return mcp
if __name__ == "__main__":
run_server_from_args(build_server)