Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 31 additions & 16 deletions docs/servers/telemetry.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -55,36 +55,36 @@ FastMCP creates spans for all MCP operations, providing end-to-end visibility in

### Server Spans

The server creates spans for each operation:
The server creates spans for each operation using [MCP semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/mcp/):

| Span Name | Description |
|-----------|-------------|
| `tool {name}` | Tool execution (e.g., `tool get_weather`) |
| `resource {uri}` | Resource read (e.g., `resource config://database`) |
| `prompt {name}` | Prompt render (e.g., `prompt greeting`) |
| `tools/call {name}` | Tool execution (e.g., `tools/call get_weather`) |
| `resources/read {uri}` | Resource read (e.g., `resources/read config://database`) |
| `prompts/get {name}` | Prompt render (e.g., `prompts/get greeting`) |

For mounted servers, an additional `delegate {name}` span shows the delegation to the child server.

### Client Spans

The FastMCP client creates spans for outgoing requests with the same naming pattern (`tool {name}`, `resource {uri}`, `prompt {name}`).
The FastMCP client creates spans for outgoing requests with the same naming pattern (`tools/call {name}`, `resources/read {uri}`, `prompts/get {name}`).

### Span Hierarchy

Spans form a hierarchy showing the request flow. For mounted servers:

```
tool weather_forecast (CLIENT)
└── tool weather_forecast (SERVER, provider=FastMCPProvider)
tools/call weather_forecast (CLIENT)
└── tools/call weather_forecast (SERVER, provider=FastMCPProvider)
└── delegate get_weather (INTERNAL)
└── tool get_weather (SERVER, provider=LocalProvider)
└── tools/call get_weather (SERVER, provider=LocalProvider)
```

For proxy providers connecting to remote servers:

```
tool remote_search (CLIENT)
└── tool remote_search (SERVER, provider=ProxyProvider)
tools/call remote_search (CLIENT)
└── tools/call remote_search (SERVER, provider=ProxyProvider)
└── [remote server spans via trace context propagation]
```

Expand Down Expand Up @@ -191,30 +191,45 @@ def risky_operation() -> str:

## Attributes Reference

### Standard Semantic Conventions
### RPC Semantic Conventions

FastMCP uses OpenTelemetry semantic conventions where applicable:
Standard [RPC semantic conventions](https://opentelemetry.io/docs/specs/semconv/rpc/rpc-spans/):

| Attribute | Value |
|-----------|-------|
| `rpc.system` | `"mcp"` |
| `rpc.service` | Server name |
| `rpc.method` | MCP protocol method |
| `error.type` | Exception class name (on errors) |

### MCP Semantic Conventions

FastMCP implements the [OpenTelemetry MCP semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/mcp/):

| Attribute | Description |
|-----------|-------------|
| `mcp.method.name` | The MCP method being called (`tools/call`, `resources/read`, `prompts/get`) |
| `mcp.session.id` | Session identifier for the MCP connection |
| `mcp.resource.uri` | The resource URI (for resource operations) |

### Auth Attributes

Standard [identity attributes](https://opentelemetry.io/docs/specs/semconv/attributes-registry/enduser/):

| Attribute | Description |
|-----------|-------------|
| `enduser.id` | Client ID from access token (when authenticated) |
| `enduser.scope` | Space-separated OAuth scopes (when authenticated) |

### FastMCP Custom Attributes

All custom attributes use the `fastmcp.` prefix:
All custom attributes use the `fastmcp.` prefix for features unique to FastMCP:

| Attribute | Description |
|-----------|-------------|
| `fastmcp.server.name` | Server name |
| `fastmcp.component.type` | `tool`, `resource`, `prompt`, or `resource_template` |
| `fastmcp.component.key` | Full component identifier (e.g., `tool:greet`) |
| `fastmcp.provider.type` | Provider class (`LocalProvider`, `FastMCPProvider`, `ProxyProvider`) |
| `fastmcp.session.id` | Client session identifier |

Provider-specific attributes for delegation context:

Expand Down Expand Up @@ -260,5 +275,5 @@ async def test_tool_creates_span(trace_exporter: InMemorySpanExporter) -> None:
await mcp.call_tool("hello", {})

spans = trace_exporter.get_finished_spans()
assert any(s.name == "tool hello" for s in spans)
assert any(s.name == "tools/call hello" for s in spans)
```
7 changes: 4 additions & 3 deletions src/fastmcp/client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -881,10 +881,11 @@ async def read_resource_mcp(
"""
uri_str = str(uri)
with client_span(
f"resource {uri_str}",
f"resources/read {uri_str}",
"resources/read",
uri_str,
session_id=self.transport.get_session_id(),
resource_uri=uri_str,
):
logger.debug(f"[{self.name}] called read_resource: {uri}")

Expand Down Expand Up @@ -1101,7 +1102,7 @@ async def get_prompt_mcp(
McpError: If the request results in a TimeoutError | JSONRPCError
"""
with client_span(
f"prompt {name}",
f"prompts/get {name}",
"prompts/get",
name,
session_id=self.transport.get_session_id(),
Expand Down Expand Up @@ -1393,7 +1394,7 @@ async def call_tool_mcp(
McpError: If the tool call requests results in a TimeoutError | JSONRPCError
"""
with client_span(
f"tool {name}",
f"tools/call {name}",
"tools/call",
name,
session_id=self.transport.get_session_id(),
Expand Down
9 changes: 8 additions & 1 deletion src/fastmcp/client/telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ def client_span(
method: str,
component_key: str,
session_id: str | None = None,
resource_uri: str | None = None,
) -> Generator[Span, None, None]:
"""Create a CLIENT span with standard MCP attributes.

Expand All @@ -22,12 +23,18 @@ def client_span(
tracer = get_tracer()
with tracer.start_as_current_span(name, kind=SpanKind.CLIENT) as span:
attrs: dict[str, str] = {
# RPC semantic conventions
"rpc.system": "mcp",
"rpc.method": method,
# MCP semantic conventions
"mcp.method.name": method,
# FastMCP-specific attributes
"fastmcp.component.key": component_key,
}
if session_id:
attrs["fastmcp.session.id"] = session_id
attrs["mcp.session.id"] = session_id
if resource_uri:
attrs["mcp.resource.uri"] = resource_uri
span.set_attributes(attrs)
try:
yield span
Expand Down
9 changes: 6 additions & 3 deletions src/fastmcp/server/providers/proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ async def run(
"""Executes the tool by making a call through the client."""
backend_name = self._backend_name or self.name
with client_span(
f"proxy tool {backend_name}", "tools/call", backend_name
f"tools/call {backend_name}", "tools/call", backend_name
) as span:
span.set_attribute("fastmcp.provider.type", "ProxyProvider")
client = await self._get_client()
Expand Down Expand Up @@ -222,7 +222,10 @@ async def read(self) -> ResourceResult:

backend_uri = self._backend_uri or str(self.uri)
with client_span(
f"proxy resource {backend_uri}", "resources/read", backend_uri
f"resources/read {backend_uri}",
"resources/read",
backend_uri,
resource_uri=backend_uri,
) as span:
span.set_attribute("fastmcp.provider.type", "ProxyProvider")
client = await self._get_client()
Expand Down Expand Up @@ -434,7 +437,7 @@ async def render(self, arguments: dict[str, Any]) -> PromptResult: # type: igno
"""Render the prompt by making a call through the client."""
backend_name = self._backend_name or self.name
with client_span(
f"proxy prompt {backend_name}", "prompts/get", backend_name
f"prompts/get {backend_name}", "prompts/get", backend_name
) as span:
span.set_attribute("fastmcp.provider.type", "ProxyProvider")
client = await self._get_client()
Expand Down
11 changes: 8 additions & 3 deletions src/fastmcp/server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -1473,7 +1473,7 @@ async def call_tool(

# Core logic: find and execute tool (providers queried in parallel)
with server_span(
f"tool {name}", "tools/call", self.name, "tool", name
f"tools/call {name}", "tools/call", self.name, "tool", name
) as span:
tool = await self.get_tool(name)
span.set_attributes(tool.get_span_attributes())
Expand Down Expand Up @@ -1567,7 +1567,12 @@ async def read_resource(

# Core logic: find and read resource (providers queried in parallel)
with server_span(
f"resource {uri}", "resources/read", self.name, "resource", uri
f"resources/read {uri}",
"resources/read",
self.name,
"resource",
uri,
resource_uri=uri,
) as span:
# Try concrete resources first
try:
Expand Down Expand Up @@ -1681,7 +1686,7 @@ async def render_prompt(

# Core logic: find and render prompt (providers queried in parallel)
with server_span(
f"prompt {name}", "prompts/get", self.name, "prompt", name
f"prompts/get {name}", "prompts/get", self.name, "prompt", name
) as span:
prompt = await self.get_prompt(name)
span.set_attributes(prompt.get_span_attributes())
Expand Down
32 changes: 19 additions & 13 deletions src/fastmcp/server/telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def get_session_span_attributes() -> dict[str, str]:
try:
ctx = get_context()
if ctx.request_context is not None and ctx.session_id is not None:
attrs["fastmcp.session.id"] = ctx.session_id
attrs["mcp.session.id"] = ctx.session_id
except RuntimeError:
pass
return attrs
Expand All @@ -59,6 +59,7 @@ def server_span(
server_name: str,
component_type: str,
component_key: str,
resource_uri: str | None = None,
) -> Generator[Span, None, None]:
"""Create a SERVER span with standard MCP attributes and auth context.

Expand All @@ -70,18 +71,23 @@ def server_span(
context=_get_parent_trace_context(),
kind=SpanKind.SERVER,
) as span:
span.set_attributes(
{
"rpc.system": "mcp",
"rpc.service": server_name,
"rpc.method": method,
"fastmcp.server.name": server_name,
"fastmcp.component.type": component_type,
"fastmcp.component.key": component_key,
**get_auth_span_attributes(),
**get_session_span_attributes(),
}
)
attrs: dict[str, str] = {
# RPC semantic conventions
"rpc.system": "mcp",
"rpc.service": server_name,
"rpc.method": method,
# MCP semantic conventions
"mcp.method.name": method,
# FastMCP-specific attributes
"fastmcp.server.name": server_name,
"fastmcp.component.type": component_type,
"fastmcp.component.key": component_key,
**get_auth_span_attributes(),
**get_session_span_attributes(),
}
if resource_uri is not None:
attrs["mcp.resource.uri"] = resource_uri
span.set_attributes(attrs)
try:
yield span
except Exception as e:
Expand Down
Loading
Loading