mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-22 13:34:17 +02:00
Add timeout support to client (#455)
* Add timeouts kwargs * Add timeout support * Fix windows handling and docs * Update client.mdx * Windows tests * Update test_sse.py * Update test_sse.py * Update test_sse.py * Update test_sse.py * Fix windows
This commit is contained in:
parent
5b05e80d8b
commit
837d4c407d
9 changed files with 305 additions and 20 deletions
1
.github/workflows/run-tests.yml
vendored
1
.github/workflows/run-tests.yml
vendored
|
|
@ -46,6 +46,7 @@ jobs:
|
|||
enable-cache: true
|
||||
cache-dependency-glob: "uv.lock"
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install FastMCP
|
||||
run: uv sync --dev --locked
|
||||
|
||||
|
|
|
|||
|
|
@ -115,14 +115,18 @@ The standard client methods return user-friendly representations that may change
|
|||
tools = await client.list_tools()
|
||||
# tools -> list[mcp.types.Tool]
|
||||
```
|
||||
* **`call_tool(name: str, arguments: dict[str, Any] | None = None)`**: Executes a tool on the server.
|
||||
* **`call_tool(name: str, arguments: dict[str, Any] | None = None, timeout: float | None = None)`**: Executes a tool on the server.
|
||||
```python
|
||||
result = await client.call_tool("add", {"a": 5, "b": 3})
|
||||
# result -> list[mcp.types.TextContent | mcp.types.ImageContent | ...]
|
||||
print(result[0].text) # Assuming TextContent, e.g., '8'
|
||||
|
||||
# With timeout (aborts if execution takes longer than 2 seconds)
|
||||
result = await client.call_tool("long_running_task", {"param": "value"}, timeout=2.0)
|
||||
```
|
||||
* Arguments are passed as a dictionary. FastMCP servers automatically handle JSON string parsing for complex types if needed.
|
||||
* Returns a list of content objects (usually `TextContent` or `ImageContent`).
|
||||
* The optional `timeout` parameter limits the maximum execution time (in seconds) for this specific call, overriding any client-level timeout.
|
||||
|
||||
#### Resource Operations
|
||||
|
||||
|
|
@ -191,6 +195,45 @@ These methods are especially useful for debugging or when you need to access met
|
|||
|
||||
MCP allows servers to interact with clients in order to provide additional capabilities. The `Client` constructor accepts additional configuration to handle these server requests.
|
||||
|
||||
#### Timeout Control
|
||||
|
||||
<VersionBadge version="2.3.4" />
|
||||
|
||||
You can control request timeouts at both the client level and individual request level:
|
||||
|
||||
```python
|
||||
from fastmcp import Client
|
||||
from fastmcp.exceptions import McpError
|
||||
|
||||
# Client with a global 5-second timeout for all requests
|
||||
client = Client(
|
||||
my_mcp_server,
|
||||
timeout=5.0 # Default timeout in seconds
|
||||
)
|
||||
|
||||
async with client:
|
||||
# This uses the global 5-second timeout
|
||||
result1 = await client.call_tool("quick_task", {"param": "value"})
|
||||
|
||||
# This specifies a 10-second timeout for this specific call
|
||||
result2 = await client.call_tool("slow_task", {"param": "value"}, timeout=10.0)
|
||||
|
||||
try:
|
||||
# This will likely timeout
|
||||
result3 = await client.call_tool("medium_task", {"param": "value"}, timeout=0.01)
|
||||
except McpError as e:
|
||||
# Handle timeout error
|
||||
print(f"The task timed out: {e}")
|
||||
```
|
||||
|
||||
<Warning>
|
||||
Timeout behavior varies between transport types:
|
||||
|
||||
- With **SSE** transport, the per-request (tool call) timeout **always** takes precedence, regardless of which is lower.
|
||||
- With **HTTP** transport, the **lower** of the two timeouts (client or tool call) takes precedence.
|
||||
|
||||
For consistent behavior across all transports, we recommend explicitly setting timeouts at the individual tool call level when needed, rather than relying on client-level timeouts.
|
||||
</Warning>
|
||||
|
||||
#### LLM Sampling
|
||||
|
||||
|
|
|
|||
|
|
@ -35,8 +35,35 @@ class Client:
|
|||
"""
|
||||
MCP client that delegates connection management to a Transport instance.
|
||||
|
||||
The Client class is primarily concerned with MCP protocol logic,
|
||||
while the Transport handles connection establishment and management.
|
||||
The Client class is responsible for MCP protocol logic, while the Transport
|
||||
handles connection establishment and management. Client provides methods
|
||||
for working with resources, prompts, tools and other MCP capabilities.
|
||||
|
||||
Args:
|
||||
transport: Connection source specification, which can be:
|
||||
- ClientTransport: Direct transport instance
|
||||
- FastMCP: In-process FastMCP server
|
||||
- AnyUrl | str: URL to connect to
|
||||
- Path: File path for local socket
|
||||
- dict: Transport configuration
|
||||
roots: Optional RootsList or RootsHandler for filesystem access
|
||||
sampling_handler: Optional handler for sampling requests
|
||||
log_handler: Optional handler for log messages
|
||||
message_handler: Optional handler for protocol messages
|
||||
timeout: Optional timeout for requests (seconds or timedelta)
|
||||
|
||||
Examples:
|
||||
```python
|
||||
# Connect to FastMCP server
|
||||
client = Client("http://localhost:8080")
|
||||
|
||||
async with client:
|
||||
# List available resources
|
||||
resources = await client.list_resources()
|
||||
|
||||
# Call a tool
|
||||
result = await client.call_tool("my_tool", {"param": "value"})
|
||||
```
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
|
|
@ -47,19 +74,22 @@ class Client:
|
|||
sampling_handler: SamplingHandler | None = None,
|
||||
log_handler: LogHandler | None = None,
|
||||
message_handler: MessageHandler | None = None,
|
||||
read_timeout_seconds: datetime.timedelta | None = None,
|
||||
timeout: datetime.timedelta | float | int | None = None,
|
||||
):
|
||||
self.transport = infer_transport(transport)
|
||||
self._session: ClientSession | None = None
|
||||
self._exit_stack: AsyncExitStack | None = None
|
||||
self._nesting_counter: int = 0
|
||||
|
||||
if isinstance(timeout, int | float):
|
||||
timeout = datetime.timedelta(seconds=timeout)
|
||||
|
||||
self._session_kwargs: SessionKwargs = {
|
||||
"sampling_callback": None,
|
||||
"list_roots_callback": None,
|
||||
"logging_callback": log_handler,
|
||||
"message_handler": message_handler,
|
||||
"read_timeout_seconds": read_timeout_seconds,
|
||||
"read_timeout_seconds": timeout,
|
||||
}
|
||||
|
||||
if roots is not None:
|
||||
|
|
@ -397,7 +427,10 @@ class Client:
|
|||
# --- Call Tool ---
|
||||
|
||||
async def call_tool_mcp(
|
||||
self, name: str, arguments: dict[str, Any]
|
||||
self,
|
||||
name: str,
|
||||
arguments: dict[str, Any],
|
||||
timeout: datetime.timedelta | float | int | None = None,
|
||||
) -> mcp.types.CallToolResult:
|
||||
"""Send a tools/call request and return the complete MCP protocol result.
|
||||
|
||||
|
|
@ -407,7 +440,7 @@ class Client:
|
|||
Args:
|
||||
name (str): The name of the tool to call.
|
||||
arguments (dict[str, Any]): Arguments to pass to the tool.
|
||||
|
||||
timeout (datetime.timedelta | float | int | None, optional): The timeout for the tool call. Defaults to None.
|
||||
Returns:
|
||||
mcp.types.CallToolResult: The complete response object from the protocol,
|
||||
containing the tool result and any additional metadata.
|
||||
|
|
@ -415,13 +448,19 @@ class Client:
|
|||
Raises:
|
||||
RuntimeError: If called while the client is not connected.
|
||||
"""
|
||||
result = await self.session.call_tool(name=name, arguments=arguments)
|
||||
|
||||
if isinstance(timeout, int | float):
|
||||
timeout = datetime.timedelta(seconds=timeout)
|
||||
result = await self.session.call_tool(
|
||||
name=name, arguments=arguments, read_timeout_seconds=timeout
|
||||
)
|
||||
return result
|
||||
|
||||
async def call_tool(
|
||||
self,
|
||||
name: str,
|
||||
arguments: dict[str, Any] | None = None,
|
||||
timeout: datetime.timedelta | float | int | None = None,
|
||||
) -> list[
|
||||
mcp.types.TextContent | mcp.types.ImageContent | mcp.types.EmbeddedResource
|
||||
]:
|
||||
|
|
@ -441,7 +480,11 @@ class Client:
|
|||
ToolError: If the tool call results in an error.
|
||||
RuntimeError: If called while the client is not connected.
|
||||
"""
|
||||
result = await self.call_tool_mcp(name=name, arguments=arguments or {})
|
||||
result = await self.call_tool_mcp(
|
||||
name=name,
|
||||
arguments=arguments or {},
|
||||
timeout=timeout,
|
||||
)
|
||||
if result.isError:
|
||||
msg = cast(mcp.types.TextContent, result.content[0]).text
|
||||
raise ToolError(msg)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import sys
|
|||
import warnings
|
||||
from collections.abc import AsyncIterator
|
||||
from pathlib import Path
|
||||
from typing import Any, TypedDict
|
||||
from typing import Any, TypedDict, cast
|
||||
|
||||
from mcp import ClientSession, StdioServerParameters
|
||||
from mcp.client.session import (
|
||||
|
|
@ -102,7 +102,12 @@ class WSTransport(ClientTransport):
|
|||
class SSETransport(ClientTransport):
|
||||
"""Transport implementation that connects to an MCP server via Server-Sent Events."""
|
||||
|
||||
def __init__(self, url: str | AnyUrl, headers: dict[str, str] | None = None):
|
||||
def __init__(
|
||||
self,
|
||||
url: str | AnyUrl,
|
||||
headers: dict[str, str] | None = None,
|
||||
sse_read_timeout: datetime.timedelta | float | int | None = None,
|
||||
):
|
||||
if isinstance(url, AnyUrl):
|
||||
url = str(url)
|
||||
if not isinstance(url, str) or not url.startswith("http"):
|
||||
|
|
@ -110,11 +115,28 @@ class SSETransport(ClientTransport):
|
|||
self.url = url
|
||||
self.headers = headers or {}
|
||||
|
||||
if isinstance(sse_read_timeout, int | float):
|
||||
sse_read_timeout = datetime.timedelta(seconds=sse_read_timeout)
|
||||
self.sse_read_timeout = sse_read_timeout
|
||||
|
||||
@contextlib.asynccontextmanager
|
||||
async def connect_session(
|
||||
self, **session_kwargs: Unpack[SessionKwargs]
|
||||
) -> AsyncIterator[ClientSession]:
|
||||
async with sse_client(self.url, headers=self.headers) as transport:
|
||||
client_kwargs = {}
|
||||
# sse_read_timeout has a default value set, so we can't pass None without overriding it
|
||||
# instead we simply leave the kwarg out if it's not provided
|
||||
if self.sse_read_timeout is not None:
|
||||
client_kwargs["sse_read_timeout"] = self.sse_read_timeout.total_seconds()
|
||||
if session_kwargs.get("read_timeout_seconds", None) is not None:
|
||||
read_timeout_seconds = cast(
|
||||
datetime.timedelta, session_kwargs.get("read_timeout_seconds")
|
||||
)
|
||||
client_kwargs["timeout"] = read_timeout_seconds.total_seconds()
|
||||
|
||||
async with sse_client(
|
||||
self.url, headers=self.headers, **client_kwargs
|
||||
) as transport:
|
||||
read_stream, write_stream = transport
|
||||
async with ClientSession(
|
||||
read_stream, write_stream, **session_kwargs
|
||||
|
|
@ -129,7 +151,12 @@ class SSETransport(ClientTransport):
|
|||
class StreamableHttpTransport(ClientTransport):
|
||||
"""Transport implementation that connects to an MCP server via Streamable HTTP Requests."""
|
||||
|
||||
def __init__(self, url: str | AnyUrl, headers: dict[str, str] | None = None):
|
||||
def __init__(
|
||||
self,
|
||||
url: str | AnyUrl,
|
||||
headers: dict[str, str] | None = None,
|
||||
sse_read_timeout: datetime.timedelta | float | int | None = None,
|
||||
):
|
||||
if isinstance(url, AnyUrl):
|
||||
url = str(url)
|
||||
if not isinstance(url, str) or not url.startswith("http"):
|
||||
|
|
@ -137,11 +164,25 @@ class StreamableHttpTransport(ClientTransport):
|
|||
self.url = url
|
||||
self.headers = headers or {}
|
||||
|
||||
if isinstance(sse_read_timeout, int | float):
|
||||
sse_read_timeout = datetime.timedelta(seconds=sse_read_timeout)
|
||||
self.sse_read_timeout = sse_read_timeout
|
||||
|
||||
@contextlib.asynccontextmanager
|
||||
async def connect_session(
|
||||
self, **session_kwargs: Unpack[SessionKwargs]
|
||||
) -> AsyncIterator[ClientSession]:
|
||||
async with streamablehttp_client(self.url, headers=self.headers) as transport:
|
||||
client_kwargs = {}
|
||||
# sse_read_timeout has a default value set, so we can't pass None without overriding it
|
||||
# instead we simply leave the kwarg out if it's not provided
|
||||
if self.sse_read_timeout is not None:
|
||||
client_kwargs["sse_read_timeout"] = self.sse_read_timeout
|
||||
if session_kwargs.get("read_timeout_seconds", None) is not None:
|
||||
client_kwargs["timeout"] = session_kwargs.get("read_timeout_seconds")
|
||||
|
||||
async with streamablehttp_client(
|
||||
self.url, headers=self.headers, **client_kwargs
|
||||
) as transport:
|
||||
read_stream, write_stream, _ = transport
|
||||
async with ClientSession(
|
||||
read_stream, write_stream, **session_kwargs
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
"""Custom exceptions for FastMCP."""
|
||||
|
||||
from mcp import McpError # noqa: F401
|
||||
|
||||
|
||||
class FastMCPError(Exception):
|
||||
"""Base error for FastMCP."""
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
from collections.abc import Callable, Iterable, Mapping
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
import mcp.types
|
||||
from exceptiongroup import BaseExceptionGroup
|
||||
from mcp import McpError
|
||||
|
||||
import fastmcp
|
||||
|
||||
|
|
@ -16,12 +19,19 @@ def iter_exc(group: BaseExceptionGroup):
|
|||
|
||||
def _exception_handler(group: BaseExceptionGroup):
|
||||
for leaf in iter_exc(group):
|
||||
if isinstance(leaf, httpx.ConnectTimeout):
|
||||
raise McpError(
|
||||
error=mcp.types.ErrorData(
|
||||
code=httpx.codes.REQUEST_TIMEOUT,
|
||||
message="Timed out while waiting for response.",
|
||||
)
|
||||
)
|
||||
raise leaf
|
||||
|
||||
|
||||
# this catch handler is used to catch taskgroup exception groups and raise the
|
||||
# first exception. This allows more sane debugging.
|
||||
catch_handlers: Mapping[
|
||||
_catch_handlers: Mapping[
|
||||
type[BaseException] | Iterable[type[BaseException]],
|
||||
Callable[[BaseExceptionGroup[Any]], Any],
|
||||
] = {
|
||||
|
|
@ -34,6 +44,6 @@ def get_catch_handlers() -> Mapping[
|
|||
Callable[[BaseExceptionGroup[Any]], Any],
|
||||
]:
|
||||
if fastmcp.settings.settings.client_raise_first_exceptiongroup_error:
|
||||
return catch_handlers
|
||||
return _catch_handlers
|
||||
else:
|
||||
return {}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import asyncio
|
||||
from typing import cast
|
||||
|
||||
import pytest
|
||||
from mcp import McpError
|
||||
from pydantic import AnyUrl
|
||||
|
||||
from fastmcp.client import Client
|
||||
|
|
@ -27,6 +29,12 @@ def fastmcp_server():
|
|||
"""Add two numbers together."""
|
||||
return a + b
|
||||
|
||||
@server.tool()
|
||||
async def sleep(seconds: float) -> str:
|
||||
"""Sleep for a given number of seconds."""
|
||||
await asyncio.sleep(seconds)
|
||||
return f"Slept for {seconds} seconds"
|
||||
|
||||
# Add a resource
|
||||
@server.resource(uri="data://users")
|
||||
async def get_users():
|
||||
|
|
@ -78,8 +86,8 @@ async def test_list_tools(fastmcp_server):
|
|||
result = await client.list_tools()
|
||||
|
||||
# Check that our tools are available
|
||||
assert len(result) == 2
|
||||
assert set(tool.name for tool in result) == {"greet", "add"}
|
||||
assert len(result) == 3
|
||||
assert set(tool.name for tool in result) == {"greet", "add", "sleep"}
|
||||
|
||||
|
||||
async def test_list_tools_mcp(fastmcp_server):
|
||||
|
|
@ -91,8 +99,8 @@ async def test_list_tools_mcp(fastmcp_server):
|
|||
|
||||
# Check that we got the raw MCP ListToolsResult object
|
||||
assert hasattr(result, "tools")
|
||||
assert len(result.tools) == 2
|
||||
assert set(tool.name for tool in result.tools) == {"greet", "add"}
|
||||
assert len(result.tools) == 3
|
||||
assert set(tool.name for tool in result.tools) == {"greet", "add", "sleep"}
|
||||
|
||||
|
||||
async def test_call_tool(fastmcp_server):
|
||||
|
|
@ -499,3 +507,39 @@ class TestErrorHandling:
|
|||
with pytest.raises(Exception) as excinfo:
|
||||
await client.read_resource(AnyUrl("error://resource/123"))
|
||||
assert "This is a resource error (xyz)" in str(excinfo.value)
|
||||
|
||||
|
||||
class TestTimeout:
|
||||
async def test_timeout(self, fastmcp_server: FastMCP):
|
||||
async with Client(
|
||||
transport=FastMCPTransport(fastmcp_server), timeout=0.01
|
||||
) as client:
|
||||
with pytest.raises(
|
||||
McpError,
|
||||
match="Timed out while waiting for response to ClientRequest. Waited 0.01 seconds",
|
||||
):
|
||||
await client.call_tool("sleep", {"seconds": 0.1})
|
||||
|
||||
async def test_timeout_tool_call(self, fastmcp_server: FastMCP):
|
||||
async with Client(transport=FastMCPTransport(fastmcp_server)) as client:
|
||||
with pytest.raises(McpError):
|
||||
await client.call_tool("sleep", {"seconds": 0.1}, timeout=0.01)
|
||||
|
||||
async def test_timeout_tool_call_overrides_client_timeout(
|
||||
self, fastmcp_server: FastMCP
|
||||
):
|
||||
async with Client(
|
||||
transport=FastMCPTransport(fastmcp_server),
|
||||
timeout=2,
|
||||
) as client:
|
||||
with pytest.raises(McpError):
|
||||
await client.call_tool("sleep", {"seconds": 0.1}, timeout=0.01)
|
||||
|
||||
async def test_timeout_tool_call_overrides_client_timeout_even_if_lower(
|
||||
self, fastmcp_server: FastMCP
|
||||
):
|
||||
async with Client(
|
||||
transport=FastMCPTransport(fastmcp_server),
|
||||
timeout=0.01,
|
||||
) as client:
|
||||
await client.call_tool("sleep", {"seconds": 0.1}, timeout=2)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
from collections.abc import Generator
|
||||
|
||||
import pytest
|
||||
import uvicorn
|
||||
from mcp import McpError
|
||||
from mcp.types import TextResourceContents
|
||||
from starlette.applications import Starlette
|
||||
from starlette.routing import Mount
|
||||
|
|
@ -31,6 +33,12 @@ def fastmcp_server():
|
|||
"""Add two numbers together."""
|
||||
return a + b
|
||||
|
||||
@server.tool()
|
||||
async def sleep(seconds: float) -> str:
|
||||
"""Sleep for a given number of seconds."""
|
||||
await asyncio.sleep(seconds)
|
||||
return f"Slept for {seconds} seconds"
|
||||
|
||||
# Add a resource
|
||||
@server.resource(uri="data://users")
|
||||
async def get_users():
|
||||
|
|
@ -126,3 +134,49 @@ async def test_nested_sse_server_resolves_correctly():
|
|||
) as client:
|
||||
result = await client.ping()
|
||||
assert result is True
|
||||
|
||||
|
||||
class TestTimeout:
|
||||
async def test_timeout(self, sse_server: str):
|
||||
with pytest.raises(
|
||||
McpError,
|
||||
match="Timed out while waiting for response to ClientRequest. Waited 0.01 seconds",
|
||||
):
|
||||
async with Client(
|
||||
transport=SSETransport(sse_server),
|
||||
timeout=0.01,
|
||||
) as client:
|
||||
await client.call_tool("sleep", {"seconds": 0.1})
|
||||
|
||||
async def test_timeout_tool_call(self, sse_server: str):
|
||||
async with Client(transport=SSETransport(sse_server)) as client:
|
||||
with pytest.raises(McpError, match="Timed out"):
|
||||
await client.call_tool("sleep", {"seconds": 0.1}, timeout=0.01)
|
||||
|
||||
async def test_timeout_tool_call_overrides_client_timeout_if_lower(
|
||||
self, sse_server: str
|
||||
):
|
||||
async with Client(
|
||||
transport=SSETransport(sse_server),
|
||||
timeout=2,
|
||||
) as client:
|
||||
with pytest.raises(McpError, match="Timed out"):
|
||||
await client.call_tool("sleep", {"seconds": 0.1}, timeout=0.01)
|
||||
|
||||
@pytest.mark.skipif(
|
||||
sys.platform == "win32",
|
||||
reason="This test is flaky on Windows. Sometimes the client timeout is respected and sometimes it is not.",
|
||||
)
|
||||
async def test_timeout_client_timeout_does_not_override_tool_call_timeout_if_lower(
|
||||
self, sse_server: str
|
||||
):
|
||||
"""
|
||||
With SSE, the tool call timeout always takes precedence over the client.
|
||||
|
||||
Note: on Windows, the behavior appears unpredictable.
|
||||
"""
|
||||
async with Client(
|
||||
transport=SSETransport(sse_server),
|
||||
timeout=0.01,
|
||||
) as client:
|
||||
await client.call_tool("sleep", {"seconds": 0.1}, timeout=2)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
from collections.abc import Generator
|
||||
|
||||
import pytest
|
||||
import uvicorn
|
||||
from mcp import McpError
|
||||
from mcp.types import TextResourceContents
|
||||
from starlette.applications import Starlette
|
||||
from starlette.routing import Mount
|
||||
|
|
@ -31,6 +33,12 @@ def fastmcp_server():
|
|||
"""Add two numbers together."""
|
||||
return a + b
|
||||
|
||||
@server.tool()
|
||||
async def sleep(seconds: float) -> str:
|
||||
"""Sleep for a given number of seconds."""
|
||||
await asyncio.sleep(seconds)
|
||||
return f"Slept for {seconds} seconds"
|
||||
|
||||
# Add a resource
|
||||
@server.resource(uri="data://users")
|
||||
async def get_users():
|
||||
|
|
@ -139,3 +147,42 @@ async def test_nested_streamable_http_server_resolves_correctly():
|
|||
) as client:
|
||||
result = await client.ping()
|
||||
assert result is True
|
||||
|
||||
|
||||
class TestTimeout:
|
||||
async def test_timeout(self, streamable_http_server: str):
|
||||
# note this transport behaves differently than others and raises
|
||||
# McpError from the *client* context
|
||||
with pytest.raises(McpError, match="Timed out"):
|
||||
async with Client(
|
||||
transport=StreamableHttpTransport(streamable_http_server),
|
||||
timeout=0.01,
|
||||
) as client:
|
||||
await client.call_tool("sleep", {"seconds": 0.1})
|
||||
|
||||
async def test_timeout_tool_call(self, streamable_http_server: str):
|
||||
async with Client(
|
||||
transport=StreamableHttpTransport(streamable_http_server),
|
||||
) as client:
|
||||
with pytest.raises(McpError):
|
||||
await client.call_tool("sleep", {"seconds": 0.1}, timeout=0.01)
|
||||
|
||||
async def test_timeout_tool_call_overrides_client_timeout(
|
||||
self, streamable_http_server: str
|
||||
):
|
||||
async with Client(
|
||||
transport=StreamableHttpTransport(streamable_http_server),
|
||||
timeout=2,
|
||||
) as client:
|
||||
with pytest.raises(McpError):
|
||||
await client.call_tool("sleep", {"seconds": 0.1}, timeout=0.01)
|
||||
|
||||
async def test_timeout_client_timeout_overrides_tool_call_timeout_if_lower(
|
||||
self, streamable_http_server: str
|
||||
):
|
||||
with pytest.raises(McpError):
|
||||
async with Client(
|
||||
transport=StreamableHttpTransport(streamable_http_server),
|
||||
timeout=0.01,
|
||||
) as client:
|
||||
await client.call_tool("sleep", {"seconds": 0.1}, timeout=2)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue