Drop deprecated streamablehttp_client (#1836)
This commit is contained in:
parent
b26e5b907f
commit
8893b022e8
3 changed files with 31 additions and 223 deletions
|
|
@ -4,8 +4,7 @@ SessionGroup concurrently manages multiple MCP session connections.
|
||||||
Tools, resources, and prompts are aggregated across servers. Servers may
|
Tools, resources, and prompts are aggregated across servers. Servers may
|
||||||
be connected to or disconnected from at any point after initialization.
|
be connected to or disconnected from at any point after initialization.
|
||||||
|
|
||||||
This abstractions can handle naming collisions using a custom user-provided
|
This abstractions can handle naming collisions using a custom user-provided hook.
|
||||||
hook.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,20 @@
|
||||||
"""
|
"""Implements StreamableHTTP transport for MCP clients."""
|
||||||
StreamableHTTP Client Transport Module
|
|
||||||
|
|
||||||
This module implements the StreamableHTTP transport for MCP clients,
|
from __future__ import annotations as _annotations
|
||||||
providing support for HTTP POST requests with optional SSE streaming responses
|
|
||||||
and session management.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
import logging
|
import logging
|
||||||
from collections.abc import AsyncGenerator, Awaitable, Callable
|
from collections.abc import AsyncGenerator, Awaitable, Callable
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import timedelta
|
|
||||||
from typing import Any, overload
|
|
||||||
from warnings import warn
|
|
||||||
|
|
||||||
import anyio
|
import anyio
|
||||||
import httpx
|
import httpx
|
||||||
from anyio.abc import TaskGroup
|
from anyio.abc import TaskGroup
|
||||||
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
|
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
|
||||||
from httpx_sse import EventSource, ServerSentEvent, aconnect_sse
|
from httpx_sse import EventSource, ServerSentEvent, aconnect_sse
|
||||||
from typing_extensions import deprecated
|
|
||||||
|
|
||||||
from mcp.shared._httpx_utils import (
|
from mcp.shared._httpx_utils import create_mcp_http_client
|
||||||
McpHttpClientFactory,
|
|
||||||
create_mcp_http_client,
|
|
||||||
)
|
|
||||||
from mcp.shared.message import ClientMessageMetadata, SessionMessage
|
from mcp.shared.message import ClientMessageMetadata, SessionMessage
|
||||||
from mcp.types import (
|
from mcp.types import (
|
||||||
ErrorData,
|
ErrorData,
|
||||||
|
|
@ -53,15 +42,6 @@ LAST_EVENT_ID = "last-event-id"
|
||||||
# Reconnection defaults
|
# Reconnection defaults
|
||||||
DEFAULT_RECONNECTION_DELAY_MS = 1000 # 1 second fallback when server doesn't provide retry
|
DEFAULT_RECONNECTION_DELAY_MS = 1000 # 1 second fallback when server doesn't provide retry
|
||||||
MAX_RECONNECTION_ATTEMPTS = 2 # Max retry attempts before giving up
|
MAX_RECONNECTION_ATTEMPTS = 2 # Max retry attempts before giving up
|
||||||
CONTENT_TYPE = "content-type"
|
|
||||||
ACCEPT = "accept"
|
|
||||||
|
|
||||||
|
|
||||||
JSON = "application/json"
|
|
||||||
SSE = "text/event-stream"
|
|
||||||
|
|
||||||
# Sentinel value for detecting unset optional parameters
|
|
||||||
_UNSET = object()
|
|
||||||
|
|
||||||
|
|
||||||
class StreamableHTTPError(Exception):
|
class StreamableHTTPError(Exception):
|
||||||
|
|
@ -81,69 +61,20 @@ class RequestContext:
|
||||||
session_message: SessionMessage
|
session_message: SessionMessage
|
||||||
metadata: ClientMessageMetadata | None
|
metadata: ClientMessageMetadata | None
|
||||||
read_stream_writer: StreamWriter
|
read_stream_writer: StreamWriter
|
||||||
headers: dict[str, str] | None = None # Deprecated - no longer used
|
|
||||||
sse_read_timeout: float | None = None # Deprecated - no longer used
|
|
||||||
|
|
||||||
|
|
||||||
class StreamableHTTPTransport:
|
class StreamableHTTPTransport:
|
||||||
"""StreamableHTTP client transport implementation."""
|
"""StreamableHTTP client transport implementation."""
|
||||||
|
|
||||||
@overload
|
def __init__(self, url: str) -> None:
|
||||||
def __init__(self, url: str) -> None: ...
|
|
||||||
|
|
||||||
@overload
|
|
||||||
@deprecated(
|
|
||||||
"Parameters headers, timeout, sse_read_timeout, and auth are deprecated. "
|
|
||||||
"Configure these on the httpx.AsyncClient instead."
|
|
||||||
)
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
url: str,
|
|
||||||
headers: dict[str, str] | None = None,
|
|
||||||
timeout: float = 30.0,
|
|
||||||
sse_read_timeout: float = 300.0,
|
|
||||||
auth: httpx.Auth | None = None,
|
|
||||||
) -> None: ...
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
url: str,
|
|
||||||
headers: Any = _UNSET,
|
|
||||||
timeout: Any = _UNSET,
|
|
||||||
sse_read_timeout: Any = _UNSET,
|
|
||||||
auth: Any = _UNSET,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize the StreamableHTTP transport.
|
"""Initialize the StreamableHTTP transport.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
url: The endpoint URL.
|
url: The endpoint URL.
|
||||||
headers: Optional headers to include in requests.
|
|
||||||
timeout: HTTP timeout for regular operations (in seconds).
|
|
||||||
sse_read_timeout: Timeout for SSE read operations (in seconds).
|
|
||||||
auth: Optional HTTPX authentication handler.
|
|
||||||
"""
|
"""
|
||||||
# Check for deprecated parameters and issue runtime warning
|
|
||||||
deprecated_params: list[str] = []
|
|
||||||
if headers is not _UNSET:
|
|
||||||
deprecated_params.append("headers")
|
|
||||||
if timeout is not _UNSET:
|
|
||||||
deprecated_params.append("timeout")
|
|
||||||
if sse_read_timeout is not _UNSET:
|
|
||||||
deprecated_params.append("sse_read_timeout")
|
|
||||||
if auth is not _UNSET:
|
|
||||||
deprecated_params.append("auth")
|
|
||||||
|
|
||||||
if deprecated_params:
|
|
||||||
warn(
|
|
||||||
f"Parameters {', '.join(deprecated_params)} are deprecated and will be ignored. "
|
|
||||||
"Configure these on the httpx.AsyncClient instead.",
|
|
||||||
DeprecationWarning,
|
|
||||||
stacklevel=2,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.url = url
|
self.url = url
|
||||||
self.session_id = None
|
self.session_id: str | None = None
|
||||||
self.protocol_version = None
|
self.protocol_version: str | None = None
|
||||||
|
|
||||||
def _prepare_headers(self) -> dict[str, str]:
|
def _prepare_headers(self) -> dict[str, str]:
|
||||||
"""Build MCP-specific request headers.
|
"""Build MCP-specific request headers.
|
||||||
|
|
@ -151,10 +82,10 @@ class StreamableHTTPTransport:
|
||||||
These headers will be merged with the httpx.AsyncClient's default headers,
|
These headers will be merged with the httpx.AsyncClient's default headers,
|
||||||
with these MCP-specific headers taking precedence.
|
with these MCP-specific headers taking precedence.
|
||||||
"""
|
"""
|
||||||
headers: dict[str, str] = {}
|
headers: dict[str, str] = {
|
||||||
# Add MCP protocol headers
|
"accept": "application/json, text/event-stream",
|
||||||
headers[ACCEPT] = f"{JSON}, {SSE}"
|
"content-type": "application/json",
|
||||||
headers[CONTENT_TYPE] = JSON
|
}
|
||||||
# Add session headers if available
|
# Add session headers if available
|
||||||
if self.session_id:
|
if self.session_id:
|
||||||
headers[MCP_SESSION_ID] = self.session_id
|
headers[MCP_SESSION_ID] = self.session_id
|
||||||
|
|
@ -170,20 +101,14 @@ class StreamableHTTPTransport:
|
||||||
"""Check if the message is an initialized notification."""
|
"""Check if the message is an initialized notification."""
|
||||||
return isinstance(message.root, JSONRPCNotification) and message.root.method == "notifications/initialized"
|
return isinstance(message.root, JSONRPCNotification) and message.root.method == "notifications/initialized"
|
||||||
|
|
||||||
def _maybe_extract_session_id_from_response(
|
def _maybe_extract_session_id_from_response(self, response: httpx.Response) -> None:
|
||||||
self,
|
|
||||||
response: httpx.Response,
|
|
||||||
) -> None:
|
|
||||||
"""Extract and store session ID from response headers."""
|
"""Extract and store session ID from response headers."""
|
||||||
new_session_id = response.headers.get(MCP_SESSION_ID)
|
new_session_id = response.headers.get(MCP_SESSION_ID)
|
||||||
if new_session_id:
|
if new_session_id:
|
||||||
self.session_id = new_session_id
|
self.session_id = new_session_id
|
||||||
logger.info(f"Received session ID: {self.session_id}")
|
logger.info(f"Received session ID: {self.session_id}")
|
||||||
|
|
||||||
def _maybe_extract_protocol_version_from_message(
|
def _maybe_extract_protocol_version_from_message(self, message: JSONRPCMessage) -> None:
|
||||||
self,
|
|
||||||
message: JSONRPCMessage,
|
|
||||||
) -> None:
|
|
||||||
"""Extract protocol version from initialization response message."""
|
"""Extract protocol version from initialization response message."""
|
||||||
if isinstance(message.root, JSONRPCResponse) and message.root.result: # pragma: no branch
|
if isinstance(message.root, JSONRPCResponse) and message.root.result: # pragma: no branch
|
||||||
try:
|
try:
|
||||||
|
|
@ -191,10 +116,8 @@ class StreamableHTTPTransport:
|
||||||
init_result = InitializeResult.model_validate(message.root.result)
|
init_result = InitializeResult.model_validate(message.root.result)
|
||||||
self.protocol_version = str(init_result.protocolVersion)
|
self.protocol_version = str(init_result.protocolVersion)
|
||||||
logger.info(f"Negotiated protocol version: {self.protocol_version}")
|
logger.info(f"Negotiated protocol version: {self.protocol_version}")
|
||||||
except Exception as exc: # pragma: no cover
|
except Exception: # pragma: no cover
|
||||||
logger.warning(
|
logger.warning("Failed to parse initialization response as InitializeResult", exc_info=True)
|
||||||
f"Failed to parse initialization response as InitializeResult: {exc}"
|
|
||||||
) # pragma: no cover
|
|
||||||
logger.warning(f"Raw result: {message.root.result}")
|
logger.warning(f"Raw result: {message.root.result}")
|
||||||
|
|
||||||
async def _handle_sse_event(
|
async def _handle_sse_event(
|
||||||
|
|
@ -244,11 +167,7 @@ class StreamableHTTPTransport:
|
||||||
logger.warning(f"Unknown SSE event: {sse.event}")
|
logger.warning(f"Unknown SSE event: {sse.event}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def handle_get_stream(
|
async def handle_get_stream(self, client: httpx.AsyncClient, read_stream_writer: StreamWriter) -> None:
|
||||||
self,
|
|
||||||
client: httpx.AsyncClient,
|
|
||||||
read_stream_writer: StreamWriter,
|
|
||||||
) -> None:
|
|
||||||
"""Handle GET stream for server-initiated messages with auto-reconnect."""
|
"""Handle GET stream for server-initiated messages with auto-reconnect."""
|
||||||
last_event_id: str | None = None
|
last_event_id: str | None = None
|
||||||
retry_interval_ms: int | None = None
|
retry_interval_ms: int | None = None
|
||||||
|
|
@ -263,12 +182,7 @@ class StreamableHTTPTransport:
|
||||||
if last_event_id:
|
if last_event_id:
|
||||||
headers[LAST_EVENT_ID] = last_event_id # pragma: no cover
|
headers[LAST_EVENT_ID] = last_event_id # pragma: no cover
|
||||||
|
|
||||||
async with aconnect_sse(
|
async with aconnect_sse(client, "GET", self.url, headers=headers) as event_source:
|
||||||
client,
|
|
||||||
"GET",
|
|
||||||
self.url,
|
|
||||||
headers=headers,
|
|
||||||
) as event_source:
|
|
||||||
event_source.response.raise_for_status()
|
event_source.response.raise_for_status()
|
||||||
logger.debug("GET SSE connection established")
|
logger.debug("GET SSE connection established")
|
||||||
|
|
||||||
|
|
@ -311,12 +225,7 @@ class StreamableHTTPTransport:
|
||||||
if isinstance(ctx.session_message.message.root, JSONRPCRequest): # pragma: no branch
|
if isinstance(ctx.session_message.message.root, JSONRPCRequest): # pragma: no branch
|
||||||
original_request_id = ctx.session_message.message.root.id
|
original_request_id = ctx.session_message.message.root.id
|
||||||
|
|
||||||
async with aconnect_sse(
|
async with aconnect_sse(ctx.client, "GET", self.url, headers=headers) as event_source:
|
||||||
ctx.client,
|
|
||||||
"GET",
|
|
||||||
self.url,
|
|
||||||
headers=headers,
|
|
||||||
) as event_source:
|
|
||||||
event_source.response.raise_for_status()
|
event_source.response.raise_for_status()
|
||||||
logger.debug("Resumption GET SSE connection established")
|
logger.debug("Resumption GET SSE connection established")
|
||||||
|
|
||||||
|
|
@ -362,10 +271,10 @@ class StreamableHTTPTransport:
|
||||||
# Per https://modelcontextprotocol.io/specification/2025-06-18/basic#notifications:
|
# Per https://modelcontextprotocol.io/specification/2025-06-18/basic#notifications:
|
||||||
# The server MUST NOT send a response to notifications.
|
# The server MUST NOT send a response to notifications.
|
||||||
if isinstance(message.root, JSONRPCRequest):
|
if isinstance(message.root, JSONRPCRequest):
|
||||||
content_type = response.headers.get(CONTENT_TYPE, "").lower()
|
content_type = response.headers.get("content-type", "").lower()
|
||||||
if content_type.startswith(JSON):
|
if content_type.startswith("application/json"):
|
||||||
await self._handle_json_response(response, ctx.read_stream_writer, is_initialization)
|
await self._handle_json_response(response, ctx.read_stream_writer, is_initialization)
|
||||||
elif content_type.startswith(SSE):
|
elif content_type.startswith("text/event-stream"):
|
||||||
await self._handle_sse_response(response, ctx, is_initialization)
|
await self._handle_sse_response(response, ctx, is_initialization)
|
||||||
else:
|
else:
|
||||||
await self._handle_unexpected_content_type( # pragma: no cover
|
await self._handle_unexpected_content_type( # pragma: no cover
|
||||||
|
|
@ -460,12 +369,7 @@ class StreamableHTTPTransport:
|
||||||
original_request_id = ctx.session_message.message.root.id
|
original_request_id = ctx.session_message.message.root.id
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with aconnect_sse(
|
async with aconnect_sse(ctx.client, "GET", self.url, headers=headers) as event_source:
|
||||||
ctx.client,
|
|
||||||
"GET",
|
|
||||||
self.url,
|
|
||||||
headers=headers,
|
|
||||||
) as event_source:
|
|
||||||
event_source.response.raise_for_status()
|
event_source.response.raise_for_status()
|
||||||
logger.info("Reconnected to SSE stream")
|
logger.info("Reconnected to SSE stream")
|
||||||
|
|
||||||
|
|
@ -498,20 +402,14 @@ class StreamableHTTPTransport:
|
||||||
await self._handle_reconnection(ctx, last_event_id, retry_interval_ms, attempt + 1)
|
await self._handle_reconnection(ctx, last_event_id, retry_interval_ms, attempt + 1)
|
||||||
|
|
||||||
async def _handle_unexpected_content_type(
|
async def _handle_unexpected_content_type(
|
||||||
self,
|
self, content_type: str, read_stream_writer: StreamWriter
|
||||||
content_type: str,
|
|
||||||
read_stream_writer: StreamWriter,
|
|
||||||
) -> None: # pragma: no cover
|
) -> None: # pragma: no cover
|
||||||
"""Handle unexpected content type in response."""
|
"""Handle unexpected content type in response."""
|
||||||
error_msg = f"Unexpected content type: {content_type}" # pragma: no cover
|
error_msg = f"Unexpected content type: {content_type}" # pragma: no cover
|
||||||
logger.error(error_msg) # pragma: no cover
|
logger.error(error_msg) # pragma: no cover
|
||||||
await read_stream_writer.send(ValueError(error_msg)) # pragma: no cover
|
await read_stream_writer.send(ValueError(error_msg)) # pragma: no cover
|
||||||
|
|
||||||
async def _send_session_terminated_error(
|
async def _send_session_terminated_error(self, read_stream_writer: StreamWriter, request_id: RequestId) -> None:
|
||||||
self,
|
|
||||||
read_stream_writer: StreamWriter,
|
|
||||||
request_id: RequestId,
|
|
||||||
) -> None:
|
|
||||||
"""Send a session terminated error response."""
|
"""Send a session terminated error response."""
|
||||||
jsonrpc_error = JSONRPCError(
|
jsonrpc_error = JSONRPCError(
|
||||||
jsonrpc="2.0",
|
jsonrpc="2.0",
|
||||||
|
|
@ -619,8 +517,7 @@ async def streamable_http_client(
|
||||||
http_client: Optional pre-configured httpx.AsyncClient. If None, a default
|
http_client: Optional pre-configured httpx.AsyncClient. If None, a default
|
||||||
client with recommended MCP timeouts will be created. To configure headers,
|
client with recommended MCP timeouts will be created. To configure headers,
|
||||||
authentication, or other HTTP settings, create an httpx.AsyncClient and pass it here.
|
authentication, or other HTTP settings, create an httpx.AsyncClient and pass it here.
|
||||||
terminate_on_close: If True, send a DELETE request to terminate the session
|
terminate_on_close: If True, send a DELETE request to terminate the session when the context exits.
|
||||||
when the context exits.
|
|
||||||
|
|
||||||
Yields:
|
Yields:
|
||||||
Tuple containing:
|
Tuple containing:
|
||||||
|
|
@ -667,11 +564,7 @@ async def streamable_http_client(
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
yield (
|
yield (read_stream, write_stream, transport.get_session_id)
|
||||||
read_stream,
|
|
||||||
write_stream,
|
|
||||||
transport.get_session_id,
|
|
||||||
)
|
|
||||||
finally:
|
finally:
|
||||||
if transport.session_id and terminate_on_close:
|
if transport.session_id and terminate_on_close:
|
||||||
await transport.terminate_session(client)
|
await transport.terminate_session(client)
|
||||||
|
|
@ -679,44 +572,3 @@ async def streamable_http_client(
|
||||||
finally:
|
finally:
|
||||||
await read_stream_writer.aclose()
|
await read_stream_writer.aclose()
|
||||||
await write_stream.aclose()
|
await write_stream.aclose()
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
|
||||||
@deprecated("Use `streamable_http_client` instead.")
|
|
||||||
async def streamablehttp_client(
|
|
||||||
url: str,
|
|
||||||
headers: dict[str, str] | None = None,
|
|
||||||
timeout: float | timedelta = 30,
|
|
||||||
sse_read_timeout: float | timedelta = 60 * 5,
|
|
||||||
terminate_on_close: bool = True,
|
|
||||||
httpx_client_factory: McpHttpClientFactory = create_mcp_http_client,
|
|
||||||
auth: httpx.Auth | None = None,
|
|
||||||
) -> AsyncGenerator[
|
|
||||||
tuple[
|
|
||||||
MemoryObjectReceiveStream[SessionMessage | Exception],
|
|
||||||
MemoryObjectSendStream[SessionMessage],
|
|
||||||
GetSessionIdCallback,
|
|
||||||
],
|
|
||||||
None,
|
|
||||||
]:
|
|
||||||
# Convert timeout parameters
|
|
||||||
timeout_seconds = timeout.total_seconds() if isinstance(timeout, timedelta) else timeout
|
|
||||||
sse_read_timeout_seconds = (
|
|
||||||
sse_read_timeout.total_seconds() if isinstance(sse_read_timeout, timedelta) else sse_read_timeout
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create httpx client using the factory with old-style parameters
|
|
||||||
client = httpx_client_factory(
|
|
||||||
headers=headers,
|
|
||||||
timeout=httpx.Timeout(timeout_seconds, read=sse_read_timeout_seconds),
|
|
||||||
auth=auth,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Manage client lifecycle since we created it
|
|
||||||
async with client:
|
|
||||||
async with streamable_http_client(
|
|
||||||
url,
|
|
||||||
http_client=client,
|
|
||||||
terminate_on_close=terminate_on_close,
|
|
||||||
) as streams:
|
|
||||||
yield streams
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ Tests for the StreamableHTTP server and client transport.
|
||||||
Contains tests for both server and client sides of the StreamableHTTP transport.
|
Contains tests for both server and client sides of the StreamableHTTP transport.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations as _annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
import socket
|
import socket
|
||||||
|
|
@ -25,11 +27,7 @@ from starlette.routing import Mount
|
||||||
|
|
||||||
import mcp.types as types
|
import mcp.types as types
|
||||||
from mcp.client.session import ClientSession
|
from mcp.client.session import ClientSession
|
||||||
from mcp.client.streamable_http import (
|
from mcp.client.streamable_http import StreamableHTTPTransport, streamable_http_client
|
||||||
StreamableHTTPTransport,
|
|
||||||
streamable_http_client,
|
|
||||||
streamablehttp_client, # pyright: ignore[reportDeprecated]
|
|
||||||
)
|
|
||||||
from mcp.server import Server
|
from mcp.server import Server
|
||||||
from mcp.server.streamable_http import (
|
from mcp.server.streamable_http import (
|
||||||
MCP_PROTOCOL_VERSION_HEADER,
|
MCP_PROTOCOL_VERSION_HEADER,
|
||||||
|
|
@ -1728,11 +1726,7 @@ async def test_client_crash_handled(basic_server: None, basic_server_url: str):
|
||||||
# Simulate bad client that crashes after init
|
# Simulate bad client that crashes after init
|
||||||
async def bad_client():
|
async def bad_client():
|
||||||
"""Client that triggers ClosedResourceError"""
|
"""Client that triggers ClosedResourceError"""
|
||||||
async with streamable_http_client(f"{basic_server_url}/mcp") as (
|
async with streamable_http_client(f"{basic_server_url}/mcp") as (read_stream, write_stream, _):
|
||||||
read_stream,
|
|
||||||
write_stream,
|
|
||||||
_,
|
|
||||||
):
|
|
||||||
async with ClientSession(read_stream, write_stream) as session:
|
async with ClientSession(read_stream, write_stream) as session:
|
||||||
await session.initialize()
|
await session.initialize()
|
||||||
raise Exception("client crash")
|
raise Exception("client crash")
|
||||||
|
|
@ -1746,11 +1740,7 @@ async def test_client_crash_handled(basic_server: None, basic_server_url: str):
|
||||||
await anyio.sleep(0.1)
|
await anyio.sleep(0.1)
|
||||||
|
|
||||||
# Try a good client, it should still be able to connect and list tools
|
# Try a good client, it should still be able to connect and list tools
|
||||||
async with streamable_http_client(f"{basic_server_url}/mcp") as (
|
async with streamable_http_client(f"{basic_server_url}/mcp") as (read_stream, write_stream, _):
|
||||||
read_stream,
|
|
||||||
write_stream,
|
|
||||||
_,
|
|
||||||
):
|
|
||||||
async with ClientSession(read_stream, write_stream) as session:
|
async with ClientSession(read_stream, write_stream) as session:
|
||||||
result = await session.initialize()
|
result = await session.initialize()
|
||||||
assert isinstance(result, InitializeResult)
|
assert isinstance(result, InitializeResult)
|
||||||
|
|
@ -2360,36 +2350,3 @@ async def test_streamable_http_client_preserves_custom_with_mcp_headers(
|
||||||
|
|
||||||
assert "content-type" in headers_data
|
assert "content-type" in headers_data
|
||||||
assert headers_data["content-type"] == "application/json"
|
assert headers_data["content-type"] == "application/json"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
|
||||||
async def test_streamable_http_transport_deprecated_params_ignored(basic_server: None, basic_server_url: str) -> None:
|
|
||||||
"""Test that deprecated parameters passed to StreamableHTTPTransport are properly ignored."""
|
|
||||||
with pytest.warns(DeprecationWarning):
|
|
||||||
transport = StreamableHTTPTransport( # pyright: ignore[reportDeprecated]
|
|
||||||
url=f"{basic_server_url}/mcp",
|
|
||||||
headers={"X-Should-Be-Ignored": "ignored"},
|
|
||||||
timeout=999.0,
|
|
||||||
sse_read_timeout=999.0,
|
|
||||||
auth=None,
|
|
||||||
)
|
|
||||||
|
|
||||||
headers = transport._prepare_headers()
|
|
||||||
assert "X-Should-Be-Ignored" not in headers
|
|
||||||
assert headers["accept"] == "application/json, text/event-stream"
|
|
||||||
assert headers["content-type"] == "application/json"
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
|
||||||
async def test_streamablehttp_client_deprecation_warning(basic_server: None, basic_server_url: str) -> None:
|
|
||||||
"""Test that the old streamablehttp_client() function issues a deprecation warning."""
|
|
||||||
with pytest.warns(DeprecationWarning, match="Use `streamable_http_client` instead"):
|
|
||||||
async with streamablehttp_client(f"{basic_server_url}/mcp") as ( # pyright: ignore[reportDeprecated]
|
|
||||||
read_stream,
|
|
||||||
write_stream,
|
|
||||||
_,
|
|
||||||
):
|
|
||||||
async with ClientSession(read_stream, write_stream) as session: # pragma: no branch
|
|
||||||
await session.initialize()
|
|
||||||
tools = await session.list_tools()
|
|
||||||
assert len(tools.tools) > 0
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue