MCP Distributed Tracing
Rastir supports distributed tracing across MCP (Model Context Protocol) tool boundaries. Trace context flows automatically from agent to tool server using W3C traceparent HTTP headers.
How It Works
Rastir uses W3C traceparent HTTP headers — the industry-standard approach for distributed tracing.
Client side: Framework decorators (@langgraph_agent, @crew_kickoff, @llamaindex_agent) auto-discover MCP client objects and set the traceparent header before each tool call. No client-side code needed.
Server side: RastirMCPMiddleware (ASGI middleware) reads traceparent from incoming HTTP requests. @mcp_endpoint creates server spans linked to the client’s trace.
┌──────────────────────────────┐ ┌──────────────────────────────┐
│ Client (your agent) │ │ MCP Tool Server │
│ │ │ │
│ @langgraph_agent │ │ RastirMCPMiddleware │
│ └── auto-discovers MCP │ HTTP │ reads traceparent header │
│ client, sets │ ────────▸ │ │
│ traceparent header │ header │ @mcp.tool() │
│ │ │ @mcp_endpoint │
│ client span: remote=true │ │ server span: remote=false │
└──────────────────────────────┘ └──────────────────────────────┘
W3C traceparent format: 00-<32-char-trace-id>-<16-char-span-id>-01
Framework Integration (Zero Config)
When using framework decorators, MCP trace propagation is automatic:
LangGraph
from rastir import configure, langgraph_agent
from langchain_mcp_adapters.client import MultiServerMCPClient
configure(service="my-agent", push_url="http://localhost:8080")
async def run():
async with MultiServerMCPClient({
"weather": {"url": "http://localhost:9000/mcp", "transport": "streamable_http"},
}) as mcp_client:
tools = mcp_client.get_tools()
llm = ChatGoogleGenerativeAI(model="gemini-2.5-flash")
graph = create_react_agent(llm, tools)
@langgraph_agent(agent_name="react")
async def invoke(graph, mcp_client):
# traceparent is auto-injected into mcp_client.connections headers
return await graph.ainvoke({"messages": [("user", "What's the weather?")]})
return await invoke(graph, mcp_client)
CrewAI
from rastir import configure, crew_kickoff
from crewai import Agent, Task, Crew
from crewai.mcp.config import MCPServerHTTP
configure(service="my-agent", push_url="http://localhost:8080")
mcp_server = MCPServerHTTP(url="http://localhost:9000/mcp")
agent = Agent(
role="researcher",
llm="gemini/gemini-2.5-flash",
mcp_servers=[mcp_server], # Rastir auto-discovers this
)
@crew_kickoff(agent_name="research_crew")
def run(crew):
return crew.kickoff()
LlamaIndex
from rastir import configure, llamaindex_agent
from llama_index.tools.mcp import BasicMCPClient, McpToolSpec
configure(service="my-agent", push_url="http://localhost:8080")
mcp_client = BasicMCPClient("http://localhost:9000/mcp")
@llamaindex_agent(agent_name="qa")
async def run(agent, mcp_client):
# traceparent is auto-injected into mcp_client.headers
return await agent.achat("What's the weather?")
Server Side Setup
The MCP server needs two things: RastirMCPMiddleware to read headers, and @mcp_endpoint to create server spans.
# ── server.py ──
from mcp.server.fastmcp import FastMCP
from rastir import configure, mcp_endpoint
from rastir.remote import RastirMCPMiddleware
# Server must configure() independently
configure(service="tool-server", push_url="http://localhost:8080")
mcp = FastMCP("weather-server", host="0.0.0.0", port=9000, stateless_http=True)
@mcp.tool()
@mcp_endpoint
async def get_weather(city: str) -> str:
"""Get weather for a city."""
return f"22°C, sunny in {city}"
if __name__ == "__main__":
# Wrap the ASGI app with middleware
app = mcp.streamable_http_app()
app = RastirMCPMiddleware(app)
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=9000)
Important: The MCP server process must call
configure(push_url=...)independently. Without this,@mcp_endpointspans are created but never exported.
Standalone Usage (wrap_mcp)
For direct MCP SDK usage without a framework decorator:
from rastir import configure, agent_span, wrap_mcp
from mcp.client.streamable_http import streamable_http_client
from mcp.client.session import ClientSession
configure(service="my-agent", push_url="http://localhost:8080")
@agent_span(agent_name="weather_agent")
async def run():
async with streamable_http_client("http://localhost:9000/mcp") as (r, w, _):
async with ClientSession(r, w) as session:
await session.initialize()
session = wrap_mcp(session) # creates client spans per tool call
result = await session.call_tool("get_weather", {"city": "Tokyo"})
return result
With http_client for header propagation to standalone httpx clients:
import httpx
from rastir import wrap_mcp
http_client = httpx.AsyncClient()
session = wrap_mcp(session, http_client=http_client)
# traceparent header is set on http_client.headers before each call_tool()
Manual Header Injection
For custom scenarios, use traceparent_headers():
from rastir import traceparent_headers
headers = traceparent_headers()
# Returns {"traceparent": "00-<trace_id>-<span_id>-01"} or {} if no active span
API Reference
wrap_mcp(session, *, http_client=None)
Wraps an MCP ClientSession with a transparent proxy. Only call_tool() is intercepted.
Client span attributes:
| Attribute | Type | Source | Description |
|---|---|---|---|
tool_name | string | call | MCP tool name |
remote | string | auto | Always "true" |
agent | string | context | Parent @agent name (if present) |
model | string | context | From @llm context |
provider | string | context | From @llm context |
@mcp_endpoint
Server-side decorator placed under @mcp.tool(). Creates a child span linked to the client’s trace context (read from _incoming_trace_context ContextVar set by RastirMCPMiddleware).
Server span attributes:
| Attribute | Type | Source | Description |
|---|---|---|---|
tool_name | string | auto | Server function name |
remote | string | auto | Always "false" |
RastirMCPMiddleware(app)
ASGI middleware that reads traceparent from incoming HTTP requests and stores parsed trace context in a ContextVar for @mcp_endpoint.
traceparent_headers()
Returns {"traceparent": "00-<trace_id>-<span_id>-01"} from the current active span, or {} if no span exists.
Trace Topology
Agent Span
└── Tool Client Span (remote="true")
└── Tool Server Span (remote="false") ← same trace_id
Both spans share the same traceId. The server span’s parentSpanId points to the client span.
With and Without @mcp_endpoint
| Scenario | Client span | Server span | Trace propagation |
|---|---|---|---|
Server uses RastirMCPMiddleware + @mcp_endpoint | ✅ | ✅ | Full |
| Server does NOT use either | ✅ | ❌ | Client-side only |
When the server doesn’t use @mcp_endpoint, the traceparent header is sent but ignored. The client span is still created — you just don’t get server-side visibility.