fastmcp/tests/server/mount/test_proxy.py
Jeremiah Lowin 6a6fdcb2bb
Repoint tests and examples off removed deprecations
Replaces FastMCP.as_proxy() helper calls with create_proxy(), rewrites the
mount() as_proxy=/prefix= kwarg tests to plain mount() (the params are gone),
and deletes deprecation-only tests for as_proxy() and remove_tool().
2026-07-07 07:53:11 -04:00

225 lines
7.5 KiB
Python

"""Tests for proxy server mounting."""
import json
from contextlib import asynccontextmanager
from fastmcp import FastMCP
from fastmcp.client import Client
from fastmcp.client.transports import FastMCPTransport
from fastmcp.server import create_proxy
from fastmcp.server.providers import FastMCPProvider
from fastmcp.server.providers.wrapped_provider import _WrappedProvider
from fastmcp.server.transforms import Namespace
class TestProxyServer:
"""Test mounting a proxy server."""
async def test_mount_proxy_server(self):
"""Test mounting a proxy server."""
# Create original server
original_server = FastMCP("OriginalServer")
@original_server.tool
def get_data(query: str) -> str:
return f"Data for {query}"
# Create proxy server
proxy_server = create_proxy(FastMCPTransport(original_server))
# Mount proxy server
main_app = FastMCP("MainApp")
main_app.mount(proxy_server, "proxy")
# Tool should be accessible through main app
tools = await main_app.list_tools()
assert any(t.name == "proxy_get_data" for t in tools)
# Call the tool
result = await main_app.call_tool("proxy_get_data", {"query": "test"})
assert result.structured_content == {"result": "Data for test"}
async def test_dynamically_adding_to_proxied_server(self):
"""Test that changes to the original server are reflected in the mounted proxy."""
# Create original server
original_server = FastMCP("OriginalServer")
# Create proxy server
proxy_server = create_proxy(FastMCPTransport(original_server))
# Mount proxy server
main_app = FastMCP("MainApp")
main_app.mount(proxy_server, "proxy")
# Add a tool to the original server
@original_server.tool
def dynamic_data() -> str:
return "Dynamic data"
# Tool should be accessible through main app via proxy
tools = await main_app.list_tools()
assert any(t.name == "proxy_dynamic_data" for t in tools)
# Call the tool
result = await main_app.call_tool("proxy_dynamic_data", {})
assert result.structured_content == {"result": "Dynamic data"}
async def test_proxy_server_with_resources(self):
"""Test mounting a proxy server with resources."""
# Create original server
original_server = FastMCP("OriginalServer")
@original_server.resource(uri="config://settings")
def get_config() -> str:
return json.dumps({"api_key": "12345"})
# Create proxy server
proxy_server = create_proxy(FastMCPTransport(original_server))
# Mount proxy server
main_app = FastMCP("MainApp")
main_app.mount(proxy_server, "proxy")
# Resource should be accessible through main app
result = await main_app.read_resource("config://proxy/settings")
assert len(result.contents) == 1
config = json.loads(result.contents[0].content)
assert config["api_key"] == "12345"
async def test_proxy_server_with_prompts(self):
"""Test mounting a proxy server with prompts."""
# Create original server
original_server = FastMCP("OriginalServer")
@original_server.prompt
def welcome(name: str) -> str:
return f"Welcome, {name}!"
# Create proxy server
proxy_server = create_proxy(FastMCPTransport(original_server))
# Mount proxy server
main_app = FastMCP("MainApp")
main_app.mount(proxy_server, "proxy")
# Prompt should be accessible through main app
result = await main_app.render_prompt("proxy_welcome", {"name": "World"})
assert result.messages is not None
# The message should contain our welcome text
class TestMountProviderStructure:
"""Test the provider structure produced by mounting."""
async def test_mount_wraps_in_fastmcp_provider(self):
mcp = FastMCP("Main")
sub = FastMCP("Sub")
@sub.tool
def sub_tool() -> str:
return "test"
mcp.mount(sub, "sub")
# Index 1 because LocalProvider is at index 0
provider = mcp.providers[1]
# Provider is wrapped with Namespace transform
assert isinstance(provider, _WrappedProvider)
assert len(provider._transforms) == 1
assert isinstance(provider._transforms[0], Namespace)
# Inner provider is FastMCPProvider
assert isinstance(provider._inner, FastMCPProvider)
assert provider._inner.server is sub
# Verify namespace is applied
tools = await mcp.list_tools()
assert {t.name for t in tools} == {"sub_sub_tool"}
async def test_lifespan_server_mounted_directly(self):
"""Test that servers with lifespan are mounted directly (not auto-proxied).
Since FastMCPProvider now handles lifespan via the provider lifespan interface,
there's no need to auto-convert to a proxy. The server is mounted directly.
"""
@asynccontextmanager
async def server_lifespan(mcp: FastMCP):
yield
mcp = FastMCP("Main")
sub = FastMCP("Sub", lifespan=server_lifespan)
@sub.tool
def sub_tool() -> str:
return "test"
mcp.mount(sub, "sub")
# Server should be mounted directly without auto-proxying
# Index 1 because LocalProvider is at index 0
provider = mcp.providers[1]
# Provider is wrapped with Namespace transform
assert isinstance(provider, _WrappedProvider)
assert len(provider._transforms) == 1
assert isinstance(provider._transforms[0], Namespace)
# Inner provider is FastMCPProvider
assert isinstance(provider._inner, FastMCPProvider)
assert provider._inner.server is sub
# Verify namespace is applied
tools = await mcp.list_tools()
assert {t.name for t in tools} == {"sub_sub_tool"}
async def test_mounting_a_proxy_preserves_the_proxy(self):
mcp = FastMCP("Main")
sub = FastMCP("Sub")
sub_proxy = create_proxy(FastMCPTransport(sub))
mcp.mount(sub_proxy, "sub")
# Index 1 because LocalProvider is at index 0
provider = mcp.providers[1]
# Provider is wrapped with Namespace transform
assert isinstance(provider, _WrappedProvider)
assert len(provider._transforms) == 1
assert isinstance(provider._transforms[0], Namespace)
# Inner provider is FastMCPProvider
assert isinstance(provider._inner, FastMCPProvider)
assert provider._inner.server is sub_proxy
async def test_mounts_have_live_link(self):
mcp = FastMCP("Main")
sub = FastMCP("Sub")
mcp.mount(sub, "sub")
assert len(await mcp.list_tools()) == 0
@sub.tool
def hello():
return "hi"
assert len(await mcp.list_tools()) == 1
async def test_sub_lifespan_is_executed(self):
lifespan_check = []
@asynccontextmanager
async def lifespan(mcp: FastMCP):
lifespan_check.append("start")
yield
mcp = FastMCP("Main")
sub = FastMCP("Sub", lifespan=lifespan)
@sub.tool
def hello():
return "hi"
mcp.mount(sub)
assert lifespan_check == []
async with Client(mcp) as client:
await client.call_tool("hello", {})
# Lifespan is executed at least once (may be multiple times for proxy connections)
assert len(lifespan_check) >= 1
assert all(x == "start" for x in lifespan_check)