33import time
44import json
55import anyio
6+ import threading
7+ import uvicorn
8+ import pytest
69from pydantic import AnyUrl
710from pydantic_core import Url
811import pytest
1114from starlette .applications import Starlette
1215from starlette .routing import Mount , Route
1316
17+ from mcp .shared .exceptions import McpError
1418from mcp .client .session import ClientSession
1519from mcp .client .sse import sse_client
1620from mcp .server import Server
1721from mcp .server .sse import SseServerTransport
18- from mcp .types import EmptyResult , InitializeResult , TextContent , TextResourceContents , Tool
19-
20- SERVER_URL = "http://127.0.0.1:8765"
21- SERVER_SSE_URL = f"{ SERVER_URL } /sse"
22+ from mcp .types import EmptyResult , ErrorData , InitializeResult , TextContent , TextResourceContents , Tool
2223
2324SERVER_NAME = "test_server_for_SSE"
2425
26+ @pytest .fixture
27+ def server_port () -> int :
28+ import socket
29+
30+ s = socket .socket ()
31+ s .bind (('' , 0 ))
32+ port = s .getsockname ()[1 ]
33+ s .close ()
34+ return port
35+
36+ @pytest .fixture
37+ def server_url (server_port : int ) -> str :
38+ return f"http://127.0.0.1:{ server_port } "
39+
2540# Test server implementation
2641class TestServer (Server ):
2742 def __init__ (self ):
@@ -32,7 +47,7 @@ async def handle_read_resource(uri: AnyUrl) -> str | bytes:
3247 if uri .scheme == "foobar" :
3348 return f"Read { uri .host } "
3449 # TODO: make this an error
35- return "NOT FOUND"
50+ raise McpError ( error = ErrorData ( code = 404 , message = "OOPS! no resource with that URI was found" ))
3651
3752 @self .list_tools ()
3853 async def handle_list_tools ():
@@ -48,9 +63,6 @@ async def handle_list_tools():
4863 async def handle_call_tool (name : str , args : dict ):
4964 return [TextContent (type = "text" , text = f"Called { name } " )]
5065
51- import threading
52- import uvicorn
53- import pytest
5466
5567
5668# Test fixtures
@@ -78,10 +90,10 @@ async def handle_sse(request):
7890 return app
7991
8092@pytest .fixture ()
81- def server (server_app : Starlette ):
82- server = uvicorn .Server (config = uvicorn .Config (app = server_app , host = "127.0.0.1" , port = 8765 , log_level = "error" ))
93+ def server (server_app : Starlette , server_port : int ):
94+ server = uvicorn .Server (config = uvicorn .Config (app = server_app , host = "127.0.0.1" , port = server_port , log_level = "error" ))
8395 server_thread = threading .Thread ( target = server .run , daemon = True )
84- print ('starting server' )
96+ print (f 'starting server on { server_port } ' )
8597 server_thread .start ()
8698 # Give server time to start
8799 while not server .started :
@@ -92,9 +104,9 @@ def server(server_app: Starlette):
92104 server_thread .join (timeout = 0.1 )
93105
94106@pytest .fixture ()
95- async def http_client (server ) -> AsyncGenerator [httpx .AsyncClient , None ]:
107+ async def http_client (server , server_url ) -> AsyncGenerator [httpx .AsyncClient , None ]:
96108 """Create test client"""
97- async with httpx .AsyncClient (base_url = SERVER_URL ) as client :
109+ async with httpx .AsyncClient (base_url = server_url ) as client :
98110 yield client
99111
100112# Tests
@@ -123,8 +135,8 @@ async def connection_test():
123135
124136
125137@pytest .mark .anyio
126- async def test_sse_client_basic_connection (server ):
127- async with sse_client (SERVER_SSE_URL ) as streams :
138+ async def test_sse_client_basic_connection (server , server_url ):
139+ async with sse_client (server_url + "/sse" ) as streams :
128140 async with ClientSession (* streams ) as session :
129141 # Test initialization
130142 result = await session .initialize ()
@@ -136,18 +148,22 @@ async def test_sse_client_basic_connection(server):
136148 assert isinstance (ping_result , EmptyResult )
137149
138150@pytest .fixture
139- async def initialized_sse_client_session (server ) -> AsyncGenerator [ClientSession , None ]:
140- async with sse_client (SERVER_SSE_URL ) as streams :
151+ async def initialized_sse_client_session (server , server_url : str ) -> AsyncGenerator [ClientSession , None ]:
152+ async with sse_client (server_url + "/sse" ) as streams :
141153 async with ClientSession (* streams ) as session :
142154 await session .initialize ()
143155 yield session
144156
145157@pytest .mark .anyio
146- async def test_sse_client_request_and_response (initialized_sse_client_session : ClientSession ):
158+ async def test_sse_client_happy_request_and_response (initialized_sse_client_session : ClientSession ):
147159 session = initialized_sse_client_session
148- # TODO: expect raise
149- await session .read_resource (uri = AnyUrl ("xxx://will-not-work" ))
150160 response = await session .read_resource (uri = AnyUrl ("foobar://should-work" ))
151161 assert len (response .contents ) == 1
152162 assert isinstance (response .contents [0 ], TextResourceContents )
153163 assert response .contents [0 ].text == "Read should-work"
164+
165+ @pytest .mark .anyio
166+ async def test_sse_client_exception_handling (initialized_sse_client_session : ClientSession ):
167+ session = initialized_sse_client_session
168+ with pytest .raises (McpError , match = "OOPS! no resource with that URI was found" ):
169+ await session .read_resource (uri = AnyUrl ("xxx://will-not-work" ))
0 commit comments