Graceful degradation for multi-server proxy setup (#3546)

* Graceful degradation for multi-server proxy setup

* Add comment for broad except, parameterize test ordering
This commit is contained in:
Jeremiah Lowin 2026-03-18 15:25:56 -04:00 committed by GitHub
commit 02d55dec32
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 165 additions and 11 deletions

View file

@ -17,6 +17,9 @@ from fastmcp.mcp_config import (
TransformingStdioMCPServer,
)
from fastmcp.server.server import FastMCP, create_proxy
from fastmcp.utilities.logging import get_logger
logger = get_logger(__name__)
class MCPConfigTransport(ClientTransport):
@ -104,21 +107,25 @@ class MCPConfigTransport(ClientTransport):
await t.close()
self._transports = []
try:
for name, server_config in self.config.mcpServers.items():
for name, server_config in self.config.mcpServers.items():
try:
transport, _client, proxy = await self._create_proxy(
name, server_config, timeout, stack
)
self._transports.append(transport)
composite.mount(
proxy, namespace=name if self.name_as_prefix else None
except Exception: # Broad catch is intentional: failure modes
# are diverse (OSError, TimeoutError, RuntimeError, etc.)
# and the whole point is to skip any server that can't connect.
logger.warning(
"Failed to connect to MCP server %r, skipping",
name,
exc_info=True,
)
except Exception:
# Clean up any transports created before the failure
for t in self._transports:
await t.close()
self._transports = []
raise
continue
self._transports.append(transport)
composite.mount(proxy, namespace=name if self.name_as_prefix else None)
if not self._transports:
raise ConnectionError("All MCP servers failed to connect")
async with FastMCPTransport(mcp=composite).connect_session(
**session_kwargs

View file

@ -1007,6 +1007,153 @@ async def test_single_server_config_transport():
assert len(transport._transports) == 1
@pytest.mark.parametrize(
"server_order",
[
{"good_server": True, "bad_server": False},
{"bad_server": False, "good_server": True},
],
ids=["good_first", "bad_first"],
)
async def test_multi_server_partial_failure(tmp_path: Path, server_order: dict):
"""When one server fails to connect, the others should still work."""
server_script = inspect.cleandoc("""
from fastmcp import FastMCP
mcp = FastMCP()
@mcp.tool
def add(a: int, b: int) -> int:
return a + b
if __name__ == '__main__':
mcp.run()
""")
script_path = tmp_path / "test.py"
script_path.write_text(server_script)
servers = {}
for name, is_good in server_order.items():
if is_good:
servers[name] = {
"command": "python",
"args": [str(script_path)],
}
else:
servers[name] = {
"command": "this-command-does-not-exist-anywhere",
"args": [],
}
client = Client({"mcpServers": servers})
async with client:
tools = await client.list_tools()
tool_names = [t.name for t in tools]
assert "good_server_add" in tool_names
assert len(tools) == 1
async def test_multi_server_partial_failure_logs_warning(tmp_path: Path, caplog):
"""A warning should be logged when a server fails to connect."""
server_script = inspect.cleandoc("""
from fastmcp import FastMCP
mcp = FastMCP()
@mcp.tool
def add(a: int, b: int) -> int:
return a + b
if __name__ == '__main__':
mcp.run()
""")
script_path = tmp_path / "test.py"
script_path.write_text(server_script)
config = {
"mcpServers": {
"good_server": {
"command": "python",
"args": [str(script_path)],
},
"bad_server": {
"command": "this-command-does-not-exist-anywhere",
"args": [],
},
}
}
with caplog.at_level(logging.WARNING):
async with Client(config):
pass
warning_records = [
r
for r in caplog.records
if r.levelno == logging.WARNING and "bad_server" in r.message
]
assert len(warning_records) == 1
async def test_multi_server_all_fail():
"""When all servers fail to connect, a ConnectionError should be raised."""
config = MCPConfig(
mcpServers={
"bad_1": StdioMCPServer(
command="this-command-does-not-exist-anywhere",
args=[],
),
"bad_2": StdioMCPServer(
command="this-other-command-does-not-exist-either",
args=[],
),
}
)
transport = MCPConfigTransport(config)
with pytest.raises(ConnectionError, match="All MCP servers failed to connect"):
async with transport.connect_session():
pass
async def test_multi_server_partial_failure_cleanup(tmp_path: Path):
"""Transports for failed servers should not leak into _transports."""
server_script = inspect.cleandoc("""
from fastmcp import FastMCP
mcp = FastMCP()
@mcp.tool
def ping() -> str:
return "pong"
if __name__ == '__main__':
mcp.run()
""")
script_path = tmp_path / "test.py"
script_path.write_text(server_script)
config = {
"mcpServers": {
"working": {
"command": "python",
"args": [str(script_path)],
},
"broken": {
"command": "this-command-does-not-exist-anywhere",
"args": [],
},
}
}
transport = MCPConfigTransport(config)
async with transport.connect_session():
assert len(transport._transports) == 1
def sample_tool_fn(arg1: int, arg2: str) -> str:
return f"Hello, world! {arg1} {arg2}"