Skip to content

Commit f561854

Browse files
committed
Add streamable_http_client and deprecate old usage
1 parent 5441767 commit f561854

File tree

7 files changed

+121
-60
lines changed

7 files changed

+121
-60
lines changed

README.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,20 @@
3030
- [Prompts](#prompts)
3131
- [Images](#images)
3232
- [Context](#context)
33+
- [Authentication](#authentication)
3334
- [Running Your Server](#running-your-server)
3435
- [Development Mode](#development-mode)
3536
- [Claude Desktop Integration](#claude-desktop-integration)
3637
- [Direct Execution](#direct-execution)
38+
- [Streamable HTTP Transport](#streamable-http-transport)
3739
- [Mounting to an Existing ASGI Server](#mounting-to-an-existing-asgi-server)
3840
- [Examples](#examples)
3941
- [Echo Server](#echo-server)
4042
- [SQLite Explorer](#sqlite-explorer)
4143
- [Advanced Usage](#advanced-usage)
4244
- [Low-Level Server](#low-level-server)
4345
- [Writing MCP Clients](#writing-mcp-clients)
46+
- [OAuth Authentication for Clients](#oauth-authentication-for-clients)
4447
- [MCP Primitives](#mcp-primitives)
4548
- [Server Capabilities](#server-capabilities)
4649
- [Documentation](#documentation)
@@ -73,7 +76,7 @@ The Model Context Protocol allows applications to provide context for LLMs in a
7376

7477
### Adding MCP to your python project
7578

76-
We recommend using [uv](https://docs.astral.sh/uv/) to manage your Python projects.
79+
We recommend using [uv](https://docs.astral.sh/uv/) to manage your Python projects.
7780

7881
If you haven't created a uv-managed project yet, create one:
7982

@@ -790,13 +793,13 @@ if __name__ == "__main__":
790793
Clients can also connect using [Streamable HTTP transport](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http):
791794

792795
```python
793-
from mcp.client.streamable_http import streamablehttp_client
796+
from mcp.client.streamable_http import streamable_http_client
794797
from mcp import ClientSession
795798

796799

797800
async def main():
798801
# Connect to a streamable HTTP server
799-
async with streamablehttp_client("example/mcp") as (
802+
async with streamable_http_client("example/mcp") as (
800803
read_stream,
801804
write_stream,
802805
_,
@@ -816,7 +819,7 @@ The SDK includes [authorization support](https://modelcontextprotocol.io/specifi
816819
```python
817820
from mcp.client.auth import OAuthClientProvider, TokenStorage
818821
from mcp.client.session import ClientSession
819-
from mcp.client.streamable_http import streamablehttp_client
822+
from mcp.client.streamable_http import streamable_http_client
820823
from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthToken
821824

822825

@@ -852,7 +855,7 @@ async def main():
852855
)
853856

854857
# Use with streamable HTTP client
855-
async with streamablehttp_client(
858+
async with streamable_http_client(
856859
"https://api.example.com/mcp", auth=oauth_auth
857860
) as (read, write, _):
858861
async with ClientSession(read, write) as session:

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from mcp.client.auth import OAuthClientProvider, TokenStorage
2020
from mcp.client.session import ClientSession
2121
from mcp.client.sse import sse_client
22-
from mcp.client.streamable_http import streamablehttp_client
22+
from mcp.client.streamable_http import streamable_http_client
2323
from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthToken
2424

2525

@@ -208,7 +208,7 @@ async def _default_redirect_handler(authorization_url: str) -> None:
208208
await self._run_session(read_stream, write_stream, None)
209209
else:
210210
print("📡 Opening StreamableHTTP transport connection with auth...")
211-
async with streamablehttp_client(
211+
async with streamable_http_client(
212212
url=self.server_url,
213213
auth=oauth_auth,
214214
timeout=timedelta(seconds=60),

src/mcp/client/session_group.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from mcp import types
2424
from mcp.client.sse import sse_client
2525
from mcp.client.stdio import StdioServerParameters
26-
from mcp.client.streamable_http import streamablehttp_client
26+
from mcp.client.streamable_http import streamable_http_client
2727
from mcp.shared.exceptions import McpError
2828

2929

@@ -44,7 +44,7 @@ class SseServerParameters(BaseModel):
4444

4545

4646
class StreamableHttpParameters(BaseModel):
47-
"""Parameters for intializing a streamablehttp_client."""
47+
"""Parameters for intializing a streamable_http_client."""
4848

4949
# The endpoint URL.
5050
url: str
@@ -252,11 +252,11 @@ async def _establish_session(
252252
)
253253
read, write = await session_stack.enter_async_context(client)
254254
else:
255-
client = streamablehttp_client(
255+
client = streamable_http_client(
256256
url=server_params.url,
257257
headers=server_params.headers,
258-
timeout=server_params.timeout,
259-
sse_read_timeout=server_params.sse_read_timeout,
258+
timeout=server_params.timeout.total_seconds(),
259+
sse_read_timeout=server_params.sse_read_timeout.total_seconds(),
260260
terminate_on_close=server_params.terminate_on_close,
261261
)
262262
read, write, _ = await session_stack.enter_async_context(client)

src/mcp/client/streamable_http.py

Lines changed: 71 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"""
88

99
import logging
10+
import warnings
1011
from collections.abc import AsyncGenerator, Awaitable, Callable
1112
from contextlib import asynccontextmanager
1213
from dataclasses import dataclass
@@ -71,7 +72,7 @@ class RequestContext:
7172
session_message: SessionMessage
7273
metadata: ClientMessageMetadata | None
7374
read_stream_writer: StreamWriter
74-
sse_read_timeout: timedelta
75+
sse_read_timeout: float
7576

7677

7778
class StreamableHTTPTransport:
@@ -81,8 +82,8 @@ def __init__(
8182
self,
8283
url: str,
8384
headers: dict[str, Any] | None = None,
84-
timeout: timedelta = timedelta(seconds=30),
85-
sse_read_timeout: timedelta = timedelta(seconds=60 * 5),
85+
timeout: float | timedelta = 30,
86+
sse_read_timeout: float | timedelta = 60 * 5,
8687
auth: httpx.Auth | None = None,
8788
) -> None:
8889
"""Initialize the StreamableHTTP transport.
@@ -96,8 +97,25 @@ def __init__(
9697
"""
9798
self.url = url
9899
self.headers = headers or {}
100+
101+
if isinstance(timeout, timedelta):
102+
warnings.warn(
103+
"`timeout` as `timedelta` is deprecated. Use `float` instead.",
104+
DeprecationWarning,
105+
stacklevel=2,
106+
)
107+
timeout = timeout.total_seconds()
99108
self.timeout = timeout
109+
110+
if isinstance(sse_read_timeout, timedelta):
111+
warnings.warn(
112+
"`sse_read_timeout` as `timedelta` is deprecated. Use `float` instead.",
113+
DeprecationWarning,
114+
stacklevel=2,
115+
)
116+
sse_read_timeout = sse_read_timeout.total_seconds()
100117
self.sse_read_timeout = sse_read_timeout
118+
101119
self.auth = auth
102120
self.session_id: str | None = None
103121
self.request_headers = {
@@ -194,9 +212,7 @@ async def handle_get_stream(
194212
"GET",
195213
self.url,
196214
headers=headers,
197-
timeout=httpx.Timeout(
198-
self.timeout.seconds, read=self.sse_read_timeout.seconds
199-
),
215+
timeout=httpx.Timeout(self.timeout, read=self.sse_read_timeout),
200216
) as event_source:
201217
event_source.response.raise_for_status()
202218
logger.debug("GET SSE connection established")
@@ -225,9 +241,7 @@ async def _handle_resumption_request(self, ctx: RequestContext) -> None:
225241
"GET",
226242
self.url,
227243
headers=headers,
228-
timeout=httpx.Timeout(
229-
self.timeout.seconds, read=ctx.sse_read_timeout.seconds
230-
),
244+
timeout=httpx.Timeout(self.timeout, read=ctx.sse_read_timeout),
231245
) as event_source:
232246
event_source.response.raise_for_status()
233247
logger.debug("Resumption GET SSE connection established")
@@ -446,6 +460,52 @@ async def streamablehttp_client(
446460
`sse_read_timeout` determines how long (in seconds) the client will wait for a new
447461
event before disconnecting. All other HTTP operations are controlled by `timeout`.
448462
463+
Yields:
464+
Tuple containing:
465+
- read_stream: Stream for reading messages from the server
466+
- write_stream: Stream for sending messages to the server
467+
- get_session_id_callback: Function to retrieve the current session ID
468+
"""
469+
warnings.warn(
470+
"`streamablehttp_client` is deprecated. Use `streamable_http_client` instead.",
471+
DeprecationWarning,
472+
stacklevel=2,
473+
)
474+
async with streamable_http_client(
475+
url,
476+
headers,
477+
timeout.total_seconds(),
478+
sse_read_timeout.total_seconds(),
479+
terminate_on_close,
480+
httpx_client_factory,
481+
auth,
482+
) as (read_stream, write_stream, get_session_id):
483+
yield (read_stream, write_stream, get_session_id)
484+
485+
486+
@asynccontextmanager
487+
async def streamable_http_client(
488+
url: str,
489+
headers: dict[str, Any] | None = None,
490+
timeout: float = 30,
491+
sse_read_timeout: float = 60 * 5,
492+
terminate_on_close: bool = True,
493+
httpx_client_factory: McpHttpClientFactory = create_mcp_http_client,
494+
auth: httpx.Auth | None = None,
495+
) -> AsyncGenerator[
496+
tuple[
497+
MemoryObjectReceiveStream[SessionMessage | Exception],
498+
MemoryObjectSendStream[SessionMessage],
499+
GetSessionIdCallback,
500+
],
501+
None,
502+
]:
503+
"""
504+
Client transport for StreamableHTTP.
505+
506+
`sse_read_timeout` determines how long (in seconds) the client will wait for a new
507+
event before disconnecting. All other HTTP operations are controlled by `timeout`.
508+
449509
Yields:
450510
Tuple containing:
451511
- read_stream: Stream for reading messages from the server
@@ -468,7 +528,7 @@ async def streamablehttp_client(
468528
async with httpx_client_factory(
469529
headers=transport.request_headers,
470530
timeout=httpx.Timeout(
471-
transport.timeout.seconds, read=transport.sse_read_timeout.seconds
531+
transport.timeout, read=transport.sse_read_timeout
472532
),
473533
auth=transport.auth,
474534
) as client:
@@ -489,11 +549,7 @@ def start_get_stream() -> None:
489549
)
490550

491551
try:
492-
yield (
493-
read_stream,
494-
write_stream,
495-
transport.get_session_id,
496-
)
552+
yield (read_stream, write_stream, transport.get_session_id)
497553
finally:
498554
if transport.session_id and terminate_on_close:
499555
await transport.terminate_session(client)

tests/client/test_session_group.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ async def test_disconnect_non_existent_server(self):
296296
url="http://test.com/stream", terminate_on_close=False
297297
),
298298
"streamablehttp",
299-
"mcp.client.session_group.streamablehttp_client",
299+
"mcp.client.session_group.streamable_http_client",
300300
), # url, headers, timeout, sse_read_timeout, terminate_on_close
301301
],
302302
)
@@ -316,7 +316,7 @@ async def test_establish_session_parameterized(
316316
mock_read_stream = mock.AsyncMock(name=f"{client_type_name}Read")
317317
mock_write_stream = mock.AsyncMock(name=f"{client_type_name}Write")
318318

319-
# streamablehttp_client's __aenter__ returns three values
319+
# streamable_http_client's __aenter__ returns three values
320320
if client_type_name == "streamablehttp":
321321
mock_extra_stream_val = mock.AsyncMock(name="StreamableExtra")
322322
mock_client_cm_instance.__aenter__.return_value = (

tests/server/fastmcp/test_integration.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
import mcp.types as types
2222
from mcp.client.session import ClientSession
2323
from mcp.client.sse import sse_client
24-
from mcp.client.streamable_http import streamablehttp_client
24+
from mcp.client.streamable_http import streamable_http_client
2525
from mcp.server.fastmcp import FastMCP
2626
from mcp.server.fastmcp.resources import FunctionResource
2727
from mcp.shared.context import RequestContext
@@ -464,7 +464,7 @@ async def test_fastmcp_streamable_http(
464464
) -> None:
465465
"""Test that FastMCP works with StreamableHTTP transport."""
466466
# Connect to the server using StreamableHTTP
467-
async with streamablehttp_client(http_server_url + "/mcp") as (
467+
async with streamable_http_client(http_server_url + "/mcp") as (
468468
read_stream,
469469
write_stream,
470470
_,
@@ -489,7 +489,7 @@ async def test_fastmcp_stateless_streamable_http(
489489
) -> None:
490490
"""Test that FastMCP works with stateless StreamableHTTP transport."""
491491
# Connect to the server using StreamableHTTP
492-
async with streamablehttp_client(stateless_http_server_url + "/mcp") as (
492+
async with streamable_http_client(stateless_http_server_url + "/mcp") as (
493493
read_stream,
494494
write_stream,
495495
_,
@@ -909,7 +909,7 @@ async def test_fastmcp_all_features_streamable_http(
909909
collector = NotificationCollector()
910910

911911
# Connect to the server using StreamableHTTP
912-
async with streamablehttp_client(everything_http_server_url + "/mcp") as (
912+
async with streamable_http_client(everything_http_server_url + "/mcp") as (
913913
read_stream,
914914
write_stream,
915915
_,

0 commit comments

Comments
 (0)