diff --git a/libs/langchain_v1/langchain/mcp/adapter.py b/libs/langchain_v1/langchain/mcp/adapter.py index f36aa9e5d1..5e9254e382 100644 --- a/libs/langchain_v1/langchain/mcp/adapter.py +++ b/libs/langchain_v1/langchain/mcp/adapter.py @@ -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 = ( diff --git a/libs/langchain_v1/pyproject.toml b/libs/langchain_v1/pyproject.toml index 2280c83854..b14d55b15f 100644 --- a/libs/langchain_v1/pyproject.toml +++ b/libs/langchain_v1/pyproject.toml @@ -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/" diff --git a/libs/langchain_v1/tests/unit_tests/mcp/test_adapter.py b/libs/langchain_v1/tests/unit_tests/mcp/test_adapter.py index 2e8750e1ad..72f404101b 100644 --- a/libs/langchain_v1/tests/unit_tests/mcp/test_adapter.py +++ b/libs/langchain_v1/tests/unit_tests/mcp/test_adapter.py @@ -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") diff --git a/libs/langchain_v1/uv.lock b/libs/langchain_v1/uv.lock index 34084acbd4..99c84d4cfd 100644 --- a/libs/langchain_v1/uv.lock +++ b/libs/langchain_v1/uv.lock @@ -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 = [