mirror of
https://github.com/langchain-ai/langchain.git
synced 2026-10-05 09:25:14 +03:00
fix(langchain): require full fastmcp for the mcp extra
A multi-server `MCPConfig` mounts its backends on a router, which lives in the server half of FastMCP. The extra pinned `fastmcp-slim[client]`, so the documented multi-server example failed at connect time with "MCP configs with multiple servers require the full `fastmcp` package". The existing multi-server test only asserted on the transport it built, never dialing it, which is why this went unnoticed — so the new test drives two real stdio servers through one adapter. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
bd2f725f78
commit
24b1e72823
4 files changed
+71
-14
No files matched your search
@@ -21,10 +21,7 @@ try:
|
||||
# not an explicit re-export.
|
||||
from mcp.server.mcpserver import MCPServer
|
||||
except ImportError as _import_error:
|
||||
msg = (
|
||||
"Please install the fastmcp client to use `MCPAdapter` — "
|
||||
'`pip install "fastmcp-slim[client]"`.'
|
||||
)
|
||||
msg = "Please install FastMCP to use `MCPAdapter` — `pip install fastmcp`."
|
||||
raise ImportError(msg) from _import_error
|
||||
|
||||
|
||||
@@ -34,10 +31,16 @@ if TYPE_CHECKING:
|
||||
from fastmcp import FastMCP
|
||||
from langchain_core.tools import BaseTool
|
||||
else:
|
||||
# In-process FastMCP servers live in the server half of FastMCP, which the
|
||||
# lightweight `fastmcp-slim[client]` install does not pull in. FastMCP degrades
|
||||
# this annotation the same way in `fastmcp.client.transports.inference`.
|
||||
FastMCP = Any
|
||||
try:
|
||||
from fastmcp import FastMCP
|
||||
except ImportError:
|
||||
# In-process FastMCP servers, and multi-server `MCPConfig` targets, live
|
||||
# in the server half of FastMCP. The `mcp` extra installs it, but a
|
||||
# caller who reached this module through a client-only install
|
||||
# (`fastmcp-slim[client]`) can still use every other target type — so
|
||||
# the annotation degrades rather than the import failing, the same way
|
||||
# FastMCP degrades it in `fastmcp.client.transports.inference`.
|
||||
FastMCP = Any
|
||||
|
||||
|
||||
MCPAdapterTarget: TypeAlias = (
|
||||
|
||||
@@ -49,7 +49,7 @@ deepseek = ["langchain-deepseek"]
|
||||
xai = ["langchain-xai"]
|
||||
perplexity = ["langchain-perplexity"]
|
||||
meta = ["langchain-meta"]
|
||||
mcp = ["fastmcp-slim[client]>=4.0.0b4,<5.0.0"]
|
||||
mcp = ["fastmcp>=4.0.0b4,<5.0.0"]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://docs.langchain.com/"
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from unittest.mock import ANY
|
||||
|
||||
import pytest
|
||||
from fastmcp import Client, FastMCP
|
||||
@@ -103,6 +105,58 @@ def test_one_adapter_can_serve_several_servers() -> None:
|
||||
assert transport.name_as_prefix is True
|
||||
|
||||
|
||||
_STDIO_SERVER = """
|
||||
import sys
|
||||
from mcp.server.mcpserver import MCPServer
|
||||
|
||||
mcp = MCPServer("{name}")
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def whoami() -> str:
|
||||
\"\"\"Name the server answering this call.\"\"\"
|
||||
return "{name}"
|
||||
|
||||
|
||||
mcp.run()
|
||||
"""
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def two_stdio_servers(tmp_path: Path) -> dict[str, Any]:
|
||||
"""Write two single-tool stdio servers and return a config naming both."""
|
||||
servers = {}
|
||||
for name in ("alpha", "beta"):
|
||||
script = tmp_path / f"{name}.py"
|
||||
script.write_text(_STDIO_SERVER.format(name=name))
|
||||
servers[name] = {"command": sys.executable, "args": [str(script)]}
|
||||
return {"mcpServers": servers}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_several_servers_connect_and_keep_their_prefixes(
|
||||
two_stdio_servers: dict[str, Any],
|
||||
) -> None:
|
||||
"""A multi-server config connects, and each backend's tools stay namespaced.
|
||||
|
||||
Connecting is the point. Mounting several backends behind one client needs
|
||||
the server half of FastMCP, which a client-only install does not provide —
|
||||
a config that builds a valid transport can still fail the moment it dials.
|
||||
So this drives real servers rather than asserting on the transport.
|
||||
"""
|
||||
config = two_stdio_servers
|
||||
|
||||
async with MCPAdapter(config) as adapter:
|
||||
tools = await adapter.get_tools()
|
||||
|
||||
assert sorted(tool.name for tool in tools) == ["alpha_whoami", "beta_whoami"]
|
||||
|
||||
by_name = {tool.name: tool for tool in tools}
|
||||
assert await by_name["alpha_whoami"].ainvoke({}) == [
|
||||
{"type": "text", "text": "alpha", "id": ANY}
|
||||
]
|
||||
|
||||
|
||||
def test_prebuilt_client_is_used_as_is() -> None:
|
||||
client: Client[Any] = Client("https://example.com/mcp")
|
||||
|
||||
|
||||
Generated
+5
-5
@@ -2329,7 +2329,7 @@ huggingface = [
|
||||
{ name = "langchain-huggingface" },
|
||||
]
|
||||
mcp = [
|
||||
{ name = "fastmcp-slim", extra = ["client"] },
|
||||
{ name = "fastmcp" },
|
||||
]
|
||||
meta = [
|
||||
{ name = "langchain-meta" },
|
||||
@@ -2388,7 +2388,7 @@ typing = [
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "fastmcp-slim", extras = ["client"], marker = "extra == 'mcp'", specifier = ">=4.0.0b4,<5.0.0" },
|
||||
{ name = "fastmcp", marker = "extra == 'mcp'", specifier = ">=4.0.0b4,<5.0.0" },
|
||||
{ name = "langchain-anthropic", marker = "extra == 'anthropic'", editable = "../partners/anthropic" },
|
||||
{ name = "langchain-aws", marker = "extra == 'aws'" },
|
||||
{ name = "langchain-azure-ai", marker = "extra == 'azure-ai'" },
|
||||
@@ -2446,7 +2446,7 @@ typing = [
|
||||
|
||||
[[package]]
|
||||
name = "langchain-anthropic"
|
||||
version = "1.6.1"
|
||||
version = "1.7.0"
|
||||
source = { editable = "../partners/anthropic" }
|
||||
dependencies = [
|
||||
{ name = "anthropic" },
|
||||
@@ -2456,7 +2456,7 @@ dependencies = [
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "anthropic", specifier = ">=0.120.0,<1.0.0" },
|
||||
{ name = "anthropic", specifier = ">=0.120.0,<2.0.0" },
|
||||
{ name = "langchain-core", editable = "../core" },
|
||||
{ name = "pydantic", specifier = ">=2.7.4,<3.0.0" },
|
||||
]
|
||||
@@ -2479,7 +2479,7 @@ test = [
|
||||
{ name = "pytest-watcher", specifier = ">=0.3.4,<1.0.0" },
|
||||
{ name = "pytest-xdist", specifier = ">=3.8.0,<4.0.0" },
|
||||
{ name = "syrupy", specifier = ">=5.0.0,<6.0.0" },
|
||||
{ name = "vcrpy", specifier = ">=8.0.0,<9.0.0" },
|
||||
{ name = "vcrpy", specifier = ">=8.2.0,<9.0.0" },
|
||||
]
|
||||
test-integration = [{ name = "requests", specifier = ">=2.32.3,<3.0.0" }]
|
||||
typing = [
|
||||
|
||||
Reference in new issue
Block a user