Skip to content

Commit ac1aa38

Browse files
authored
Merge branch 'main' into main
2 parents 8bc6394 + f2f4dbd commit ac1aa38

File tree

24 files changed

+528
-133
lines changed

24 files changed

+528
-133
lines changed

examples/clients/simple-auth-client/README.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
# Simple Auth Client Example
22

3-
A demonstration of how to use the MCP Python SDK with OAuth authentication over streamable HTTP transport.
3+
A demonstration of how to use the MCP Python SDK with OAuth authentication over streamable HTTP or SSE transport.
44

55
## Features
66

77
- OAuth 2.0 authentication with PKCE
8-
- Streamable HTTP transport
8+
- Support for both StreamableHTTP and SSE transports
99
- Interactive command-line interface
1010

1111
## Installation
@@ -31,7 +31,10 @@ uv run mcp-simple-auth --transport streamable-http --port 3001
3131
uv run mcp-simple-auth-client
3232

3333
# Or with custom server URL
34-
MCP_SERVER_URL=http://localhost:3001 uv run mcp-simple-auth-client
34+
MCP_SERVER_PORT=3001 uv run mcp-simple-auth-client
35+
36+
# Use SSE transport
37+
MCP_TRANSPORT_TYPE=sse uv run mcp-simple-auth-client
3538
```
3639

3740
### 3. Complete OAuth flow
@@ -67,4 +70,5 @@ mcp> quit
6770

6871
## Configuration
6972

70-
- `MCP_SERVER_URL` - Server URL (default: http://localhost:3001)
73+
- `MCP_SERVER_PORT` - Server URL (default: 8000)
74+
- `MCP_TRANSPORT_TYPE` - Transport type: `streamable_http` (default) or `sse`

examples/clients/simple-auth-client/mcp_simple_auth_client/main.py

Lines changed: 47 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
from mcp.client.auth import OAuthClientProvider, TokenStorage
2020
from mcp.client.session import ClientSession
21+
from mcp.client.sse import sse_client
2122
from mcp.client.streamable_http import streamablehttp_client
2223
from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthToken
2324

@@ -149,8 +150,9 @@ def get_state(self):
149150
class SimpleAuthClient:
150151
"""Simple MCP client with auth support."""
151152

152-
def __init__(self, server_url: str):
153+
def __init__(self, server_url: str, transport_type: str = "streamable_http"):
153154
self.server_url = server_url
155+
self.transport_type = transport_type
154156
self.session: ClientSession | None = None
155157

156158
async def connect(self):
@@ -195,38 +197,48 @@ async def _default_redirect_handler(authorization_url: str) -> None:
195197
callback_handler=callback_handler,
196198
)
197199

198-
# Create streamable HTTP transport with auth handler
199-
stream_context = streamablehttp_client(
200-
url=self.server_url,
201-
auth=oauth_auth,
202-
timeout=timedelta(seconds=60),
203-
)
204-
205-
print(
206-
"📡 Opening transport connection (HTTPX handles auth automatically)..."
207-
)
208-
async with stream_context as (read_stream, write_stream, get_session_id):
209-
print("🤝 Initializing MCP session...")
210-
async with ClientSession(read_stream, write_stream) as session:
211-
self.session = session
212-
print("⚡ Starting session initialization...")
213-
await session.initialize()
214-
print("✨ Session initialization complete!")
215-
216-
print(f"\n✅ Connected to MCP server at {self.server_url}")
217-
session_id = get_session_id()
218-
if session_id:
219-
print(f"Session ID: {session_id}")
220-
221-
# Run interactive loop
222-
await self.interactive_loop()
200+
# Create transport with auth handler based on transport type
201+
if self.transport_type == "sse":
202+
print("📡 Opening SSE transport connection with auth...")
203+
async with sse_client(
204+
url=self.server_url,
205+
auth=oauth_auth,
206+
timeout=60,
207+
) as (read_stream, write_stream):
208+
await self._run_session(read_stream, write_stream, None)
209+
else:
210+
print("📡 Opening StreamableHTTP transport connection with auth...")
211+
async with streamablehttp_client(
212+
url=self.server_url,
213+
auth=oauth_auth,
214+
timeout=timedelta(seconds=60),
215+
) as (read_stream, write_stream, get_session_id):
216+
await self._run_session(read_stream, write_stream, get_session_id)
223217

224218
except Exception as e:
225219
print(f"❌ Failed to connect: {e}")
226220
import traceback
227221

228222
traceback.print_exc()
229223

224+
async def _run_session(self, read_stream, write_stream, get_session_id):
225+
"""Run the MCP session with the given streams."""
226+
print("🤝 Initializing MCP session...")
227+
async with ClientSession(read_stream, write_stream) as session:
228+
self.session = session
229+
print("⚡ Starting session initialization...")
230+
await session.initialize()
231+
print("✨ Session initialization complete!")
232+
233+
print(f"\n✅ Connected to MCP server at {self.server_url}")
234+
if get_session_id:
235+
session_id = get_session_id()
236+
if session_id:
237+
print(f"Session ID: {session_id}")
238+
239+
# Run interactive loop
240+
await self.interactive_loop()
241+
230242
async def list_tools(self):
231243
"""List available tools from the server."""
232244
if not self.session:
@@ -326,13 +338,20 @@ async def main():
326338
"""Main entry point."""
327339
# Default server URL - can be overridden with environment variable
328340
# Most MCP streamable HTTP servers use /mcp as the endpoint
329-
server_url = os.getenv("MCP_SERVER_URL", "http://localhost:8000/mcp")
341+
server_url = os.getenv("MCP_SERVER_PORT", 8000)
342+
transport_type = os.getenv("MCP_TRANSPORT_TYPE", "streamable_http")
343+
server_url = (
344+
f"http://localhost:{server_url}/mcp"
345+
if transport_type == "streamable_http"
346+
else f"http://localhost:{server_url}/sse"
347+
)
330348

331349
print("🚀 Simple MCP Auth Client")
332350
print(f"Connecting to: {server_url}")
351+
print(f"Transport type: {transport_type}")
333352

334353
# Start connection flow - OAuth will be handled automatically
335-
client = SimpleAuthClient(server_url)
354+
client = SimpleAuthClient(server_url, transport_type)
336355
await client.connect()
337356

338357

examples/clients/simple-chatbot/mcp_simple_chatbot/main.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ def get_response(self, messages: list[dict[str, str]]) -> str:
245245
}
246246
payload = {
247247
"messages": messages,
248-
"model": "llama-3.2-90b-vision-preview",
248+
"model": "meta-llama/llama-4-scout-17b-16e-instruct",
249249
"temperature": 0.7,
250250
"max_tokens": 4096,
251251
"top_p": 1,
@@ -284,12 +284,9 @@ def __init__(self, servers: list[Server], llm_client: LLMClient) -> None:
284284

285285
async def cleanup_servers(self) -> None:
286286
"""Clean up all servers properly."""
287-
cleanup_tasks = [
288-
asyncio.create_task(server.cleanup()) for server in self.servers
289-
]
290-
if cleanup_tasks:
287+
for server in reversed(self.servers):
291288
try:
292-
await asyncio.gather(*cleanup_tasks, return_exceptions=True)
289+
await server.cleanup()
293290
except Exception as e:
294291
logging.warning(f"Warning during final cleanup: {e}")
295292

examples/servers/simple-prompt/mcp_simple_prompt/server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ async def handle_sse(request):
114114

115115
import uvicorn
116116

117-
uvicorn.run(starlette_app, host="0.0.0.0", port=port)
117+
uvicorn.run(starlette_app, host="127.0.0.1", port=port)
118118
else:
119119
from mcp.server.stdio import stdio_server
120120

examples/servers/simple-resource/mcp_simple_resource/server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ async def handle_sse(request):
7272

7373
import uvicorn
7474

75-
uvicorn.run(starlette_app, host="0.0.0.0", port=port)
75+
uvicorn.run(starlette_app, host="127.0.0.1", port=port)
7676
else:
7777
from mcp.server.stdio import stdio_server
7878

examples/servers/simple-streamablehttp-stateless/mcp_simple_streamablehttp_stateless/server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,6 @@ async def lifespan(app: Starlette) -> AsyncIterator[None]:
136136

137137
import uvicorn
138138

139-
uvicorn.run(starlette_app, host="0.0.0.0", port=port)
139+
uvicorn.run(starlette_app, host="127.0.0.1", port=port)
140140

141141
return 0

examples/servers/simple-streamablehttp/mcp_simple_streamablehttp/server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,6 @@ async def lifespan(app: Starlette) -> AsyncIterator[None]:
164164

165165
import uvicorn
166166

167-
uvicorn.run(starlette_app, host="0.0.0.0", port=port)
167+
uvicorn.run(starlette_app, host="127.0.0.1", port=port)
168168

169169
return 0

examples/servers/simple-tool/mcp_simple_tool/server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ async def handle_sse(request):
8484

8585
import uvicorn
8686

87-
uvicorn.run(starlette_app, host="0.0.0.0", port=port)
87+
uvicorn.run(starlette_app, host="127.0.0.1", port=port)
8888
else:
8989
from mcp.server.stdio import stdio_server
9090

pyproject.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,13 @@ members = ["examples/servers/*"]
109109
mcp = { workspace = true }
110110

111111
[tool.pytest.ini_options]
112+
log_cli = true
112113
xfail_strict = true
114+
addopts = """
115+
--color=yes
116+
--capture=fd
117+
--numprocesses auto
118+
"""
113119
filterwarnings = [
114120
"error",
115121
# This should be fixed on Uvicorn's side.

src/mcp/client/session.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,9 @@ async def list_resources(
209209
types.ClientRequest(
210210
types.ListResourcesRequest(
211211
method="resources/list",
212-
cursor=cursor,
212+
params=types.PaginatedRequestParams(cursor=cursor)
213+
if cursor is not None
214+
else None,
213215
)
214216
),
215217
types.ListResourcesResult,
@@ -223,7 +225,9 @@ async def list_resource_templates(
223225
types.ClientRequest(
224226
types.ListResourceTemplatesRequest(
225227
method="resources/templates/list",
226-
cursor=cursor,
228+
params=types.PaginatedRequestParams(cursor=cursor)
229+
if cursor is not None
230+
else None,
227231
)
228232
),
229233
types.ListResourceTemplatesResult,
@@ -295,7 +299,9 @@ async def list_prompts(self, cursor: str | None = None) -> types.ListPromptsResu
295299
types.ClientRequest(
296300
types.ListPromptsRequest(
297301
method="prompts/list",
298-
cursor=cursor,
302+
params=types.PaginatedRequestParams(cursor=cursor)
303+
if cursor is not None
304+
else None,
299305
)
300306
),
301307
types.ListPromptsResult,
@@ -340,7 +346,9 @@ async def list_tools(self, cursor: str | None = None) -> types.ListToolsResult:
340346
types.ClientRequest(
341347
types.ListToolsRequest(
342348
method="tools/list",
343-
cursor=cursor,
349+
params=types.PaginatedRequestParams(cursor=cursor)
350+
if cursor is not None
351+
else None,
344352
)
345353
),
346354
types.ListToolsResult,

src/mcp/client/session_group.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,10 @@ class ClientSessionGroup:
7777
the client and can be accessed via the session.
7878
7979
Example Usage:
80-
name_fn = lambda name, server_info: f"{(server_info.name)}-{name}"
80+
name_fn = lambda name, server_info: f"{(server_info.name)}_{name}"
8181
async with ClientSessionGroup(component_name_hook=name_fn) as group:
8282
for server_params in server_params:
83-
group.connect_to_server(server_param)
83+
await group.connect_to_server(server_param)
8484
...
8585
8686
"""
@@ -145,14 +145,15 @@ async def __aexit__(
145145
) -> bool | None:
146146
"""Closes session exit stacks and main exit stack upon completion."""
147147

148+
# Only close the main exit stack if we created it
149+
if self._owns_exit_stack:
150+
await self._exit_stack.aclose()
151+
148152
# Concurrently close session stacks.
149153
async with anyio.create_task_group() as tg:
150154
for exit_stack in self._session_exit_stacks.values():
151155
tg.start_soon(exit_stack.aclose)
152156

153-
# Only close the main exit stack if we created it
154-
if self._owns_exit_stack:
155-
await self._exit_stack.aclose()
156157

157158
@property
158159
def sessions(self) -> list[mcp.ClientSession]:

src/mcp/client/sse.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from httpx_sse import aconnect_sse
1111

1212
import mcp.types as types
13-
from mcp.shared._httpx_utils import create_mcp_http_client
13+
from mcp.shared._httpx_utils import McpHttpClientFactory, create_mcp_http_client
1414
from mcp.shared.message import SessionMessage
1515

1616
logger = logging.getLogger(__name__)
@@ -26,12 +26,21 @@ async def sse_client(
2626
headers: dict[str, Any] | None = None,
2727
timeout: float = 5,
2828
sse_read_timeout: float = 60 * 5,
29+
httpx_client_factory: McpHttpClientFactory = create_mcp_http_client,
30+
auth: httpx.Auth | None = None,
2931
):
3032
"""
3133
Client transport for SSE.
3234
3335
`sse_read_timeout` determines how long (in seconds) the client will wait for a new
3436
event before disconnecting. All other HTTP operations are controlled by `timeout`.
37+
38+
Args:
39+
url: The SSE endpoint URL.
40+
headers: Optional headers to include in requests.
41+
timeout: HTTP timeout for regular operations.
42+
sse_read_timeout: Timeout for SSE read operations.
43+
auth: Optional HTTPX authentication handler.
3544
"""
3645
read_stream: MemoryObjectReceiveStream[SessionMessage | Exception]
3746
read_stream_writer: MemoryObjectSendStream[SessionMessage | Exception]
@@ -45,7 +54,7 @@ async def sse_client(
4554
async with anyio.create_task_group() as tg:
4655
try:
4756
logger.info(f"Connecting to SSE endpoint: {remove_request_params(url)}")
48-
async with create_mcp_http_client(headers=headers) as client:
57+
async with httpx_client_factory(headers=headers, auth=auth) as client:
4958
async with aconnect_sse(
5059
client,
5160
"GET",

src/mcp/client/streamable_http.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
2020
from httpx_sse import EventSource, ServerSentEvent, aconnect_sse
2121

22-
from mcp.shared._httpx_utils import create_mcp_http_client
22+
from mcp.shared._httpx_utils import McpHttpClientFactory, create_mcp_http_client
2323
from mcp.shared.message import ClientMessageMetadata, SessionMessage
2424
from mcp.types import (
2525
ErrorData,
@@ -430,6 +430,7 @@ async def streamablehttp_client(
430430
timeout: timedelta = timedelta(seconds=30),
431431
sse_read_timeout: timedelta = timedelta(seconds=60 * 5),
432432
terminate_on_close: bool = True,
433+
httpx_client_factory: McpHttpClientFactory = create_mcp_http_client,
433434
auth: httpx.Auth | None = None,
434435
) -> AsyncGenerator[
435436
tuple[
@@ -464,7 +465,7 @@ async def streamablehttp_client(
464465
try:
465466
logger.info(f"Connecting to StreamableHTTP endpoint: {url}")
466467

467-
async with create_mcp_http_client(
468+
async with httpx_client_factory(
468469
headers=transport.request_headers,
469470
timeout=httpx.Timeout(
470471
transport.timeout.seconds, read=transport.sse_read_timeout.seconds

0 commit comments

Comments
 (0)