3
3
import time
4
4
import json
5
5
import anyio
6
+ import threading
7
+ import uvicorn
8
+ import pytest
6
9
from pydantic import AnyUrl
7
10
from pydantic_core import Url
8
11
import pytest
11
14
from starlette .applications import Starlette
12
15
from starlette .routing import Mount , Route
13
16
17
+ from mcp .shared .exceptions import McpError
14
18
from mcp .client .session import ClientSession
15
19
from mcp .client .sse import sse_client
16
20
from mcp .server import Server
17
21
from 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
22
23
23
24
SERVER_NAME = "test_server_for_SSE"
24
25
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
+
25
40
# Test server implementation
26
41
class TestServer (Server ):
27
42
def __init__ (self ):
@@ -32,7 +47,7 @@ async def handle_read_resource(uri: AnyUrl) -> str | bytes:
32
47
if uri .scheme == "foobar" :
33
48
return f"Read { uri .host } "
34
49
# 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" ))
36
51
37
52
@self .list_tools ()
38
53
async def handle_list_tools ():
@@ -48,9 +63,6 @@ async def handle_list_tools():
48
63
async def handle_call_tool (name : str , args : dict ):
49
64
return [TextContent (type = "text" , text = f"Called { name } " )]
50
65
51
- import threading
52
- import uvicorn
53
- import pytest
54
66
55
67
56
68
# Test fixtures
@@ -78,10 +90,10 @@ async def handle_sse(request):
78
90
return app
79
91
80
92
@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" ))
83
95
server_thread = threading .Thread ( target = server .run , daemon = True )
84
- print ('starting server' )
96
+ print (f 'starting server on { server_port } ' )
85
97
server_thread .start ()
86
98
# Give server time to start
87
99
while not server .started :
@@ -92,9 +104,9 @@ def server(server_app: Starlette):
92
104
server_thread .join (timeout = 0.1 )
93
105
94
106
@pytest .fixture ()
95
- async def http_client (server ) -> AsyncGenerator [httpx .AsyncClient , None ]:
107
+ async def http_client (server , server_url ) -> AsyncGenerator [httpx .AsyncClient , None ]:
96
108
"""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 :
98
110
yield client
99
111
100
112
# Tests
@@ -123,8 +135,8 @@ async def connection_test():
123
135
124
136
125
137
@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 :
128
140
async with ClientSession (* streams ) as session :
129
141
# Test initialization
130
142
result = await session .initialize ()
@@ -136,18 +148,22 @@ async def test_sse_client_basic_connection(server):
136
148
assert isinstance (ping_result , EmptyResult )
137
149
138
150
@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 :
141
153
async with ClientSession (* streams ) as session :
142
154
await session .initialize ()
143
155
yield session
144
156
145
157
@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 ):
147
159
session = initialized_sse_client_session
148
- # TODO: expect raise
149
- await session .read_resource (uri = AnyUrl ("xxx://will-not-work" ))
150
160
response = await session .read_resource (uri = AnyUrl ("foobar://should-work" ))
151
161
assert len (response .contents ) == 1
152
162
assert isinstance (response .contents [0 ], TextResourceContents )
153
163
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