chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:39:17 +08:00
commit 4ed4e9ff99
1368 changed files with 334957 additions and 0 deletions
+78
View File
@@ -0,0 +1,78 @@
# MCP Manager Example (FastAPI)
This example shows how to use `MCPServerManager` to keep MCP server lifecycle management in a single task inside a FastAPI app with the Streamable HTTP transport.
## Run the MCP server (Streamable HTTP)
```
uv run python examples/mcp/manager_example/mcp_server.py
```
The server listens at `http://localhost:8000/mcp` by default.
You can override the host/port with:
```
export STREAMABLE_HTTP_HOST=127.0.0.1
export STREAMABLE_HTTP_PORT=8000
```
This example also configures an inactive MCP server at `http://localhost:8001/mcp` to demonstrate how the manager drops failed
servers. You can override it with:
```
export INACTIVE_MCP_SERVER_URL=http://localhost:8001/mcp
```
## Run the FastAPI app
```
uv run python examples/mcp/manager_example/app.py
```
The app listens at `http://127.0.0.1:9001`.
## Run the smoke test
To verify the MCP manager and app integration without calling a model:
```
uv run python -m examples.mcp.manager_example.smoke_test
```
The smoke test starts the local MCP server on a temporary port, points both app MCP server settings at that server, and checks `/health`, `/tools`, and `/add`.
## Toggle MCP manager usage
By default, the app uses `MCPServerManager`. To disable it:
```
export USE_MCP_MANAGER=0
```
## Try the endpoints
```
curl http://127.0.0.1:9001/health
curl http://127.0.0.1:9001/tools
curl -X POST http://127.0.0.1:9001/add \
-H 'Content-Type: application/json' \
-d '{"a": 2, "b": 3}'
```
Reconnect failed MCP servers (manager must be enabled):
```
curl -X POST http://127.0.0.1:9001/reconnect \
-H 'Content-Type: application/json' \
-d '{"failed_only": true}'
```
To use `/run`, set `OPENAI_API_KEY`:
```
export OPENAI_API_KEY=...
curl -X POST http://127.0.0.1:9001/run \
-H 'Content-Type: application/json' \
-d '{"input": "Add 4 and 9."}'
```
+130
View File
@@ -0,0 +1,130 @@
import os
from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from agents import Agent, Runner
from agents.mcp import MCPServer, MCPServerManager, MCPServerStreamableHttp
from agents.model_settings import ModelSettings
MCP_SERVER_URL = os.getenv("MCP_SERVER_URL", "http://localhost:8000/mcp")
INACTIVE_MCP_SERVER_URL = os.getenv("INACTIVE_MCP_SERVER_URL", "http://localhost:8001/mcp")
APP_HOST = "127.0.0.1"
APP_PORT = 9001
USE_MCP_MANAGER = os.getenv("USE_MCP_MANAGER", "1") != "0"
class AddRequest(BaseModel):
a: int
b: int
class RunRequest(BaseModel):
input: str
class ReconnectRequest(BaseModel):
failed_only: bool = True
@asynccontextmanager
async def lifespan(app: FastAPI):
server = MCPServerStreamableHttp({"url": MCP_SERVER_URL})
inactive_server = MCPServerStreamableHttp({"url": INACTIVE_MCP_SERVER_URL})
servers = [server, inactive_server]
if USE_MCP_MANAGER:
async with MCPServerManager(
servers=servers,
connect_in_parallel=True,
) as manager:
app.state.mcp_manager = manager
app.state.mcp_servers = servers
yield
return
await server.connect()
app.state.mcp_servers = servers
app.state.active_servers = [server]
try:
yield
finally:
await server.cleanup()
app = FastAPI(lifespan=lifespan)
@app.get("/health")
async def health() -> dict[str, object]:
if USE_MCP_MANAGER:
manager: MCPServerManager = app.state.mcp_manager
return {
"connected_servers": [server.name for server in manager.active_servers],
"failed_servers": [server.name for server in manager.failed_servers],
}
active_servers = _get_active_servers()
return {
"connected_servers": [server.name for server in active_servers],
"failed_servers": [],
}
@app.get("/tools")
async def list_tools() -> dict[str, object]:
active_servers = _get_active_servers()
if not active_servers:
return {"tools": []}
tools = await active_servers[0].list_tools()
return {"tools": [tool.name for tool in tools]}
@app.post("/add")
async def add(req: AddRequest) -> dict[str, object]:
active_servers = _get_active_servers()
if not active_servers:
raise HTTPException(status_code=503, detail="No MCP servers available")
result = await active_servers[0].call_tool("add", {"a": req.a, "b": req.b})
return {"result": result.model_dump(mode="json")}
@app.post("/run")
async def run_agent(req: RunRequest) -> dict[str, object]:
if not os.getenv("OPENAI_API_KEY"):
raise HTTPException(status_code=400, detail="OPENAI_API_KEY is required")
servers = _get_active_servers()
if not servers:
raise HTTPException(status_code=503, detail="No MCP servers available")
agent = Agent(
name="FastAPI Agent",
instructions="Use the MCP tools when needed.",
mcp_servers=servers,
model_settings=ModelSettings(tool_choice="auto"),
)
result = await Runner.run(starting_agent=agent, input=req.input)
return {"output": result.final_output}
@app.post("/reconnect")
async def reconnect(req: ReconnectRequest) -> dict[str, object]:
if not USE_MCP_MANAGER:
raise HTTPException(status_code=400, detail="MCPServerManager is disabled")
manager: MCPServerManager = app.state.mcp_manager
servers = await manager.reconnect(failed_only=req.failed_only)
return {"connected_servers": [server.name for server in servers]}
def _get_active_servers() -> list[MCPServer]:
if USE_MCP_MANAGER:
manager: MCPServerManager = app.state.mcp_manager
return list(manager.active_servers)
return list(app.state.active_servers)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host=APP_HOST, port=APP_PORT)
@@ -0,0 +1,26 @@
import os
from mcp.server.fastmcp import FastMCP
STREAMABLE_HTTP_HOST = os.getenv("STREAMABLE_HTTP_HOST", "127.0.0.1")
STREAMABLE_HTTP_PORT = int(os.getenv("STREAMABLE_HTTP_PORT", "8000"))
mcp = FastMCP(
"FastAPI Example Server",
host=STREAMABLE_HTTP_HOST,
port=STREAMABLE_HTTP_PORT,
)
@mcp.tool()
def add(a: int, b: int) -> int:
return a + b
@mcp.tool()
def echo(message: str) -> str:
return f"echo: {message}"
if __name__ == "__main__":
mcp.run(transport="streamable-http")
+144
View File
@@ -0,0 +1,144 @@
"""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())