Files
wehub-resource-sync 60e0ffc959
Schema Crash Test / Real-world schema crash test (232K schemas) (push) Waiting to run
Run static analysis / static_analysis (push) Waiting to run
Tests / Tests: Python 3.10 on ubuntu-latest (push) Waiting to run
Tests / Tests: Python 3.13 on ubuntu-latest (push) Waiting to run
Tests / Tests: Python 3.10 on windows-latest (push) Waiting to run
Tests / Tests with lowest-direct dependencies (push) Waiting to run
Tests / Package install smoke (push) Waiting to run
Upgrade checks / Static analysis (push) Waiting to run
Upgrade checks / Tests: Python 3.10 on ubuntu-latest (push) Waiting to run
Upgrade checks / Tests: Python 3.13 on ubuntu-latest (push) Waiting to run
Upgrade checks / Tests: Python 3.10 on windows-latest (push) Waiting to run
Upgrade checks / Integration tests (push) Waiting to run
Upgrade checks / Notify on failure (push) Blocked by required conditions
Upgrade checks / Close issue on success (push) Blocked by required conditions
Update MCPServerConfig Schema / update-config-schema (push) Waiting to run
Update SDK Documentation / update-sdk-docs (push) Waiting to run
Tests / MCP conformance tests (push) Waiting to run
Tests / Integration tests (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 12:39:59 +08:00

292 lines
9.7 KiB
Python

import asyncio
import json
import sys
from contextlib import suppress
from unittest.mock import AsyncMock, call
import pytest
from mcp import MCPError
from mcp_types import TextResourceContents
from fastmcp import Context
from fastmcp.client import Client
from fastmcp.client.transports import StreamableHttpTransport
from fastmcp.server.dependencies import get_http_request
from fastmcp.server.server import FastMCP
from fastmcp.utilities.tests import run_server_async
def create_test_server() -> FastMCP:
"""Create a FastMCP server with tools, resources, and prompts."""
server = FastMCP("TestServer")
@server.tool
def greet(name: str) -> str:
"""Greet someone by name."""
return f"Hello, {name}!"
@server.tool
async def elicit(ctx: Context) -> str:
"""Elicit a response from the user."""
result = await ctx.elicit("What is your name?", response_type=str)
if result.action == "accept":
return f"You said your name was: {result.data}!"
else:
return "No name provided"
@server.tool
def add(a: int, b: int) -> int:
"""Add two numbers together."""
return a + b
@server.tool
async def sleep(seconds: float) -> str:
"""Sleep for a given number of seconds."""
await asyncio.sleep(seconds)
return f"Slept for {seconds} seconds"
@server.tool
async def greet_with_progress(name: str, ctx: Context) -> str:
"""Report progress for a greeting."""
await ctx.report_progress(0.5, 1.0, "Greeting in progress")
await ctx.report_progress(0.75, 1.0, "Almost there!")
return f"Hello, {name}!"
@server.resource(uri="data://users")
async def get_users() -> str:
import json
return json.dumps(["Alice", "Bob", "Charlie"])
@server.resource(uri="data://user/{user_id}")
async def get_user(user_id: str) -> str:
import json
return json.dumps({"id": user_id, "name": f"User {user_id}", "active": True})
@server.resource(uri="request://headers")
async def get_headers() -> str:
import json
request = get_http_request()
return json.dumps(dict(request.headers))
@server.prompt
def welcome(name: str) -> str:
"""Example greeting prompt."""
return f"Welcome to FastMCP, {name}!"
return server
@pytest.fixture
async def streamable_http_server(request):
"""Start a test server and return its URL."""
import fastmcp
stateless_http = getattr(request, "param", False)
if stateless_http:
fastmcp.settings.stateless_http = True
server = create_test_server()
async with run_server_async(server) as url:
yield url
if stateless_http:
fastmcp.settings.stateless_http = False
@pytest.fixture
async def streamable_http_server_with_streamable_http_alias():
"""Test that the "streamable-http" transport alias works."""
server = create_test_server()
async with run_server_async(server, transport="streamable-http") as url:
yield url
@pytest.fixture
async def nested_server():
"""Test nested server mounts with Starlette."""
import uvicorn
from starlette.applications import Starlette
from starlette.routing import Mount
from fastmcp.utilities.http import find_available_port
mcp_server = create_test_server()
mcp_app = mcp_server.http_app(path="/final/mcp")
# Nest the app under multiple mounts to test URL resolution
inner = Starlette(routes=[Mount("/nest-inner", app=mcp_app)])
outer = Starlette(
routes=[Mount("/nest-outer", app=inner)], lifespan=mcp_app.lifespan
)
# Run uvicorn with the nested ASGI app
port = find_available_port()
config = uvicorn.Config(
app=outer,
host="127.0.0.1",
port=port,
log_level="critical",
ws="websockets-sansio",
timeout_graceful_shutdown=0,
)
uvicorn_server = uvicorn.Server(config)
server_task = asyncio.create_task(uvicorn_server.serve())
await asyncio.sleep(0.1)
yield f"http://127.0.0.1:{port}/nest-outer/nest-inner/final/mcp"
# Graceful shutdown - required for uvicorn 0.39+ due to context isolation
uvicorn_server.should_exit = True
with suppress(asyncio.CancelledError, asyncio.TimeoutError):
await asyncio.wait_for(server_task, timeout=2.0)
async def test_ping(streamable_http_server: str):
"""Test pinging the server."""
async with Client(
transport=StreamableHttpTransport(streamable_http_server)
) as client:
result = await client.ping()
assert result is True
async def test_ping_with_streamable_http_alias(
streamable_http_server_with_streamable_http_alias: str,
):
"""Test pinging the server."""
async with Client(
transport=StreamableHttpTransport(
streamable_http_server_with_streamable_http_alias
)
) as client:
result = await client.ping()
assert result is True
async def test_http_headers(streamable_http_server: str):
"""Test getting HTTP headers from the server."""
async with Client(
transport=StreamableHttpTransport(
streamable_http_server, headers={"X-DEMO-HEADER": "ABC"}
)
) as client:
raw_result = await client.read_resource("request://headers")
assert isinstance(raw_result[0], TextResourceContents)
json_result = json.loads(raw_result[0].text)
assert "x-demo-header" in json_result
assert json_result["x-demo-header"] == "ABC"
async def test_session_id_callback(streamable_http_server: str):
"""Test getting mcp-session-id from the transport."""
transport = StreamableHttpTransport(streamable_http_server)
assert transport.get_session_id() is None
async with Client(transport=transport):
session_id = transport.get_session_id()
assert session_id is not None
@pytest.mark.parametrize("streamable_http_server", [True, False], indirect=True)
async def test_greet_with_progress_tool(streamable_http_server: str):
"""Test calling the greet tool."""
progress_handler = AsyncMock(return_value=None)
async with Client(
transport=StreamableHttpTransport(streamable_http_server),
progress_handler=progress_handler,
) as client:
result = await client.call_tool("greet_with_progress", {"name": "Alice"})
assert result.data == "Hello, Alice!"
progress_handler.assert_has_calls(
[
call(0.5, 1.0, "Greeting in progress"),
call(0.75, 1.0, "Almost there!"),
]
)
@pytest.mark.parametrize("streamable_http_server", [True, False], indirect=True)
async def test_elicitation_tool(streamable_http_server: str, request):
"""Test calling the elicitation tool in both stateless and stateful modes."""
async def elicitation_handler(message, response_type, params, ctx):
return {"value": "Alice"}
stateless_http = request.node.callspec.params.get("streamable_http_server", False)
if stateless_http:
pytest.xfail("Elicitation is not supported in stateless HTTP mode")
async with Client(
transport=StreamableHttpTransport(streamable_http_server),
elicitation_handler=elicitation_handler,
) as client:
result = await client.call_tool("elicit")
assert result.data == "You said your name was: Alice!"
@pytest.mark.parametrize("streamable_http_server", [True], indirect=True)
async def test_stateless_http_rejects_get_sse(streamable_http_server: str):
"""Stateless servers should reject GET SSE requests with 405."""
import httpx
async with httpx.AsyncClient() as http_client:
response = await http_client.get(streamable_http_server)
assert response.status_code == 405
@pytest.mark.parametrize("streamable_http_server", [True], indirect=True)
async def test_stateless_http_still_accepts_post(streamable_http_server: str):
"""Stateless servers should still handle POST requests normally."""
async with Client(
transport=StreamableHttpTransport(streamable_http_server)
) as client:
result = await client.call_tool("greet", {"name": "World"})
assert result.data == "Hello, World!"
async def test_nested_streamable_http_server_resolves_correctly(nested_server: str):
"""Test patch for https://github.com/modelcontextprotocol/python-sdk/pull/659"""
async with Client(transport=StreamableHttpTransport(nested_server)) as client:
result = await client.ping()
assert result is True
@pytest.mark.skipif(
sys.platform == "win32",
reason="Timeout tests are flaky on Windows. Timeouts *are* supported but the tests are unreliable.",
)
class TestTimeout:
async def test_timeout(self, streamable_http_server: str):
# note this transport behaves differently than others and raises
# MCPError from the *client* context
with pytest.raises(MCPError, match="timed out"):
async with Client(
transport=StreamableHttpTransport(streamable_http_server),
timeout=0.02,
) as client:
await client.call_tool("sleep", {"seconds": 0.05})
async def test_timeout_tool_call(self, streamable_http_server: str):
async with Client(
transport=StreamableHttpTransport(streamable_http_server),
) as client:
with pytest.raises(MCPError):
await client.call_tool("sleep", {"seconds": 0.2}, timeout=0.1)
async def test_timeout_tool_call_overrides_client_timeout(
self, streamable_http_server: str
):
async with Client(
transport=StreamableHttpTransport(streamable_http_server),
timeout=2,
) as client:
with pytest.raises(MCPError):
await client.call_tool("sleep", {"seconds": 0.2}, timeout=0.1)