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:
Sydney RunkleandClaude Opus 5 committed 2026-08-27 15:58:01 -07:00
1 parent bd2f725f78
commit 24b1e72823
4 files changed
+71 -14

No files matched your search

+11 -8
View File
@@ -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 = (
+1 -1
View File
@@ -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")
+5 -5
View File
@@ -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 = [