Pass InputRequiredResult through the MCPServer prompt and resource pipelines (#3020)
This commit is contained in:
parent
8f2c97b769
commit
8d0f928e40
2 changed files with 627 additions and 32 deletions
|
|
@ -3,6 +3,8 @@ import threading
|
|||
import anyio
|
||||
import anyio.from_thread
|
||||
import pytest
|
||||
from inline_snapshot import snapshot
|
||||
from mcp_types import InputRequiredResult
|
||||
from pydantic import BaseModel
|
||||
|
||||
from mcp.server.mcpserver.resources import FunctionResource
|
||||
|
|
@ -242,3 +244,20 @@ async def test_sync_fn_does_not_block_event_loop():
|
|||
release.set()
|
||||
|
||||
assert result == ["done"]
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_read_rejects_an_input_required_result_from_a_static_function():
|
||||
"""A static resource function returning an InputRequiredResult is a mistake (it can
|
||||
never read the retry's input_responses), so read() raises instead of JSON-dumping it."""
|
||||
|
||||
def ask() -> InputRequiredResult:
|
||||
return InputRequiredResult(request_state="round-1")
|
||||
|
||||
resource = FunctionResource(uri="resource://ask", name="ask", fn=ask)
|
||||
with pytest.raises(ValueError) as exc:
|
||||
await resource.read()
|
||||
assert str(exc.value) == snapshot(
|
||||
"Error reading resource resource://ask: static resources cannot return "
|
||||
"InputRequiredResult; only resource template functions participate in the multi-round-trip flow"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import threading
|
|||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from mcp_types import Annotations
|
||||
from mcp_types import Annotations, ElicitRequest, ElicitRequestFormParams, InputRequiredResult
|
||||
from pydantic import BaseModel
|
||||
|
||||
from mcp.server.mcpserver import Context, MCPServer
|
||||
|
|
@ -403,6 +403,7 @@ class TestResourceTemplateAnnotations:
|
|||
|
||||
# Create a resource from the template
|
||||
resource = await template.create_resource("resource://items/123", {"item_id": "123"}, Context())
|
||||
assert not isinstance(resource, InputRequiredResult)
|
||||
|
||||
# The resource should inherit the template's annotations
|
||||
assert resource.annotations is not None
|
||||
|
|
@ -477,3 +478,30 @@ async def test_sync_fn_runs_in_worker_thread():
|
|||
assert isinstance(resource, FunctionResource)
|
||||
assert await resource.read() == "hello world"
|
||||
assert fn_thread[0] != main_thread
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_create_resource_passes_input_required_result_through_unchanged():
|
||||
"""create_resource returns the InputRequiredResult the template function returned
|
||||
instead of wrapping it in a FunctionResource (SEP-2322 multi-round-trip pass-through)."""
|
||||
sentinel = InputRequiredResult(
|
||||
input_requests={
|
||||
"who": ElicitRequest(
|
||||
params=ElicitRequestFormParams(
|
||||
message="Who is this for?",
|
||||
requested_schema={
|
||||
"type": "object",
|
||||
"properties": {"name": {"type": "string"}},
|
||||
"required": ["name"],
|
||||
},
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
def ask(topic: str) -> InputRequiredResult:
|
||||
return sentinel
|
||||
|
||||
template = ResourceTemplate.from_function(fn=ask, uri_template="ask://{topic}")
|
||||
result = await template.create_resource("ask://databases", {"topic": "databases"}, Context())
|
||||
assert result is sentinel
|
||||
|
|
|
|||
Loading…
Reference in a new issue