[v2] ClientSession runs on JSONRPCDispatcher; BaseSession removed (#2838)
This commit is contained in:
parent
cf110e3214
commit
1012d60004
5 changed files with 2886 additions and 1734 deletions
|
|
@ -6,9 +6,9 @@ import pytest
|
|||
from pydantic import BaseModel, Field
|
||||
|
||||
from mcp import Client, types
|
||||
from mcp.client.session import ClientSession, ElicitationFnT
|
||||
from mcp.client import ClientRequestContext
|
||||
from mcp.client.session import ElicitationFnT
|
||||
from mcp.server.mcpserver import Context, MCPServer
|
||||
from mcp.shared._context import RequestContext
|
||||
from mcp.types import ElicitRequestParams, ElicitResult, TextContent
|
||||
|
||||
|
||||
|
|
@ -64,7 +64,7 @@ async def test_elicitation_accept_returns_the_users_answer_to_the_tool():
|
|||
create_ask_user_tool(mcp)
|
||||
|
||||
# Create a custom handler for elicitation requests
|
||||
async def elicitation_callback(context: RequestContext[ClientSession], params: ElicitRequestParams):
|
||||
async def elicitation_callback(context: ClientRequestContext, params: ElicitRequestParams):
|
||||
if params.message == "Tool wants to ask: What is your name?":
|
||||
return ElicitResult(action="accept", content={"answer": "Test User"})
|
||||
else: # pragma: no cover
|
||||
|
|
@ -81,7 +81,7 @@ async def test_elicitation_decline_reaches_the_tool_without_content():
|
|||
mcp = MCPServer(name="ElicitationDeclineServer")
|
||||
create_ask_user_tool(mcp)
|
||||
|
||||
async def elicitation_callback(context: RequestContext[ClientSession], params: ElicitRequestParams):
|
||||
async def elicitation_callback(context: ClientRequestContext, params: ElicitRequestParams):
|
||||
return ElicitResult(action="decline")
|
||||
|
||||
await call_tool_and_assert(
|
||||
|
|
@ -119,9 +119,7 @@ async def test_elicitation_schema_validation():
|
|||
create_validation_tool("nested_model", InvalidNestedSchema)
|
||||
|
||||
# Dummy callback (won't be called due to validation failure)
|
||||
async def elicitation_callback(
|
||||
context: RequestContext[ClientSession], params: ElicitRequestParams
|
||||
): # pragma: no cover
|
||||
async def elicitation_callback(context: ClientRequestContext, params: ElicitRequestParams): # pragma: no cover
|
||||
return ElicitResult(action="accept", content={})
|
||||
|
||||
async with Client(mcp, elicitation_callback=elicitation_callback) as client:
|
||||
|
|
@ -176,7 +174,7 @@ async def test_elicitation_with_optional_fields():
|
|||
|
||||
for content, expected in test_cases:
|
||||
|
||||
async def callback(context: RequestContext[ClientSession], params: ElicitRequestParams):
|
||||
async def callback(context: ClientRequestContext, params: ElicitRequestParams):
|
||||
return ElicitResult(action="accept", content=content)
|
||||
|
||||
await call_tool_and_assert(mcp, callback, "optional_tool", {}, expected)
|
||||
|
|
@ -194,9 +192,7 @@ async def test_elicitation_with_optional_fields():
|
|||
except TypeError as e:
|
||||
return f"Validation failed: {str(e)}"
|
||||
|
||||
async def elicitation_callback(
|
||||
context: RequestContext[ClientSession], params: ElicitRequestParams
|
||||
): # pragma: no cover
|
||||
async def elicitation_callback(context: ClientRequestContext, params: ElicitRequestParams): # pragma: no cover
|
||||
return ElicitResult(action="accept", content={})
|
||||
|
||||
await call_tool_and_assert(
|
||||
|
|
@ -219,7 +215,7 @@ async def test_elicitation_with_optional_fields():
|
|||
return f"Name: {result.data.name}, Tags: {', '.join(result.data.tags)}"
|
||||
return f"User {result.action}" # pragma: no cover
|
||||
|
||||
async def multiselect_callback(context: RequestContext[ClientSession], params: ElicitRequestParams):
|
||||
async def multiselect_callback(context: ClientRequestContext, params: ElicitRequestParams):
|
||||
if "Please provide tags" in params.message:
|
||||
return ElicitResult(action="accept", content={"name": "Test", "tags": ["tag1", "tag2"]})
|
||||
return ElicitResult(action="decline") # pragma: no cover
|
||||
|
|
@ -239,7 +235,7 @@ async def test_elicitation_with_optional_fields():
|
|||
return f"Name: {result.data.name}, Tags: {tags_str}"
|
||||
return f"User {result.action}" # pragma: no cover
|
||||
|
||||
async def optional_multiselect_callback(context: RequestContext[ClientSession], params: ElicitRequestParams):
|
||||
async def optional_multiselect_callback(context: ClientRequestContext, params: ElicitRequestParams):
|
||||
if "Please provide optional tags" in params.message:
|
||||
return ElicitResult(action="accept", content={"name": "Test", "tags": ["tag1", "tag2"]})
|
||||
return ElicitResult(action="decline") # pragma: no cover
|
||||
|
|
@ -273,7 +269,7 @@ async def test_elicitation_with_default_values():
|
|||
return f"User {result.action}"
|
||||
|
||||
# First verify that defaults are present in the JSON schema sent to clients
|
||||
async def callback_schema_verify(context: RequestContext[ClientSession], params: ElicitRequestParams):
|
||||
async def callback_schema_verify(context: ClientRequestContext, params: ElicitRequestParams):
|
||||
# Verify the schema includes defaults
|
||||
assert isinstance(params, types.ElicitRequestFormParams), "Expected form mode elicitation"
|
||||
schema = params.requested_schema
|
||||
|
|
@ -295,7 +291,7 @@ async def test_elicitation_with_default_values():
|
|||
)
|
||||
|
||||
# Test overriding defaults
|
||||
async def callback_override(context: RequestContext[ClientSession], params: ElicitRequestParams):
|
||||
async def callback_override(context: ClientRequestContext, params: ElicitRequestParams):
|
||||
return ElicitResult(
|
||||
action="accept", content={"email": "john@example.com", "name": "John", "age": 25, "subscribe": False}
|
||||
)
|
||||
|
|
@ -371,7 +367,7 @@ async def test_elicitation_with_enum_titles():
|
|||
return f"User: {result.data.user_name}, Color: {result.data.color}"
|
||||
return f"User {result.action}" # pragma: no cover
|
||||
|
||||
async def enum_callback(context: RequestContext[ClientSession], params: ElicitRequestParams):
|
||||
async def enum_callback(context: ClientRequestContext, params: ElicitRequestParams):
|
||||
if "colors" in params.message and "legacy" not in params.message:
|
||||
return ElicitResult(action="accept", content={"user_name": "Bob", "favorite_colors": ["red", "green"]})
|
||||
elif "color" in params.message:
|
||||
|
|
|
|||
|
|
@ -26,9 +26,7 @@ from examples.snippets.servers import (
|
|||
structured_output,
|
||||
tool_progress,
|
||||
)
|
||||
from mcp.client import Client
|
||||
from mcp.client.session import ClientSession
|
||||
from mcp.shared._context import RequestContext
|
||||
from mcp.client import Client, ClientRequestContext
|
||||
from mcp.shared.session import RequestResponder
|
||||
from mcp.types import (
|
||||
ClientResult,
|
||||
|
|
@ -80,9 +78,7 @@ class NotificationCollector:
|
|||
self.tool_notifications.append(message.params)
|
||||
|
||||
|
||||
async def sampling_callback(
|
||||
context: RequestContext[ClientSession], params: CreateMessageRequestParams
|
||||
) -> CreateMessageResult:
|
||||
async def sampling_callback(context: ClientRequestContext, params: CreateMessageRequestParams) -> CreateMessageResult:
|
||||
"""Sampling callback for tests."""
|
||||
return CreateMessageResult(
|
||||
role="assistant",
|
||||
|
|
@ -94,7 +90,7 @@ async def sampling_callback(
|
|||
)
|
||||
|
||||
|
||||
async def elicitation_callback(context: RequestContext[ClientSession], params: ElicitRequestParams):
|
||||
async def elicitation_callback(context: ClientRequestContext, params: ElicitRequestParams):
|
||||
"""Elicitation callback for tests."""
|
||||
# For restaurant booking test
|
||||
if "No tables available" in params.message:
|
||||
|
|
|
|||
|
|
@ -1098,10 +1098,10 @@ class TestContextInjection:
|
|||
assert "Logged messages for test" in content.text
|
||||
|
||||
assert mock_log.call_count == 4
|
||||
mock_log.assert_any_call(level="debug", data="Debug message", logger=None, related_request_id="1")
|
||||
mock_log.assert_any_call(level="info", data="Info message", logger=None, related_request_id="1")
|
||||
mock_log.assert_any_call(level="warning", data="Warning message", logger=None, related_request_id="1")
|
||||
mock_log.assert_any_call(level="error", data="Error message", logger=None, related_request_id="1")
|
||||
mock_log.assert_any_call(level="debug", data="Debug message", logger=None, related_request_id="2")
|
||||
mock_log.assert_any_call(level="info", data="Info message", logger=None, related_request_id="2")
|
||||
mock_log.assert_any_call(level="warning", data="Warning message", logger=None, related_request_id="2")
|
||||
mock_log.assert_any_call(level="error", data="Error message", logger=None, related_request_id="2")
|
||||
|
||||
async def test_optional_context(self):
|
||||
"""Test that context is optional."""
|
||||
|
|
|
|||
|
|
@ -5,10 +5,9 @@ import pytest
|
|||
from pydantic import BaseModel, Field
|
||||
|
||||
from mcp import Client, types
|
||||
from mcp.client.session import ClientSession
|
||||
from mcp.client import ClientRequestContext
|
||||
from mcp.server.elicitation import CancelledElicitation, DeclinedElicitation, elicit_url
|
||||
from mcp.server.mcpserver import Context, MCPServer
|
||||
from mcp.shared._context import RequestContext
|
||||
from mcp.types import ElicitRequestParams, ElicitResult, TextContent
|
||||
|
||||
|
||||
|
|
@ -28,7 +27,7 @@ async def test_url_elicitation_accept():
|
|||
return f"User {result.action}"
|
||||
|
||||
# Create elicitation callback that accepts URL mode
|
||||
async def elicitation_callback(context: RequestContext[ClientSession], params: ElicitRequestParams):
|
||||
async def elicitation_callback(context: ClientRequestContext, params: ElicitRequestParams):
|
||||
assert params.mode == "url"
|
||||
assert params.url == "https://example.com/api_key_setup"
|
||||
assert params.elicitation_id == "test-elicitation-001"
|
||||
|
|
@ -57,7 +56,7 @@ async def test_url_elicitation_decline():
|
|||
# Test only checks decline path
|
||||
return f"User {result.action} authorization"
|
||||
|
||||
async def elicitation_callback(context: RequestContext[ClientSession], params: ElicitRequestParams):
|
||||
async def elicitation_callback(context: ClientRequestContext, params: ElicitRequestParams):
|
||||
assert params.mode == "url"
|
||||
return ElicitResult(action="decline")
|
||||
|
||||
|
|
@ -83,7 +82,7 @@ async def test_url_elicitation_cancel():
|
|||
# Test only checks cancel path
|
||||
return f"User {result.action} payment"
|
||||
|
||||
async def elicitation_callback(context: RequestContext[ClientSession], params: ElicitRequestParams):
|
||||
async def elicitation_callback(context: ClientRequestContext, params: ElicitRequestParams):
|
||||
assert params.mode == "url"
|
||||
return ElicitResult(action="cancel")
|
||||
|
||||
|
|
@ -110,7 +109,7 @@ async def test_url_elicitation_helper_function():
|
|||
# Test only checks accept path - return the type name
|
||||
return type(result).__name__
|
||||
|
||||
async def elicitation_callback(context: RequestContext[ClientSession], params: ElicitRequestParams):
|
||||
async def elicitation_callback(context: ClientRequestContext, params: ElicitRequestParams):
|
||||
return ElicitResult(action="accept")
|
||||
|
||||
async with Client(mcp, elicitation_callback=elicitation_callback) as client:
|
||||
|
|
@ -137,7 +136,7 @@ async def test_url_no_content_in_response():
|
|||
assert result.content is None
|
||||
return f"Action: {result.action}, Content: {result.content}"
|
||||
|
||||
async def elicitation_callback(context: RequestContext[ClientSession], params: ElicitRequestParams):
|
||||
async def elicitation_callback(context: ClientRequestContext, params: ElicitRequestParams):
|
||||
# Verify that this is URL mode
|
||||
assert params.mode == "url"
|
||||
assert isinstance(params, types.ElicitRequestURLParams)
|
||||
|
|
@ -170,7 +169,7 @@ async def test_form_mode_still_works():
|
|||
assert result.data is not None
|
||||
return f"Hello, {result.data.name}!"
|
||||
|
||||
async def elicitation_callback(context: RequestContext[ClientSession], params: ElicitRequestParams):
|
||||
async def elicitation_callback(context: ClientRequestContext, params: ElicitRequestParams):
|
||||
# Verify form mode parameters
|
||||
assert params.mode == "form"
|
||||
assert isinstance(params, types.ElicitRequestFormParams)
|
||||
|
|
@ -206,7 +205,7 @@ async def test_elicit_complete_notification():
|
|||
|
||||
return "Elicitation completed"
|
||||
|
||||
async def elicitation_callback(context: RequestContext[ClientSession], params: ElicitRequestParams):
|
||||
async def elicitation_callback(context: ClientRequestContext, params: ElicitRequestParams):
|
||||
return ElicitResult(action="accept") # pragma: no cover
|
||||
|
||||
async with Client(mcp, elicitation_callback=elicitation_callback) as client:
|
||||
|
|
@ -263,7 +262,7 @@ async def test_elicit_url_typed_results():
|
|||
return "Not cancelled" # pragma: no cover
|
||||
|
||||
# Test declined result
|
||||
async def decline_callback(context: RequestContext[ClientSession], params: ElicitRequestParams):
|
||||
async def decline_callback(context: ClientRequestContext, params: ElicitRequestParams):
|
||||
return ElicitResult(action="decline")
|
||||
|
||||
async with Client(mcp, elicitation_callback=decline_callback) as client:
|
||||
|
|
@ -273,7 +272,7 @@ async def test_elicit_url_typed_results():
|
|||
assert result.content[0].text == "Declined"
|
||||
|
||||
# Test cancelled result
|
||||
async def cancel_callback(context: RequestContext[ClientSession], params: ElicitRequestParams):
|
||||
async def cancel_callback(context: ClientRequestContext, params: ElicitRequestParams):
|
||||
return ElicitResult(action="cancel")
|
||||
|
||||
async with Client(mcp, elicitation_callback=cancel_callback) as client:
|
||||
|
|
@ -303,7 +302,7 @@ async def test_deprecated_elicit_method():
|
|||
return f"Email: {result.content.get('email', 'none')}"
|
||||
return "No email provided" # pragma: no cover
|
||||
|
||||
async def elicitation_callback(context: RequestContext[ClientSession], params: ElicitRequestParams):
|
||||
async def elicitation_callback(context: ClientRequestContext, params: ElicitRequestParams):
|
||||
# Verify this is form mode
|
||||
assert params.mode == "form"
|
||||
assert params.requested_schema is not None
|
||||
|
|
@ -331,7 +330,7 @@ async def test_ctx_elicit_url_convenience_method():
|
|||
)
|
||||
return f"Result: {result.action}"
|
||||
|
||||
async def elicitation_callback(context: RequestContext[ClientSession], params: ElicitRequestParams):
|
||||
async def elicitation_callback(context: ClientRequestContext, params: ElicitRequestParams):
|
||||
assert params.mode == "url"
|
||||
assert params.elicitation_id == "ctx-test-001"
|
||||
return ElicitResult(action="accept")
|
||||
|
|
|
|||
|
|
@ -103,13 +103,13 @@ async def test_send_request_omits_call_options_when_none_given():
|
|||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_send_request_timeout_zero_means_no_timeout():
|
||||
"""0 falls through BaseSession's `or`-fallback, so it has always meant
|
||||
"no timeout"; ClientSession still reads it that way."""
|
||||
async def test_send_request_timeout_zero_is_forwarded():
|
||||
"""0 is a real timeout (fail at the first checkpoint, `anyio.fail_after(0)`
|
||||
semantics) and must reach the dispatcher; only `None` means "no timeout"."""
|
||||
dispatcher = StubDispatcher(result={})
|
||||
session = _make_session(dispatcher)
|
||||
await session.send_request(types.PingRequest(), types.EmptyResult, request_read_timeout_seconds=0)
|
||||
assert dispatcher.requests[0][2] is None
|
||||
await session.send_request(types.PingRequest(), types.EmptyResult, request_read_timeout_seconds=0.0)
|
||||
assert dispatcher.requests[0][2] == {"timeout": 0.0}
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
|
|
|
|||
Loading…
Reference in a new issue