Skip to content

Commit 7353161

Browse files
committed
♻️ Use Starlette middleware to connect FastMCP traces
Signed-off-by: Evaline Ju <69598118+evaline-ju@users.noreply.github.com>
1 parent 3f9e50f commit 7353161

2 files changed

Lines changed: 22 additions & 2 deletions

File tree

mcp/weather_tool/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ dependencies = [
1111
"urllib3>=2.6.3", # Indirect; prevents CVE-2025-66418
1212
"python-multipart>=0.0.22", # Indirect; prevents CVE-2026-24486
1313
"fastmcp>=3.2.0", # Indirect; prevents CVE-2026-32871
14+
"uvicorn>=0.34.0",
1415
"cryptography>=46.0.5", # Indirect; prevents CVE-2026-26007
1516
"starlette>=0.49.1", # Indirect; prevents CVE-2025-62727
1617
"opentelemetry-api>=1.27.0",

mcp/weather_tool/weather_tool.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@
77

88
import httpx
99
import requests
10+
import uvicorn
1011
from fastmcp import FastMCP
1112
from requests.adapters import HTTPAdapter
13+
from starlette.middleware import Middleware
14+
from starlette.middleware.base import BaseHTTPMiddleware
1215
from urllib3.util.retry import Retry
1316
from fastmcp.server.dependencies import get_http_headers
1417

@@ -157,15 +160,31 @@ async def get_weather(city: str) -> str:
157160

158161

159162

163+
async def _trace_propagation_middleware(request, call_next):
164+
"""Extract W3C traceparent from HTTP headers before FastMCP creates its span.
165+
166+
FastMCP creates its own root span per request. Without this middleware,
167+
that span has no parent, producing a disconnected trace in Phoenix/Jaeger.
168+
Attaching the incoming context here makes FastMCP's span a child of the
169+
caller's span automatically, since OTEL picks up the ambient context.
170+
"""
171+
incoming_ctx = extract(dict(request.headers))
172+
token = otel_context.attach(incoming_ctx)
173+
try:
174+
return await call_next(request)
175+
finally:
176+
otel_context.detach(token)
177+
178+
160179
# host can be specified with HOST env variable
161180
# transport can be specified with MCP_TRANSPORT env variable (defaults to streamable-http)
162181
def run_server():
163182
"Run the MCP server"
164183
setup_tracing()
165-
transport = os.getenv("MCP_TRANSPORT", "streamable-http")
166184
host = os.getenv("HOST", "0.0.0.0")
167185
port = int(os.getenv("PORT", "8000"))
168-
mcp.run(transport=transport, host=host, port=port)
186+
app = mcp.http_app(middleware=[Middleware(BaseHTTPMiddleware, dispatch=_trace_propagation_middleware)])
187+
uvicorn.run(app, host=host, port=port)
169188

170189

171190
if __name__ == "__main__":

0 commit comments

Comments
 (0)