60e0ffc959
Upgrade checks / Notify on failure (push) Has been cancelled
Upgrade checks / Close issue on success (push) Has been cancelled
Schema Crash Test / Real-world schema crash test (232K schemas) (push) Has been cancelled
Run static analysis / static_analysis (push) Has been cancelled
Tests / Tests: Python 3.10 on ubuntu-latest (push) Has been cancelled
Tests / Tests: Python 3.13 on ubuntu-latest (push) Has been cancelled
Tests / Tests: Python 3.10 on windows-latest (push) Has been cancelled
Tests / Tests with lowest-direct dependencies (push) Has been cancelled
Tests / MCP conformance tests (push) Has been cancelled
Tests / Integration tests (push) Has been cancelled
Tests / Package install smoke (push) Has been cancelled
Upgrade checks / Static analysis (push) Has been cancelled
Upgrade checks / Tests: Python 3.10 on ubuntu-latest (push) Has been cancelled
Upgrade checks / Tests: Python 3.13 on ubuntu-latest (push) Has been cancelled
Upgrade checks / Tests: Python 3.10 on windows-latest (push) Has been cancelled
Upgrade checks / Integration tests (push) Has been cancelled
Update MCPServerConfig Schema / update-config-schema (push) Has been cancelled
Update SDK Documentation / update-sdk-docs (push) Has been cancelled
226 lines
7.5 KiB
Python
226 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)
|