Skip to content

Rename MCPServer sse_read_timeout to read_timeout and pass to ClientSession #2240

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 22 commits into from
Jul 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a96d914
pass sse_read_timeout to MCP ClientSession read_timeout_seconds
AntSan813 Jul 17, 2025
18f24b9
add read_timeout; deprecate sse_read_timeout
AntSan813 Jul 18, 2025
e959273
add back __post_init__; use self.read_timeout
AntSan813 Jul 18, 2025
e9ea957
fix __init__
AntSan813 Jul 18, 2025
311dca5
Merge branch 'main' into add-read-timeout-seconds
AntSan813 Jul 18, 2025
9baf651
fix _MCPServerHTTP __init__
AntSan813 Jul 18, 2025
49aae3c
fix tests
AntSan813 Jul 18, 2025
00fb00b
Merge branch 'main' into add-read-timeout-seconds
AntSan813 Jul 18, 2025
76550fe
cleanup
AntSan813 Jul 18, 2025
c104958
fix coverage
AntSan813 Jul 18, 2025
59ca95d
Merge branch 'main' into add-read-timeout-seconds
AntSan813 Jul 21, 2025
d041396
Merge branch 'main' into add-read-timeout-seconds
AntSan813 Jul 22, 2025
69f14b3
Merge branch 'main' into add-read-timeout-seconds
AntSan813 Jul 23, 2025
ab4ae98
Merge branch 'main' into add-read-timeout-seconds
AntSan813 Jul 23, 2025
067a6a5
validate empty kwargs
AntSan813 Jul 23, 2025
3199d09
Merge branch 'main' into add-read-timeout-seconds
AntSan813 Jul 23, 2025
5b4b3e7
add __init__ to MCPServerStreamableHTTP
AntSan813 Jul 23, 2025
5318211
Merge branch 'main' into add-read-timeout-seconds
AntSan813 Jul 23, 2025
993a39e
dedupe __init__
AntSan813 Jul 23, 2025
7091a57
Merge branch 'main' into add-read-timeout-seconds
AntSan813 Jul 23, 2025
dadad12
Merge branch 'add-read-timeout-seconds' of https://github.com/AntSan8…
AntSan813 Jul 23, 2025
1585359
add back _utils.validate_empty_kwargs
AntSan813 Jul 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 56 additions & 7 deletions pydantic_ai_slim/pydantic_ai/mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

import base64
import functools
import warnings
from abc import ABC, abstractmethod
from asyncio import Lock
from collections.abc import AsyncIterator, Awaitable, Sequence
from contextlib import AbstractAsyncContextManager, AsyncExitStack, asynccontextmanager
from dataclasses import dataclass, field, replace
from datetime import timedelta
from pathlib import Path
from typing import Any, Callable

Expand Down Expand Up @@ -37,7 +39,7 @@
) from _import_error

# after mcp imports so any import error maps to this file, not _mcp.py
from . import _mcp, exceptions, messages, models
from . import _mcp, _utils, exceptions, messages, models

__all__ = 'MCPServer', 'MCPServerStdio', 'MCPServerHTTP', 'MCPServerSSE', 'MCPServerStreamableHTTP'

Expand All @@ -59,6 +61,7 @@ class MCPServer(AbstractToolset[Any], ABC):
log_level: mcp_types.LoggingLevel | None = None
log_handler: LoggingFnT | None = None
timeout: float = 5
read_timeout: float = 5 * 60
process_tool_call: ProcessToolCallback | None = None
allow_sampling: bool = True
max_retries: int = 1
Expand Down Expand Up @@ -208,6 +211,7 @@ async def __aenter__(self) -> Self:
write_stream=self._write_stream,
sampling_callback=self._sampling_callback if self.allow_sampling else None,
logging_callback=self.log_handler,
read_timeout_seconds=timedelta(seconds=self.read_timeout),
)
self._client = await self._exit_stack.enter_async_context(client)

Expand Down Expand Up @@ -401,7 +405,7 @@ def __repr__(self) -> str:
return f'MCPServerStdio(command={self.command!r}, args={self.args!r}, tool_prefix={self.tool_prefix!r})'


@dataclass
@dataclass(init=False)
class _MCPServerHTTP(MCPServer):
url: str
"""The URL of the endpoint on the MCP server."""
Expand Down Expand Up @@ -438,10 +442,10 @@ class _MCPServerHTTP(MCPServer):
```
"""

sse_read_timeout: float = 5 * 60
"""Maximum time in seconds to wait for new SSE messages before timing out.
read_timeout: float = 5 * 60
"""Maximum time in seconds to wait for new messages before timing out.

This timeout applies to the long-lived SSE connection after it's established.
This timeout applies to the long-lived connection after it's established.
If no new messages are received within this time, the connection will be considered stale
and may be closed. Defaults to 5 minutes (300 seconds).
"""
Expand Down Expand Up @@ -485,6 +489,51 @@ class _MCPServerHTTP(MCPServer):
sampling_model: models.Model | None = None
"""The model to use for sampling."""

def __init__(
self,
*,
url: str,
headers: dict[str, str] | None = None,
http_client: httpx.AsyncClient | None = None,
read_timeout: float | None = None,
tool_prefix: str | None = None,
log_level: mcp_types.LoggingLevel | None = None,
log_handler: LoggingFnT | None = None,
timeout: float = 5,
process_tool_call: ProcessToolCallback | None = None,
allow_sampling: bool = True,
max_retries: int = 1,
sampling_model: models.Model | None = None,
**kwargs: Any,
):
# Handle deprecated sse_read_timeout parameter
if 'sse_read_timeout' in kwargs:
if read_timeout is not None:
raise TypeError("'read_timeout' and 'sse_read_timeout' cannot be set at the same time.")

warnings.warn(
"'sse_read_timeout' is deprecated, use 'read_timeout' instead.", DeprecationWarning, stacklevel=2
)
read_timeout = kwargs.pop('sse_read_timeout')

_utils.validate_empty_kwargs(kwargs)

if read_timeout is None:
read_timeout = 5 * 60

self.url = url
self.headers = headers
self.http_client = http_client
self.tool_prefix = tool_prefix
self.log_level = log_level
self.log_handler = log_handler
self.timeout = timeout
self.process_tool_call = process_tool_call
self.allow_sampling = allow_sampling
self.max_retries = max_retries
self.sampling_model = sampling_model
self.read_timeout = read_timeout

@property
@abstractmethod
def _transport_client(
Expand Down Expand Up @@ -522,7 +571,7 @@ async def client_streams(
self._transport_client,
url=self.url,
timeout=self.timeout,
sse_read_timeout=self.sse_read_timeout,
sse_read_timeout=self.read_timeout,
)

if self.http_client is not None:
Expand All @@ -549,7 +598,7 @@ def __repr__(self) -> str: # pragma: no cover
return f'{self.__class__.__name__}(url={self.url!r}, tool_prefix={self.tool_prefix!r})'


@dataclass
@dataclass(init=False)
class MCPServerSSE(_MCPServerHTTP):
"""An MCP server that connects over streamable HTTP connections.

Expand Down
26 changes: 18 additions & 8 deletions tests/test_mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,20 +140,30 @@ def test_sse_server():


def test_sse_server_with_header_and_timeout():
sse_server = MCPServerSSE(
url='http://localhost:8000/sse',
headers={'my-custom-header': 'my-header-value'},
timeout=10,
sse_read_timeout=100,
log_level='info',
)
with pytest.warns(DeprecationWarning, match="'sse_read_timeout' is deprecated, use 'read_timeout' instead."):
sse_server = MCPServerSSE(
url='http://localhost:8000/sse',
headers={'my-custom-header': 'my-header-value'},
timeout=10,
sse_read_timeout=100,
log_level='info',
)
assert sse_server.url == 'http://localhost:8000/sse'
assert sse_server.headers is not None and sse_server.headers['my-custom-header'] == 'my-header-value'
assert sse_server.timeout == 10
assert sse_server.sse_read_timeout == 100
assert sse_server.read_timeout == 100
assert sse_server.log_level == 'info'


def test_sse_server_conflicting_timeout_params():
with pytest.raises(TypeError, match="'read_timeout' and 'sse_read_timeout' cannot be set at the same time."):
MCPServerSSE(
url='http://localhost:8000/sse',
read_timeout=50,
sse_read_timeout=100,
)


@pytest.mark.vcr()
async def test_agent_with_stdio_server(allow_model_requests: None, agent: Agent):
async with agent:
Expand Down