| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 1066199 commit ff22f48
6 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -476,9 +476,21 @@ server_params = StdioServerParameters( | |||
| 476 | 476 | env=None # Optional environment variables | |
| 477 | 477 | ) | |
| 478 | 478 | ||
| 479 | + # Optional: create a sampling callback | ||
| 480 | + async def handle_sampling_message(message: types.CreateMessageRequestParams) -> types.CreateMessageResult: | ||
| 481 | + return types.CreateMessageResult( | ||
| 482 | + role="assistant", | ||
| 483 | + content=types.TextContent( | ||
| 484 | + type="text", | ||
| 485 | + text="Hello, world! from model", | ||
| 486 | + ), | ||
| 487 | + model="gpt-3.5-turbo", | ||
| 488 | + stopReason="endTurn", | ||
| 489 | + ) | ||
| 490 | + | ||
| 479 | 491 | async def run(): | |
| 480 | 492 | async with stdio_client(server_params) as (read, write): | |
| 481 | - async with ClientSession(read, write) as session: | ||
| 493 | + async with ClientSession(read, write, sampling_callback=handle_sampling_message) as session: | ||
| 482 | 494 | # Initialize the connection | |
| 483 | 495 | await session.initialize() | |
| 484 | 496 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1,13 +1,51 @@ | |||
| 1 | 1 | from datetime import timedelta | |
| 2 | + from typing import Any, Protocol | ||
| 2 | 3 | ||
| 3 | 4 | from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream | |
| 4 | - from pydantic import AnyUrl | ||
| 5 | + from pydantic import AnyUrl, TypeAdapter | ||
| 5 | 6 | ||
| 6 | 7 | import mcp.types as types | |
| 7 | - from mcp.shared.session import BaseSession | ||
| 8 | + from mcp.shared.context import RequestContext | ||
| 9 | + from mcp.shared.session import BaseSession, RequestResponder | ||
| 8 | 10 | from mcp.shared.version import SUPPORTED_PROTOCOL_VERSIONS | |
| 9 | 11 | ||
| 10 | 12 | ||
| 13 | + class SamplingFnT(Protocol): | ||
| 14 | + async def __call__( | ||
| 15 | + self, | ||
| 16 | + context: RequestContext["ClientSession", Any], | ||
| 17 | + params: types.CreateMessageRequestParams, | ||
| 18 | + ) -> types.CreateMessageResult | types.ErrorData: ... | ||
| 19 | + | ||
| 20 | + | ||
| 21 | + class ListRootsFnT(Protocol): | ||
| 22 | + async def __call__( | ||
| 23 | + self, context: RequestContext["ClientSession", Any] | ||
| 24 | + ) -> types.ListRootsResult | types.ErrorData: ... | ||
| 25 | + | ||
| 26 | + | ||
| 27 | + async def _default_sampling_callback( | ||
| 28 | + context: RequestContext["ClientSession", Any], | ||
| 29 | + params: types.CreateMessageRequestParams, | ||
| 30 | + ) -> types.CreateMessageResult | types.ErrorData: | ||
| 31 | + return types.ErrorData( | ||
| 32 | + code=types.INVALID_REQUEST, | ||
| 33 | + message="Sampling not supported", | ||
| 34 | + ) | ||
| 35 | + | ||
| 36 | + | ||
| 37 | + async def _default_list_roots_callback( | ||
| 38 | + context: RequestContext["ClientSession", Any], | ||
| 39 | + ) -> types.ListRootsResult | types.ErrorData: | ||
| 40 | + return types.ErrorData( | ||
| 41 | + code=types.INVALID_REQUEST, | ||
| 42 | + message="List roots not supported", | ||
| 43 | + ) | ||
| 44 | + | ||
| 45 | + | ||
| 46 | + ClientResponse = TypeAdapter(types.ClientResult | types.ErrorData) | ||
| 47 | + | ||
| 48 | + | ||
| 11 | 49 | class ClientSession( | |
| 12 | 50 | BaseSession[ | |
| 13 | 51 | types.ClientRequest, | |
@@ -22,6 +60,8 @@ def __init__( | |||
| 22 | 60 | read_stream: MemoryObjectReceiveStream[types.JSONRPCMessage | Exception], | |
| 23 | 61 | write_stream: MemoryObjectSendStream[types.JSONRPCMessage], | |
| 24 | 62 | read_timeout_seconds: timedelta | None = None, | |
| 63 | + sampling_callback: SamplingFnT | None = None, | ||
| 64 | + list_roots_callback: ListRootsFnT | None = None, | ||
| 25 | 65 | ) -> None: | |
| 26 | 66 | super().__init__( | |
| 27 | 67 | read_stream, | |
@@ -30,23 +70,34 @@ def __init__( | |||
| 30 | 70 | types.ServerNotification, | |
| 31 | 71 | read_timeout_seconds=read_timeout_seconds, | |
| 32 | 72 | ) | |
| 73 | + self._sampling_callback = sampling_callback or _default_sampling_callback | ||
| 74 | + self._list_roots_callback = list_roots_callback or _default_list_roots_callback | ||
| 33 | 75 | ||
| 34 | 76 | async def initialize(self) -> types.InitializeResult: | |
| 77 | + sampling = ( | ||
| 78 | + types.SamplingCapability() if self._sampling_callback is not None else None | ||
| 79 | + ) | ||
| 80 | + roots = ( | ||
| 81 | + types.RootsCapability( | ||
| 82 | + # TODO: Should this be based on whether we | ||
| 83 | + # _will_ send notifications, or only whether | ||
| 84 | + # they're supported? | ||
| 85 | + listChanged=True, | ||
| 86 | + ) | ||
| 87 | + if self._list_roots_callback is not None | ||
| 88 | + else None | ||
| 89 | + ) | ||
| 90 | + | ||
| 35 | 91 | result = await self.send_request( | |
| 36 | 92 | types.ClientRequest( | |
| 37 | 93 | types.InitializeRequest( | |
| 38 | 94 | method="initialize", | |
| 39 | 95 | params=types.InitializeRequestParams( | |
| 40 | 96 | protocolVersion=types.LATEST_PROTOCOL_VERSION, | |
| 41 | 97 | capabilities=types.ClientCapabilities( | |
| 42 | - sampling=None, | ||
| 98 | + sampling=sampling, | ||
| 43 | 99 | experimental=None, | |
| 44 | - roots=types.RootsCapability( | ||
| 45 | - # TODO: Should this be based on whether we | ||
| 46 | - # _will_ send notifications, or only whether | ||
| 47 | - # they're supported? | ||
| 48 | - listChanged=True | ||
| 49 | - ), | ||
| 100 | + roots=roots, | ||
| 50 | 101 | ), | |
| 51 | 102 | clientInfo=types.Implementation(name="mcp", version="0.1.0"), | |
| 52 | 103 | ), | |
@@ -243,3 +294,32 @@ async def send_roots_list_changed(self) -> None: | |||
| 243 | 294 | ) | |
| 244 | 295 | ) | |
| 245 | 296 | ) | |
| 297 | + | ||
| 298 | + async def _received_request( | ||
| 299 | + self, responder: RequestResponder[types.ServerRequest, types.ClientResult] | ||
| 300 | + ) -> None: | ||
| 301 | + ctx = RequestContext[ClientSession, Any]( | ||
| 302 | + request_id=responder.request_id, | ||
| 303 | + meta=responder.request_meta, | ||
| 304 | + session=self, | ||
| 305 | + lifespan_context=None, | ||
| 306 | + ) | ||
| 307 | + | ||
| 308 | + match responder.request.root: | ||
| 309 | + case types.CreateMessageRequest(params=params): | ||
| 310 | + with responder: | ||
| 311 | + response = await self._sampling_callback(ctx, params) | ||
| 312 | + client_response = ClientResponse.validate_python(response) | ||
| 313 | + await responder.respond(client_response) | ||
| 314 | + | ||
| 315 | + case types.ListRootsRequest(): | ||
| 316 | + with responder: | ||
| 317 | + response = await self._list_roots_callback(ctx) | ||
| 318 | + client_response = ClientResponse.validate_python(response) | ||
| 319 | + await responder.respond(client_response) | ||
| 320 | + | ||
| 321 | + case types.PingRequest(): | ||
| 322 | + with responder: | ||
| 323 | + return await responder.respond( | ||
| 324 | + types.ClientResult(root=types.EmptyResult()) | ||
| 325 | + ) | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -9,7 +9,7 @@ | |||
| 9 | 9 | import anyio | |
| 10 | 10 | from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream | |
| 11 | 11 | ||
| 12 | - from mcp.client.session import ClientSession | ||
| 12 | + from mcp.client.session import ClientSession, ListRootsFnT, SamplingFnT | ||
| 13 | 13 | from mcp.server import Server | |
| 14 | 14 | from mcp.types import JSONRPCMessage | |
| 15 | 15 | ||
@@ -54,6 +54,8 @@ async def create_client_server_memory_streams() -> ( | |||
| 54 | 54 | async def create_connected_server_and_client_session( | |
| 55 | 55 | server: Server, | |
| 56 | 56 | read_timeout_seconds: timedelta | None = None, | |
| 57 | + sampling_callback: SamplingFnT | None = None, | ||
| 58 | + list_roots_callback: ListRootsFnT | None = None, | ||
| 57 | 59 | raise_exceptions: bool = False, | |
| 58 | 60 | ) -> AsyncGenerator[ClientSession, None]: | |
| 59 | 61 | """Creates a ClientSession that is connected to a running MCP server.""" | |
@@ -80,6 +82,8 @@ async def create_connected_server_and_client_session( | |||
| 80 | 82 | read_stream=client_read, | |
| 81 | 83 | write_stream=client_write, | |
| 82 | 84 | read_timeout_seconds=read_timeout_seconds, | |
| 85 | + sampling_callback=sampling_callback, | ||
| 86 | + list_roots_callback=list_roots_callback, | ||
| 83 | 87 | ) as client_session: | |
| 84 | 88 | await client_session.initialize() | |
| 85 | 89 | yield client_session | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,70 @@ | |||
| 1 | + import pytest | ||
| 2 | + from pydantic import FileUrl | ||
| 3 | + | ||
| 4 | + from mcp.client.session import ClientSession | ||
| 5 | + from mcp.server.fastmcp.server import Context | ||
| 6 | + from mcp.shared.context import RequestContext | ||
| 7 | + from mcp.shared.memory import ( | ||
| 8 | + create_connected_server_and_client_session as create_session, | ||
| 9 | + ) | ||
| 10 | + from mcp.types import ( | ||
| 11 | + ListRootsResult, | ||
| 12 | + Root, | ||
| 13 | + TextContent, | ||
| 14 | + ) | ||
| 15 | + | ||
| 16 | + | ||
| 17 | + @pytest.mark.anyio | ||
| 18 | + async def test_list_roots_callback(): | ||
| 19 | + from mcp.server.fastmcp import FastMCP | ||
| 20 | + | ||
| 21 | + server = FastMCP("test") | ||
| 22 | + | ||
| 23 | + callback_return = ListRootsResult( | ||
| 24 | + roots=[ | ||
| 25 | + Root( | ||
| 26 | + uri=FileUrl("file://users/fake/test"), | ||
| 27 | + name="Test Root 1", | ||
| 28 | + ), | ||
| 29 | + Root( | ||
| 30 | + uri=FileUrl("file://users/fake/test/2"), | ||
| 31 | + name="Test Root 2", | ||
| 32 | + ), | ||
| 33 | + ] | ||
| 34 | + ) | ||
| 35 | + | ||
| 36 | + async def list_roots_callback( | ||
| 37 | + context: RequestContext[ClientSession, None], | ||
| 38 | + ) -> ListRootsResult: | ||
| 39 | + return callback_return | ||
| 40 | + | ||
| 41 | + @server.tool("test_list_roots") | ||
| 42 | + async def test_list_roots(context: Context, message: str): | ||
| 43 | + roots = await context.session.list_roots() | ||
| 44 | + assert roots == callback_return | ||
| 45 | + return True | ||
| 46 | + | ||
| 47 | + # Test with list_roots callback | ||
| 48 | + async with create_session( | ||
| 49 | + server._mcp_server, list_roots_callback=list_roots_callback | ||
| 50 | + ) as client_session: | ||
| 51 | + # Make a request to trigger sampling callback | ||
| 52 | + result = await client_session.call_tool( | ||
| 53 | + "test_list_roots", {"message": "test message"} | ||
| 54 | + ) | ||
| 55 | + assert result.isError is False | ||
| 56 | + assert isinstance(result.content[0], TextContent) | ||
| 57 | + assert result.content[0].text == "true" | ||
| 58 | + | ||
| 59 | + # Test without list_roots callback | ||
| 60 | + async with create_session(server._mcp_server) as client_session: | ||
| 61 | + # Make a request to trigger sampling callback | ||
| 62 | + result = await client_session.call_tool( | ||
| 63 | + "test_list_roots", {"message": "test message"} | ||
| 64 | + ) | ||
| 65 | + assert result.isError is True | ||
| 66 | + assert isinstance(result.content[0], TextContent) | ||
| 67 | + assert ( | ||
| 68 | + result.content[0].text | ||
| 69 | + == "Error executing tool test_list_roots: List roots not supported" | ||
| 70 | + ) | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,73 @@ | |||
| 1 | + import pytest | ||
| 2 | + | ||
| 3 | + from mcp.client.session import ClientSession | ||
| 4 | + from mcp.shared.context import RequestContext | ||
| 5 | + from mcp.shared.memory import ( | ||
| 6 | + create_connected_server_and_client_session as create_session, | ||
| 7 | + ) | ||
| 8 | + from mcp.types import ( | ||
| 9 | + CreateMessageRequestParams, | ||
| 10 | + CreateMessageResult, | ||
| 11 | + SamplingMessage, | ||
| 12 | + TextContent, | ||
| 13 | + ) | ||
| 14 | + | ||
| 15 | + | ||
| 16 | + @pytest.mark.anyio | ||
| 17 | + async def test_sampling_callback(): | ||
| 18 | + from mcp.server.fastmcp import FastMCP | ||
| 19 | + | ||
| 20 | + server = FastMCP("test") | ||
| 21 | + | ||
| 22 | + callback_return = CreateMessageResult( | ||
| 23 | + role="assistant", | ||
| 24 | + content=TextContent( | ||
| 25 | + type="text", text="This is a response from the sampling callback" | ||
| 26 | + ), | ||
| 27 | + model="test-model", | ||
| 28 | + stopReason="endTurn", | ||
| 29 | + ) | ||
| 30 | + | ||
| 31 | + async def sampling_callback( | ||
| 32 | + context: RequestContext[ClientSession, None], | ||
| 33 | + params: CreateMessageRequestParams, | ||
| 34 | + ) -> CreateMessageResult: | ||
| 35 | + return callback_return | ||
| 36 | + | ||
| 37 | + @server.tool("test_sampling") | ||
| 38 | + async def test_sampling_tool(message: str): | ||
| 39 | + value = await server.get_context().session.create_message( | ||
| 40 | + messages=[ | ||
| 41 | + SamplingMessage( | ||
| 42 | + role="user", content=TextContent(type="text", text=message) | ||
| 43 | + ) | ||
| 44 | + ], | ||
| 45 | + max_tokens=100, | ||
| 46 | + ) | ||
| 47 | + assert value == callback_return | ||
| 48 | + return True | ||
| 49 | + | ||
| 50 | + # Test with sampling callback | ||
| 51 | + async with create_session( | ||
| 52 | + server._mcp_server, sampling_callback=sampling_callback | ||
| 53 | + ) as client_session: | ||
| 54 | + # Make a request to trigger sampling callback | ||
| 55 | + result = await client_session.call_tool( | ||
| 56 | + "test_sampling", {"message": "Test message for sampling"} | ||
| 57 | + ) | ||
| 58 | + assert result.isError is False | ||
| 59 | + assert isinstance(result.content[0], TextContent) | ||
| 60 | + assert result.content[0].text == "true" | ||
| 61 | + | ||
| 62 | + # Test without sampling callback | ||
| 63 | + async with create_session(server._mcp_server) as client_session: | ||
| 64 | + # Make a request to trigger sampling callback | ||
| 65 | + result = await client_session.call_tool( | ||
| 66 | + "test_sampling", {"message": "Test message for sampling"} | ||
| 67 | + ) | ||
| 68 | + assert result.isError is True | ||
| 69 | + assert isinstance(result.content[0], TextContent) | ||
| 70 | + assert ( | ||
| 71 | + result.content[0].text | ||
| 72 | + == "Error executing tool test_sampling: Sampling not supported" | ||
| 73 | + ) | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1,12 +1,17 @@ | |||
| 1 | + import shutil | ||
| 2 | + | ||
| 1 | 3 | import pytest | |
| 2 | 4 | ||
| 3 | 5 | from mcp.client.stdio import StdioServerParameters, stdio_client | |
| 4 | 6 | from mcp.types import JSONRPCMessage, JSONRPCRequest, JSONRPCResponse | |
| 5 | 7 | ||
| 8 | + tee: str = shutil.which("tee") # type: ignore | ||
| 9 | + | ||
| 6 | 10 | ||
| 7 | 11 | @pytest.mark.anyio | |
| 12 | + @pytest.mark.skipif(tee is None, reason="could not find tee command") | ||
| 8 | 13 | async def test_stdio_client(): | |
| 9 | - server_parameters = StdioServerParameters(command="/usr/bin/tee") | ||
| 14 | + server_parameters = StdioServerParameters(command=tee) | ||
| 10 | 15 | ||
| 11 | 16 | async with stdio_client(server_parameters) as (read_stream, write_stream): | |
| 12 | 17 | # Test sending and receiving messages | |
| Back | FazBrowse Home | New Git URL |
0 commit comments