mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-20 20:44:17 +02:00
Merge pull request #2886 from jlowin/mcp-semconv-2083
Adopt OpenTelemetry MCP semantic conventions
This commit is contained in:
commit
7b9a01c789
9 changed files with 151 additions and 84 deletions
|
|
@ -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]
|
||||
```
|
||||
|
||||
|
|
@ -191,22 +191,38 @@ 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 |
|
||||
|-----------|-------------|
|
||||
|
|
@ -214,7 +230,6 @@ All custom attributes use the `fastmcp.` prefix:
|
|||
| `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:
|
||||
|
||||
|
|
@ -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)
|
||||
```
|
||||
|
|
|
|||
|
|
@ -881,10 +881,11 @@ class Client(Generic[ClientTransportT]):
|
|||
"""
|
||||
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}")
|
||||
|
||||
|
|
@ -1101,7 +1102,7 @@ class Client(Generic[ClientTransportT]):
|
|||
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(),
|
||||
|
|
@ -1393,7 +1394,7 @@ class Client(Generic[ClientTransportT]):
|
|||
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(),
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ class ProxyTool(Tool):
|
|||
"""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()
|
||||
|
|
@ -222,7 +222,10 @@ class ProxyResource(Resource):
|
|||
|
||||
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()
|
||||
|
|
@ -434,7 +437,7 @@ class ProxyPrompt(Prompt):
|
|||
"""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()
|
||||
|
|
|
|||
|
|
@ -1473,7 +1473,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
|
||||
# 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())
|
||||
|
|
@ -1567,7 +1567,12 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
|
||||
# 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:
|
||||
|
|
@ -1681,7 +1686,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
|
||||
# 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())
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -30,8 +30,8 @@ class TestClientToolTracing:
|
|||
spans = trace_exporter.get_finished_spans()
|
||||
span_names = [s.name for s in spans]
|
||||
|
||||
# Client should create "tool greet" span
|
||||
assert "tool greet" in span_names
|
||||
# Client should create "tools/call greet" span
|
||||
assert "tools/call greet" in span_names
|
||||
|
||||
async def test_call_tool_span_attributes(
|
||||
self, trace_exporter: InMemorySpanExporter
|
||||
|
|
@ -53,15 +53,19 @@ class TestClientToolTracing:
|
|||
(
|
||||
s
|
||||
for s in spans
|
||||
if s.name == "tool add"
|
||||
if s.name == "tools/call add"
|
||||
and s.attributes is not None
|
||||
and "fastmcp.server.name" not in s.attributes
|
||||
),
|
||||
None,
|
||||
)
|
||||
assert client_span is not None
|
||||
# Standard MCP semantic conventions
|
||||
assert client_span.attributes["mcp.method.name"] == "tools/call"
|
||||
# Standard RPC semantic conventions
|
||||
assert client_span.attributes["rpc.system"] == "mcp"
|
||||
assert client_span.attributes["rpc.method"] == "tools/call"
|
||||
# FastMCP-specific attributes
|
||||
assert client_span.attributes["fastmcp.component.key"] == "add"
|
||||
|
||||
|
||||
|
|
@ -85,8 +89,8 @@ class TestClientResourceTracing:
|
|||
spans = trace_exporter.get_finished_spans()
|
||||
span_names = [s.name for s in spans]
|
||||
|
||||
# Client should create "resource data://config" span
|
||||
assert "resource data://config" in span_names
|
||||
# Client should create "resources/read data://config" span
|
||||
assert "resources/read data://config" in span_names
|
||||
|
||||
async def test_read_resource_span_attributes(
|
||||
self, trace_exporter: InMemorySpanExporter
|
||||
|
|
@ -108,15 +112,20 @@ class TestClientResourceTracing:
|
|||
(
|
||||
s
|
||||
for s in spans
|
||||
if s.name.startswith("resource data://")
|
||||
if s.name.startswith("resources/read data://")
|
||||
and s.attributes is not None
|
||||
and "fastmcp.server.name" not in s.attributes
|
||||
),
|
||||
None,
|
||||
)
|
||||
assert client_span is not None
|
||||
# Standard MCP semantic conventions
|
||||
assert client_span.attributes["mcp.method.name"] == "resources/read"
|
||||
assert "data://" in str(client_span.attributes["mcp.resource.uri"])
|
||||
# Standard RPC semantic conventions
|
||||
assert client_span.attributes["rpc.system"] == "mcp"
|
||||
assert client_span.attributes["rpc.method"] == "resources/read"
|
||||
# FastMCP-specific attributes
|
||||
# The URI may be normalized with trailing slash
|
||||
assert "data://" in str(client_span.attributes["fastmcp.component.key"])
|
||||
|
||||
|
|
@ -139,8 +148,8 @@ class TestClientPromptTracing:
|
|||
spans = trace_exporter.get_finished_spans()
|
||||
span_names = [s.name for s in spans]
|
||||
|
||||
# Client should create "prompt greeting" span
|
||||
assert "prompt greeting" in span_names
|
||||
# Client should create "prompts/get greeting" span
|
||||
assert "prompts/get greeting" in span_names
|
||||
|
||||
async def test_get_prompt_span_attributes(
|
||||
self, trace_exporter: InMemorySpanExporter
|
||||
|
|
@ -162,15 +171,19 @@ class TestClientPromptTracing:
|
|||
(
|
||||
s
|
||||
for s in spans
|
||||
if s.name == "prompt welcome"
|
||||
if s.name == "prompts/get welcome"
|
||||
and s.attributes is not None
|
||||
and "fastmcp.server.name" not in s.attributes
|
||||
),
|
||||
None,
|
||||
)
|
||||
assert client_span is not None
|
||||
# Standard MCP semantic conventions
|
||||
assert client_span.attributes["mcp.method.name"] == "prompts/get"
|
||||
# Standard RPC semantic conventions
|
||||
assert client_span.attributes["rpc.system"] == "mcp"
|
||||
assert client_span.attributes["rpc.method"] == "prompts/get"
|
||||
# FastMCP-specific attributes
|
||||
assert client_span.attributes["fastmcp.component.key"] == "welcome"
|
||||
|
||||
|
||||
|
|
@ -198,7 +211,7 @@ class TestClientServerSpanHierarchy:
|
|||
(
|
||||
s
|
||||
for s in spans
|
||||
if s.name == "tool echo"
|
||||
if s.name == "tools/call echo"
|
||||
and s.attributes is not None
|
||||
and "fastmcp.server.name" not in s.attributes
|
||||
),
|
||||
|
|
@ -208,7 +221,7 @@ class TestClientServerSpanHierarchy:
|
|||
(
|
||||
s
|
||||
for s in spans
|
||||
if s.name == "tool echo"
|
||||
if s.name == "tools/call echo"
|
||||
and s.attributes is not None
|
||||
and "fastmcp.server.name" in s.attributes
|
||||
),
|
||||
|
|
@ -248,7 +261,7 @@ class TestClientServerSpanHierarchy:
|
|||
(
|
||||
s
|
||||
for s in spans
|
||||
if s.name == "tool add"
|
||||
if s.name == "tools/call add"
|
||||
and s.attributes is not None
|
||||
and "fastmcp.server.name" not in s.attributes
|
||||
),
|
||||
|
|
@ -258,7 +271,7 @@ class TestClientServerSpanHierarchy:
|
|||
(
|
||||
s
|
||||
for s in spans
|
||||
if s.name == "tool add"
|
||||
if s.name == "tools/call add"
|
||||
and s.attributes is not None
|
||||
and "fastmcp.server.name" in s.attributes
|
||||
),
|
||||
|
|
@ -315,7 +328,7 @@ class TestClientErrorTracing:
|
|||
(
|
||||
s
|
||||
for s in spans
|
||||
if s.name == "tool failing_tool"
|
||||
if s.name == "tools/call failing_tool"
|
||||
and s.attributes is not None
|
||||
and "fastmcp.server.name" not in s.attributes
|
||||
),
|
||||
|
|
@ -326,7 +339,7 @@ class TestClientErrorTracing:
|
|||
(
|
||||
s
|
||||
for s in spans
|
||||
if s.name == "tool failing_tool"
|
||||
if s.name == "tools/call failing_tool"
|
||||
and s.attributes is not None
|
||||
and "fastmcp.server.name" in s.attributes
|
||||
),
|
||||
|
|
@ -362,7 +375,7 @@ class TestClientErrorTracing:
|
|||
(
|
||||
s
|
||||
for s in spans
|
||||
if s.name.startswith("resource data://fail")
|
||||
if s.name.startswith("resources/read data://fail")
|
||||
and s.attributes is not None
|
||||
and "fastmcp.server.name" not in s.attributes
|
||||
),
|
||||
|
|
@ -373,7 +386,7 @@ class TestClientErrorTracing:
|
|||
(
|
||||
s
|
||||
for s in spans
|
||||
if s.name.startswith("resource data://fail")
|
||||
if s.name.startswith("resources/read data://fail")
|
||||
and s.attributes is not None
|
||||
and "fastmcp.server.name" in s.attributes
|
||||
),
|
||||
|
|
@ -409,7 +422,7 @@ class TestClientErrorTracing:
|
|||
(
|
||||
s
|
||||
for s in spans
|
||||
if s.name == "prompt failing_prompt"
|
||||
if s.name == "prompts/get failing_prompt"
|
||||
and s.attributes is not None
|
||||
and "fastmcp.server.name" not in s.attributes
|
||||
),
|
||||
|
|
@ -420,7 +433,7 @@ class TestClientErrorTracing:
|
|||
(
|
||||
s
|
||||
for s in spans
|
||||
if s.name == "prompt failing_prompt"
|
||||
if s.name == "prompts/get failing_prompt"
|
||||
and s.attributes is not None
|
||||
and "fastmcp.server.name" in s.attributes
|
||||
),
|
||||
|
|
@ -452,7 +465,7 @@ class TestClientErrorTracing:
|
|||
(
|
||||
s
|
||||
for s in spans
|
||||
if s.name == "tool nonexistent"
|
||||
if s.name == "tools/call nonexistent"
|
||||
and s.attributes is not None
|
||||
and "fastmcp.server.name" not in s.attributes
|
||||
),
|
||||
|
|
@ -463,7 +476,7 @@ class TestClientErrorTracing:
|
|||
(
|
||||
s
|
||||
for s in spans
|
||||
if s.name == "tool nonexistent"
|
||||
if s.name == "tools/call nonexistent"
|
||||
and s.attributes is not None
|
||||
and "fastmcp.server.name" in s.attributes
|
||||
),
|
||||
|
|
@ -518,7 +531,7 @@ class TestSessionIdOnSpans:
|
|||
(
|
||||
s
|
||||
for s in spans
|
||||
if s.name == "tool echo"
|
||||
if s.name == "tools/call echo"
|
||||
and s.attributes is not None
|
||||
and "fastmcp.server.name" not in s.attributes
|
||||
),
|
||||
|
|
@ -526,8 +539,8 @@ class TestSessionIdOnSpans:
|
|||
)
|
||||
|
||||
assert client_span is not None, "Client should create a span"
|
||||
assert "fastmcp.session.id" in client_span.attributes
|
||||
assert client_span.attributes["fastmcp.session.id"] is not None
|
||||
assert "mcp.session.id" in client_span.attributes
|
||||
assert client_span.attributes["mcp.session.id"] is not None
|
||||
|
||||
async def test_server_span_includes_session_id(
|
||||
self,
|
||||
|
|
@ -549,7 +562,7 @@ class TestSessionIdOnSpans:
|
|||
(
|
||||
s
|
||||
for s in spans
|
||||
if s.name == "tool echo"
|
||||
if s.name == "tools/call echo"
|
||||
and s.attributes is not None
|
||||
and "fastmcp.server.name" in s.attributes
|
||||
),
|
||||
|
|
@ -557,8 +570,8 @@ class TestSessionIdOnSpans:
|
|||
)
|
||||
|
||||
assert server_span is not None, "Server should create a span"
|
||||
assert "fastmcp.session.id" in server_span.attributes
|
||||
assert server_span.attributes["fastmcp.session.id"] is not None
|
||||
assert "mcp.session.id" in server_span.attributes
|
||||
assert server_span.attributes["mcp.session.id"] is not None
|
||||
|
||||
async def test_client_and_server_share_same_session_id(
|
||||
self,
|
||||
|
|
@ -580,7 +593,7 @@ class TestSessionIdOnSpans:
|
|||
(
|
||||
s
|
||||
for s in spans
|
||||
if s.name == "tool echo"
|
||||
if s.name == "tools/call echo"
|
||||
and s.attributes is not None
|
||||
and "fastmcp.server.name" not in s.attributes
|
||||
),
|
||||
|
|
@ -590,7 +603,7 @@ class TestSessionIdOnSpans:
|
|||
(
|
||||
s
|
||||
for s in spans
|
||||
if s.name == "tool echo"
|
||||
if s.name == "tools/call echo"
|
||||
and s.attributes is not None
|
||||
and "fastmcp.server.name" in s.attributes
|
||||
),
|
||||
|
|
@ -601,8 +614,8 @@ class TestSessionIdOnSpans:
|
|||
assert server_span is not None
|
||||
|
||||
# Both should have session IDs and they should match
|
||||
client_session = client_span.attributes.get("fastmcp.session.id")
|
||||
server_session = server_span.attributes.get("fastmcp.session.id")
|
||||
client_session = client_span.attributes.get("mcp.session.id")
|
||||
server_session = server_span.attributes.get("mcp.session.id")
|
||||
|
||||
assert client_session is not None, "Client span should have session ID"
|
||||
assert server_session is not None, "Server span should have session ID"
|
||||
|
|
|
|||
|
|
@ -32,12 +32,12 @@ class TestFastMCPProviderTracing:
|
|||
spans = trace_exporter.get_finished_spans()
|
||||
span_names = [s.name for s in spans]
|
||||
|
||||
# Parent server creates "tool child_child_tool"
|
||||
assert "tool child_child_tool" in span_names
|
||||
# Parent server creates "tools/call child_child_tool"
|
||||
assert "tools/call child_child_tool" in span_names
|
||||
# FastMCPProvider creates "delegate child_tool"
|
||||
assert "delegate child_tool" in span_names
|
||||
# Child server creates "tool child_tool"
|
||||
assert "tool child_tool" in span_names
|
||||
# Child server creates "tools/call child_tool"
|
||||
assert "tools/call child_tool" in span_names
|
||||
|
||||
# Verify delegate span has correct attributes
|
||||
delegate_span = next(s for s in spans if s.name == "delegate child_tool")
|
||||
|
|
@ -120,9 +120,9 @@ class TestProviderSpanHierarchy:
|
|||
spans = trace_exporter.get_finished_spans()
|
||||
|
||||
# Find the spans
|
||||
parent_span = next(s for s in spans if s.name == "tool ns_greet")
|
||||
parent_span = next(s for s in spans if s.name == "tools/call ns_greet")
|
||||
delegate_span = next(s for s in spans if s.name == "delegate greet")
|
||||
child_span = next(s for s in spans if s.name == "tool greet")
|
||||
child_span = next(s for s in spans if s.name == "tools/call greet")
|
||||
|
||||
# Verify parent-child relationships
|
||||
assert delegate_span.parent.span_id == parent_span.context.span_id
|
||||
|
|
|
|||
|
|
@ -28,12 +28,16 @@ class TestToolTracing:
|
|||
assert len(spans) == 1
|
||||
|
||||
span = spans[0]
|
||||
assert span.name == "tool greet"
|
||||
assert span.name == "tools/call greet"
|
||||
assert span.kind == SpanKind.SERVER
|
||||
assert span.attributes is not None
|
||||
# Standard MCP semantic conventions
|
||||
assert span.attributes["mcp.method.name"] == "tools/call"
|
||||
# Standard RPC semantic conventions
|
||||
assert span.attributes["rpc.system"] == "mcp"
|
||||
assert span.attributes["rpc.service"] == "test-server"
|
||||
assert span.attributes["rpc.method"] == "tools/call"
|
||||
# FastMCP-specific attributes
|
||||
assert span.attributes["fastmcp.server.name"] == "test-server"
|
||||
assert span.attributes["fastmcp.component.type"] == "tool"
|
||||
assert span.attributes["fastmcp.component.key"] == "tool:greet"
|
||||
|
|
@ -54,7 +58,7 @@ class TestToolTracing:
|
|||
assert len(spans) == 1
|
||||
|
||||
span = spans[0]
|
||||
assert span.name == "tool failing_tool"
|
||||
assert span.name == "tools/call failing_tool"
|
||||
assert span.status.status_code == StatusCode.ERROR
|
||||
assert len(span.events) > 0 # Exception recorded
|
||||
|
||||
|
|
@ -70,7 +74,7 @@ class TestToolTracing:
|
|||
assert len(spans) == 1
|
||||
|
||||
span = spans[0]
|
||||
assert span.name == "tool nonexistent"
|
||||
assert span.name == "tools/call nonexistent"
|
||||
assert span.status.status_code == StatusCode.ERROR
|
||||
|
||||
|
||||
|
|
@ -91,12 +95,17 @@ class TestResourceTracing:
|
|||
assert len(spans) == 1
|
||||
|
||||
span = spans[0]
|
||||
assert span.name == "resource config://app"
|
||||
assert span.name == "resources/read config://app"
|
||||
assert span.kind == SpanKind.SERVER
|
||||
assert span.attributes is not None
|
||||
# Standard MCP semantic conventions
|
||||
assert span.attributes["mcp.method.name"] == "resources/read"
|
||||
assert span.attributes["mcp.resource.uri"] == "config://app"
|
||||
# Standard RPC semantic conventions
|
||||
assert span.attributes["rpc.system"] == "mcp"
|
||||
assert span.attributes["rpc.service"] == "test-server"
|
||||
assert span.attributes["rpc.method"] == "resources/read"
|
||||
# FastMCP-specific attributes
|
||||
assert span.attributes["fastmcp.server.name"] == "test-server"
|
||||
assert span.attributes["fastmcp.component.type"] == "resource"
|
||||
assert span.attributes["fastmcp.component.key"] == "resource:config://app"
|
||||
|
|
@ -117,9 +126,13 @@ class TestResourceTracing:
|
|||
assert len(spans) == 1
|
||||
|
||||
span = spans[0]
|
||||
assert span.name == "resource users://123/profile"
|
||||
assert span.name == "resources/read users://123/profile"
|
||||
assert span.kind == SpanKind.SERVER
|
||||
assert span.attributes is not None
|
||||
# Standard MCP semantic conventions
|
||||
assert span.attributes["mcp.method.name"] == "resources/read"
|
||||
assert span.attributes["mcp.resource.uri"] == "users://123/profile"
|
||||
# Standard RPC semantic conventions
|
||||
assert span.attributes["rpc.method"] == "resources/read"
|
||||
# Template component type is set by get_span_attributes
|
||||
assert span.attributes["fastmcp.component.type"] == "resource_template"
|
||||
|
|
@ -140,7 +153,7 @@ class TestResourceTracing:
|
|||
assert len(spans) == 1
|
||||
|
||||
span = spans[0]
|
||||
assert span.name == "resource nonexistent://resource"
|
||||
assert span.name == "resources/read nonexistent://resource"
|
||||
assert span.status.status_code == StatusCode.ERROR
|
||||
|
||||
|
||||
|
|
@ -161,12 +174,16 @@ class TestPromptTracing:
|
|||
assert len(spans) == 1
|
||||
|
||||
span = spans[0]
|
||||
assert span.name == "prompt greeting"
|
||||
assert span.name == "prompts/get greeting"
|
||||
assert span.kind == SpanKind.SERVER
|
||||
assert span.attributes is not None
|
||||
# Standard MCP semantic conventions
|
||||
assert span.attributes["mcp.method.name"] == "prompts/get"
|
||||
# Standard RPC semantic conventions
|
||||
assert span.attributes["rpc.system"] == "mcp"
|
||||
assert span.attributes["rpc.service"] == "test-server"
|
||||
assert span.attributes["rpc.method"] == "prompts/get"
|
||||
# FastMCP-specific attributes
|
||||
assert span.attributes["fastmcp.server.name"] == "test-server"
|
||||
assert span.attributes["fastmcp.component.type"] == "prompt"
|
||||
assert span.attributes["fastmcp.component.key"] == "prompt:greeting"
|
||||
|
|
@ -183,7 +200,7 @@ class TestPromptTracing:
|
|||
assert len(spans) == 1
|
||||
|
||||
span = spans[0]
|
||||
assert span.name == "prompt nonexistent"
|
||||
assert span.name == "prompts/get nonexistent"
|
||||
assert span.status.status_code == StatusCode.ERROR
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue