Files
openai--openai-agents-python/examples/mcp/manager_example/smoke_test.py
T
2026-07-13 12:39:17 +08:00

145 lines
4.6 KiB
Python

"""Smoke test for the MCP manager example app.
This script starts the sibling Streamable HTTP MCP server on a temporary local
port, loads the app with matching environment variables, and verifies the
manager-backed endpoints without calling a model.
"""
from __future__ import annotations
import asyncio
import importlib
import os
import socket
import subprocess
import sys
import time
from typing import Any, cast
import httpx
HOST = "127.0.0.1"
PORT_WAIT_SECONDS = 10.0
def _free_port() -> int:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.bind((HOST, 0))
return int(sock.getsockname()[1])
def _wait_for_port(process: subprocess.Popen[str], port: int) -> None:
deadline = time.monotonic() + PORT_WAIT_SECONDS
while time.monotonic() < deadline:
if process.poll() is not None:
output, _ = process.communicate(timeout=1)
raise RuntimeError(f"MCP server exited before it was ready:\n{output}")
try:
with socket.create_connection((HOST, port), timeout=0.2):
return
except OSError:
time.sleep(0.1)
raise RuntimeError(f"MCP server did not listen on {HOST}:{port}.")
def _stop_process(process: subprocess.Popen[str]) -> str:
if process.poll() is not None:
output, _ = process.communicate(timeout=1)
return output
process.terminate()
try:
output, _ = process.communicate(timeout=5)
except subprocess.TimeoutExpired:
process.kill()
output, _ = process.communicate(timeout=5)
return output
def _start_mcp_server(port: int) -> subprocess.Popen[str]:
env = {
**os.environ,
"STREAMABLE_HTTP_HOST": HOST,
"STREAMABLE_HTTP_PORT": str(port),
}
return subprocess.Popen(
[sys.executable, "-u", "-m", "examples.mcp.manager_example.mcp_server"],
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
)
def _load_app_module(mcp_port: int) -> Any:
mcp_server_url = f"http://{HOST}:{mcp_port}/mcp"
os.environ["MCP_SERVER_URL"] = mcp_server_url
# Point both configured MCP servers at the same temporary server so this
# smoke test stays on the clean app integration path.
os.environ["INACTIVE_MCP_SERVER_URL"] = mcp_server_url
os.environ["USE_MCP_MANAGER"] = "1"
module_name = "examples.mcp.manager_example.app"
if module_name in sys.modules:
module = importlib.reload(sys.modules[module_name])
else:
module = importlib.import_module(module_name)
return cast(Any, module)
def _require(condition: bool, message: str) -> None:
if not condition:
raise RuntimeError(message)
async def _exercise_app(mcp_port: int) -> None:
app_module = _load_app_module(mcp_port)
app = app_module.app
expected_server_url = f"http://{HOST}:{mcp_port}/mcp"
async with app.router.lifespan_context(app):
transport = httpx.ASGITransport(app=app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
health_response = await client.get("/health")
health_response.raise_for_status()
health = health_response.json()
_require(
any(expected_server_url in name for name in health["connected_servers"]),
f"Expected connected MCP server in health response: {health}",
)
_require(
health["failed_servers"] == [],
f"Expected no failed MCP servers in health response: {health}",
)
tools_response = await client.get("/tools")
tools_response.raise_for_status()
tools = tools_response.json()["tools"]
_require({"add", "echo"} <= set(tools), f"Expected add and echo tools: {tools}")
add_response = await client.post("/add", json={"a": 2, "b": 3})
add_response.raise_for_status()
add_result = add_response.json()["result"]
texts = [
item.get("text")
for item in add_result.get("content", [])
if item.get("type") == "text"
]
_require("5" in texts, f"Expected add tool result to include 5: {add_result}")
async def main() -> None:
mcp_port = _free_port()
process = _start_mcp_server(mcp_port)
try:
_wait_for_port(process, mcp_port)
await _exercise_app(mcp_port)
finally:
_stop_process(process)
print("MCP manager example smoke test completed successfully.")
if __name__ == "__main__":
asyncio.run(main())