Skip to content

Commit f689d8f

Browse files
committed
Make Context.read_resource raise-only on InputRequiredResult
Drop the allow_input_required overloads: ctx.read_resource is a content reader, so an InputRequiredResult from a resource template always raises, with the error pointing at the forwarding path. A handler that wants to receive and forward the result as its own calls MCPServer.read_resource(uri, context) directly, which carries the union.
1 parent 4a8cb8c commit f689d8f

3 files changed

Lines changed: 24 additions & 40 deletions

File tree

docs/migration.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,10 @@ methods directly, narrow with `isinstance` (or
3333
resource functions never return one). `Prompt.render()` and
3434
`ResourceTemplate.create_resource()` carry the same union.
3535

36-
`ctx.read_resource()` inside a handler is unchanged by default: it still
37-
returns content and raises `RuntimeError` if the resource requests input; pass
38-
`allow_input_required=True` to receive the `InputRequiredResult` and forward it
39-
as the handler's own result.
36+
`ctx.read_resource()` inside a handler is unchanged: it still returns content,
37+
and raises `RuntimeError` if the resource requests input. A handler that wants
38+
to receive the `InputRequiredResult` and forward it as its own result calls
39+
`MCPServer.read_resource(uri, context)` directly.
4040

4141
### `MCPError` raised from an `@mcp.tool()` handler now surfaces as a JSON-RPC error
4242

src/mcp/server/mcpserver/context.py

Lines changed: 12 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22

33
from collections.abc import Iterable, Mapping
4-
from typing import TYPE_CHECKING, Any, Generic, Literal, cast, overload
4+
from typing import TYPE_CHECKING, Any, Generic, cast
55

66
from mcp_types import ClientCapabilities, InputRequiredResult, InputResponseRequestParams, InputResponses, LoggingLevel
77
from pydantic import AnyUrl, BaseModel
@@ -99,47 +99,31 @@ async def report_progress(self, progress: float, total: float | None = None, mes
9999
"""
100100
await self.request_context.session.report_progress(progress, total, message)
101101

102-
@overload
103-
async def read_resource(
104-
self, uri: str | AnyUrl, *, allow_input_required: Literal[False] = False
105-
) -> Iterable[ReadResourceContents]: ...
106-
107-
@overload
108-
async def read_resource(
109-
self, uri: str | AnyUrl, *, allow_input_required: bool
110-
) -> Iterable[ReadResourceContents] | InputRequiredResult: ...
111-
112-
async def read_resource(
113-
self, uri: str | AnyUrl, *, allow_input_required: bool = False
114-
) -> Iterable[ReadResourceContents] | InputRequiredResult:
102+
async def read_resource(self, uri: str | AnyUrl) -> Iterable[ReadResourceContents]:
115103
"""Read a resource by URI.
116104
105+
This is a content reader: an `InputRequiredResult` returned by a
106+
resource template function (the 2026-07-28 multi-round-trip flow)
107+
raises here. A handler that wants to receive and forward one as its
108+
own result calls `MCPServer.read_resource(uri, context)` instead.
109+
117110
Args:
118111
uri: Resource URI to read
119-
allow_input_required: When `False` (default), an
120-
`InputRequiredResult` returned by a resource template function
121-
(the 2026-07-28 multi-round-trip flow) raises instead of being
122-
returned. Pass `True` to receive it — a handler may forward it
123-
as its own result; the retry's answers then arrive on
124-
`ctx.input_responses`.
125112
126113
Returns:
127-
The resource content as either text or bytes, or — only with
128-
`allow_input_required=True` — the `InputRequiredResult` the
129-
resource template function returned.
114+
The resource content as either text or bytes
130115
131116
Raises:
132117
ResourceNotFoundError: If no resource or template matches the URI.
133118
ResourceError: If template creation or resource reading fails.
134-
RuntimeError: If the resource returned an `InputRequiredResult`
135-
and `allow_input_required` is `False`.
119+
RuntimeError: If the resource returned an `InputRequiredResult`.
136120
"""
137121
assert self._mcp_server is not None, "Context is not available outside of a request"
138122
result = await self._mcp_server.read_resource(uri, self)
139-
if isinstance(result, InputRequiredResult) and not allow_input_required:
123+
if isinstance(result, InputRequiredResult):
140124
raise RuntimeError(
141-
"Resource returned InputRequiredResult; pass allow_input_required=True to "
142-
"receive it and forward it as this handler's result."
125+
"Resource returned InputRequiredResult; ctx.read_resource() only returns "
126+
"content — use MCPServer.read_resource(uri, context) to receive and forward it."
143127
)
144128
return result
145129

tests/server/mcpserver/test_server.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2071,9 +2071,9 @@ async def ask(topic: str, ctx: Context) -> str | InputRequiredResult:
20712071
assert contents.text == "databases notes for Alice"
20722072

20732073

2074-
async def test_context_read_resource_raises_on_input_required_result_by_default():
2074+
async def test_context_read_resource_raises_on_input_required_result():
20752075
"""ctx.read_resource is a content reader: an InputRequiredResult from the template
2076-
raises with the opt-in hint instead of widening every caller (mirrors ClientSession)."""
2076+
raises with a pointer at the forwarding path instead of widening every caller."""
20772077
mcp = MCPServer()
20782078

20792079
@mcp.resource("ask://{topic}")
@@ -2084,14 +2084,14 @@ async def ask(topic: str, ctx: Context) -> str | InputRequiredResult:
20842084
with pytest.raises(RuntimeError) as exc:
20852085
await context.read_resource("ask://databases")
20862086
assert str(exc.value) == snapshot(
2087-
"Resource returned InputRequiredResult; pass allow_input_required=True to receive it "
2088-
"and forward it as this handler's result."
2087+
"Resource returned InputRequiredResult; ctx.read_resource() only returns "
2088+
"content — use MCPServer.read_resource(uri, context) to receive and forward it."
20892089
)
20902090

20912091

2092-
async def test_context_read_resource_with_allow_input_required_forwards_the_result():
2093-
"""With allow_input_required=True the handler receives the template's
2094-
InputRequiredResult unchanged and may forward it as its own result."""
2092+
async def test_mcpserver_read_resource_returns_input_required_result_for_handler_forwarding():
2093+
"""MCPServer.read_resource hands the template's InputRequiredResult to a direct caller
2094+
unchanged — the composition path for a handler that forwards it as its own result."""
20952095
mcp = MCPServer()
20962096
sentinel = InputRequiredResult(input_requests={"who": _ask_who()})
20972097

@@ -2100,7 +2100,7 @@ async def ask(topic: str, ctx: Context) -> str | InputRequiredResult:
21002100
return sentinel
21012101

21022102
context = Context(mcp_server=mcp)
2103-
result = await context.read_resource("ask://databases", allow_input_required=True)
2103+
result = await mcp.read_resource("ask://databases", context)
21042104
assert result is sentinel
21052105

21062106

0 commit comments

Comments
 (0)