项目文件夹

文件
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:39:59 +08:00

226 行
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)