Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions langchain_mcp_adapters/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,10 +140,10 @@ class StreamableHttpConnection(TypedDict):
headers: NotRequired[dict[str, Any] | None]
"""HTTP headers to send to the endpoint."""

timeout: NotRequired[timedelta]
timeout: NotRequired[float | timedelta]
"""HTTP timeout."""

sse_read_timeout: NotRequired[timedelta]
sse_read_timeout: NotRequired[float | timedelta]
"""How long (in seconds) the client will wait for a new event before disconnecting.
All other HTTP operations are controlled by `timeout`."""

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"langchain-core>=0.3.36,<0.4",
"mcp>=1.9.2",
"mcp==1.9.2",
"typing-extensions>=4.14.0",
]

Expand Down
41 changes: 38 additions & 3 deletions tests/test_tools.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from typing import Annotated
from unittest.mock import AsyncMock, MagicMock

import httpx
import pytest
from langchain_core.callbacks import CallbackManagerForToolRun
from langchain_core.messages import ToolMessage
from langchain_core.tools import BaseTool, InjectedToolArg, ToolException, tool
from mcp.server import FastMCP
from mcp.types import (
CallToolResult,
EmbeddedResource,
Expand Down Expand Up @@ -368,10 +370,8 @@ def get_status() -> str:


# Tests for httpx_client_factory functionality
@pytest.mark.asyncio
async def test_load_mcp_tools_with_custom_httpx_client_factory(socket_enabled) -> None:
"""Test load mcp tools with custom httpx client factory."""
import httpx

# Custom httpx client factory
def custom_httpx_client_factory(
Expand Down Expand Up @@ -423,7 +423,17 @@ def get_info() -> str:
return server


@pytest.mark.asyncio
def _create_failing_server() -> FastMCP:
server = FastMCP(port=8184)

@server.tool()
def failing_tool(message: str) -> str:
"""A tool that always fails with an exception"""
raise ValueError(f"Tool failed: {message}")

return server


async def test_load_mcp_tools_with_custom_httpx_client_factory_sse(
socket_enabled,
) -> None:
Expand Down Expand Up @@ -468,3 +478,28 @@ def custom_httpx_client_factory(
# Expected to fail since server doesn't have SSE endpoint,
# but the important thing is that httpx_client_factory was passed correctly
pass


async def test_mcp_tool_exception_handling(socket_enabled) -> None:
"""Test that exceptions from MCP tools are properly handled on the client side."""
with run_streamable_http(_create_failing_server, 8184):
# Initialize client
client = MultiServerMCPClient(
{
"failing": {
"url": "http://localhost:8184/mcp/",
"transport": "streamable_http",
"timeout": 5,
"sse_read_timeout": 2,
}
},
)

# Get the failing tool
tools = await client.get_tools()
assert len(tools) == 1
tool = tools[0]
assert tool.name == "failing_tool"
await tool.ainvoke(
{"args": {"message": "test error"}, "id": "1", "type": "tool_call"}
)
8 changes: 4 additions & 4 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading