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
126 lines
3.2 KiB
Python
126 lines
3.2 KiB
Python
"""FastMCP Diagnostics Server - for testing tracing, errors, and observability."""
|
|
|
|
import asyncio
|
|
import os
|
|
import subprocess
|
|
from collections.abc import AsyncIterator
|
|
from contextlib import asynccontextmanager
|
|
from pathlib import Path
|
|
|
|
import httpx
|
|
|
|
from fastmcp import FastMCP
|
|
from fastmcp.server import create_proxy
|
|
|
|
ECHO_SERVER_PORT = 8002
|
|
ECHO_SERVER_URL = f"http://localhost:{ECHO_SERVER_PORT}/sse"
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(server: FastMCP) -> AsyncIterator[None]:
|
|
"""Start echo server subprocess and mount proxy to it."""
|
|
echo_path = Path(__file__).parent.parent / "echo.py"
|
|
|
|
# Pass OTEL config to subprocess with different service name
|
|
env = os.environ.copy()
|
|
env["OTEL_SERVICE_NAME"] = "fastmcp-echo-server"
|
|
|
|
# Start echo server as subprocess using run_with_tracing.py for OTEL export
|
|
run_with_tracing = Path(__file__).parent.parent / "run_with_tracing.py"
|
|
proc = subprocess.Popen(
|
|
[
|
|
"uv",
|
|
"run",
|
|
str(run_with_tracing),
|
|
str(echo_path),
|
|
"--transport",
|
|
"sse",
|
|
"--port",
|
|
str(ECHO_SERVER_PORT),
|
|
],
|
|
env=env,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
)
|
|
|
|
# Wait for server to be ready (async to avoid blocking event loop)
|
|
async with httpx.AsyncClient() as client:
|
|
for _ in range(50):
|
|
try:
|
|
await client.get(
|
|
f"http://localhost:{ECHO_SERVER_PORT}/sse", timeout=0.1
|
|
)
|
|
break
|
|
except Exception:
|
|
await asyncio.sleep(0.1)
|
|
|
|
# Mount proxy to the running echo server
|
|
echo_proxy = create_proxy(ECHO_SERVER_URL, name="Echo Proxy")
|
|
server.mount(echo_proxy, namespace="proxied")
|
|
|
|
try:
|
|
yield
|
|
finally:
|
|
proc.terminate()
|
|
try:
|
|
proc.wait(timeout=5)
|
|
except subprocess.TimeoutExpired:
|
|
proc.kill()
|
|
proc.wait()
|
|
|
|
|
|
mcp = FastMCP("Diagnostics Server", lifespan=lifespan)
|
|
|
|
# === SUCCESSFUL COMPONENTS ===
|
|
|
|
|
|
@mcp.tool
|
|
def ping() -> str:
|
|
"""Simple ping tool - always succeeds."""
|
|
return "pong"
|
|
|
|
|
|
@mcp.resource("diag://status")
|
|
def status_resource() -> str:
|
|
"""Status resource - always succeeds."""
|
|
return "OK"
|
|
|
|
|
|
@mcp.resource("diag://echo/{message}")
|
|
def echo_template(message: str) -> str:
|
|
"""Echo template - always succeeds."""
|
|
return f"Echo: {message}"
|
|
|
|
|
|
@mcp.prompt("greet")
|
|
def greet_prompt(name: str = "World") -> str:
|
|
"""Greeting prompt - always succeeds."""
|
|
return f"Hello, {name}!"
|
|
|
|
|
|
# === ERROR COMPONENTS ===
|
|
|
|
|
|
@mcp.tool
|
|
def fail_tool(message: str = "Intentional tool failure") -> str:
|
|
"""Tool that always raises ValueError - for error tracing."""
|
|
raise ValueError(message)
|
|
|
|
|
|
@mcp.resource("diag://error")
|
|
def error_resource() -> str:
|
|
"""Resource that always raises ValueError."""
|
|
raise ValueError("Intentional resource failure")
|
|
|
|
|
|
@mcp.resource("diag://error/{code}")
|
|
def error_template(code: str) -> str:
|
|
"""Template that always raises ValueError."""
|
|
raise ValueError(f"Intentional template failure: {code}")
|
|
|
|
|
|
@mcp.prompt("fail")
|
|
def fail_prompt() -> str:
|
|
"""Prompt that always raises ValueError."""
|
|
raise ValueError("Intentional prompt failure")
|