Pass InputRequiredResult through the MCPServer prompt and resource pipelines (#3020)
This commit is contained in:
parent
8f2c97b769
commit
8d0f928e40
3 changed files with 627 additions and 32 deletions
|
|
@ -181,7 +181,7 @@ The handshake belongs to the runner. `server/discover`, `ping`, and every other
|
|||
|
||||
Each of these is one idea you now have the vocabulary for; each has its own chapter.
|
||||
|
||||
* `on_call_tool` may return an `InputRequiredResult` instead of a `CallToolResult` to pause the call and ask the client for input; see **[Multi-round-trip requests](multi-round-trip.md)**.
|
||||
* `on_call_tool`, `on_get_prompt`, and `on_read_resource` may return an `InputRequiredResult` instead of their normal result to pause the call and ask the client for input; see **[Multi-round-trip requests](multi-round-trip.md)**.
|
||||
* `on_list_resources`, `on_read_resource`, `on_list_prompts`, `on_get_prompt`, `on_completion` are the same `(ctx, params) -> result` shape for the other primitives.
|
||||
* `server.streamable_http_app()` returns the same Starlette app `MCPServer`'s does; deploy it the way **[Running your server](../run/index.md)** deploys any other ASGI app. There is no `server.run(transport=...)` down here: `server.run(read_stream, write_stream, server.create_initialization_options())` drives one connection over a pair of streams, and that one line is the whole story.
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,19 @@ On `@mcp.tool()` you rarely build this by hand: declare a dependency that asks t
|
|||
|
||||
Everything else in that file (the explicit `input_schema`, the hand-built `CallToolResult`) is the ordinary low-level `Server`, covered in **[The low-level Server](low-level-server.md)**. This page only adds the second return type.
|
||||
|
||||
## Beyond tools
|
||||
|
||||
`tools/call` is not special: at 2026-07-28 a server may answer `prompts/get` and `resources/read` the same way. On `MCPServer`, an `@mcp.prompt()` function — or an `@mcp.resource()` **template** function — returns the `InputRequiredResult` itself and reads the retry's answers off the context:
|
||||
|
||||
```python title="server.py" hl_lines="21 23 25"
|
||||
--8<-- "docs_src/mrtr/tutorial004.py"
|
||||
```
|
||||
|
||||
* The first round returns the `InputRequiredResult`. On the retry, `ctx.input_responses` holds the answers under the same keys and the function returns its ordinary result — prompt messages here, resource content for a template resource.
|
||||
* An `@mcp.tool()` function can return the result directly the same way, when the dependency form doesn't fit.
|
||||
* Static `@mcp.resource()` functions don't participate: they take no `Context`, so they could never read the retry. Only template resources can ask.
|
||||
* The era rules below apply unchanged: returning an `InputRequiredResult` on a pre-2026 session is the same `-32603` the warning describes.
|
||||
|
||||
## The client side
|
||||
|
||||
`Client` runs the loop for you.
|
||||
|
|
@ -94,5 +107,6 @@ Drop to the underlying session, where `allow_input_required=True` hands you the
|
|||
* `Client` runs the retry loop for you: register `elicitation_callback` / `sampling_callback` / `list_roots_callback` and `call_tool` returns a plain `CallToolResult`. `input_required_max_rounds` (default 10) bounds it.
|
||||
* To inspect or persist rounds, use `client.session.call_tool(..., allow_input_required=True)` and own the `while isinstance(result, InputRequiredResult)` loop yourself.
|
||||
* On `@mcp.tool()`, a dependency that asks the user produces this result for you (**[Dependencies](../tutorial/dependencies.md)**); the **low-level** `Server` is the manual form.
|
||||
* Prompts and resources participate too: an `@mcp.prompt()` or template `@mcp.resource()` function returns the `InputRequiredResult` itself and reads `ctx.input_responses` on the retry.
|
||||
|
||||
This is the mechanism that replaces server-initiated sampling and the rest of the push-style back-channel; see **[Deprecated features](deprecated.md)**.
|
||||
|
|
|
|||
|
|
@ -20,6 +20,26 @@ If you call `MCPServer.call_tool()` directly, read `.content` and
|
|||
`.structured_content` off the returned `CallToolResult` instead of branching on
|
||||
the result type.
|
||||
|
||||
### `MCPServer.get_prompt()` and `read_resource()` may return `InputRequiredResult`
|
||||
|
||||
Like `call_tool()` above, `MCPServer.get_prompt()` now returns
|
||||
`GetPromptResult | InputRequiredResult` and `MCPServer.read_resource()` returns
|
||||
`Iterable[ReadResourceContents] | InputRequiredResult`: at 2026-07-28 an
|
||||
`@mcp.prompt()` function or an `@mcp.resource()` template function may answer
|
||||
with an `InputRequiredResult` to request client input first (see
|
||||
[Multi-round-trip requests](advanced/multi-round-trip.md)). If you call these
|
||||
methods directly, narrow with `isinstance` (or
|
||||
`assert not isinstance(result, InputRequiredResult)` when your prompt and
|
||||
resource functions never return one). `Prompt.render()` and
|
||||
`ResourceTemplate.create_resource()` carry the same union.
|
||||
|
||||
`ctx.read_resource()` inside a handler is unchanged: it still returns content,
|
||||
and raises `RuntimeError` if the resource requests input. A handler that wants
|
||||
to receive the `InputRequiredResult` and forward it as its own result calls
|
||||
`MCPServer.read_resource(uri, context)` directly — but not from a tool whose
|
||||
dependencies elicit via `Resolve(...)`: the resolver owns that tool's
|
||||
`request_state` channel, and a forwarded result's state would clobber it.
|
||||
|
||||
### `MCPError` raised from an `@mcp.tool()` handler now surfaces as a JSON-RPC error
|
||||
|
||||
Raising `MCPError` (or a subclass such as `UrlElicitationRequiredError`) inside
|
||||
|
|
|
|||
Loading…
Reference in a new issue