diff --git a/docs/servers/telemetry.mdx b/docs/servers/telemetry.mdx index 73186bfae0..b1c2a964e3 100644 --- a/docs/servers/telemetry.mdx +++ b/docs/servers/telemetry.mdx @@ -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) ``` diff --git a/src/fastmcp/client/client.py b/src/fastmcp/client/client.py index 9b947eca41..35ecde7bc0 100644 --- a/src/fastmcp/client/client.py +++ b/src/fastmcp/client/client.py @@ -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}") @@ -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(), @@ -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(), diff --git a/src/fastmcp/client/telemetry.py b/src/fastmcp/client/telemetry.py index 40fa48f73d..10d6d825fc 100644 --- a/src/fastmcp/client/telemetry.py +++ b/src/fastmcp/client/telemetry.py @@ -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 diff --git a/src/fastmcp/server/providers/proxy.py b/src/fastmcp/server/providers/proxy.py index d8d817bf28..699b18308a 100644 --- a/src/fastmcp/server/providers/proxy.py +++ b/src/fastmcp/server/providers/proxy.py @@ -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() @@ -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() @@ -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() diff --git a/src/fastmcp/server/server.py b/src/fastmcp/server/server.py index 308f44beb6..22333aeabe 100644 --- a/src/fastmcp/server/server.py +++ b/src/fastmcp/server/server.py @@ -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()) @@ -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: @@ -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()) diff --git a/src/fastmcp/server/telemetry.py b/src/fastmcp/server/telemetry.py index 6bfa8c0769..6c263225d0 100644 --- a/src/fastmcp/server/telemetry.py +++ b/src/fastmcp/server/telemetry.py @@ -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: diff --git a/tests/client/telemetry/test_client_tracing.py b/tests/client/telemetry/test_client_tracing.py index 94633fe3ef..02a90c4500 100644 --- a/tests/client/telemetry/test_client_tracing.py +++ b/tests/client/telemetry/test_client_tracing.py @@ -30,8 +30,8 @@ def greet(name: str) -> str: 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 @@ def add(a: int, b: int) -> int: ( 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 @@ def get_config() -> str: 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 @@ def get_config() -> str: ( 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 @@ def greeting() -> str: 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 @@ def welcome(name: str) -> str: ( 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 @@ def echo(message: str) -> str: ( 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 @@ def echo(message: str) -> str: ( 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 @@ def add(a: int, b: int) -> int: ( 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 @@ def add(a: int, b: int) -> int: ( 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 @@ def failing_tool() -> str: ( 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 @@ def failing_tool() -> str: ( 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 @@ def failing_resource() -> str: ( 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 @@ def failing_resource() -> str: ( 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 @@ def failing_prompt() -> str: ( 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 @@ def failing_prompt() -> str: ( 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 @@ async def test_call_nonexistent_tool_creates_spans( ( 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 @@ async def test_call_nonexistent_tool_creates_spans( ( 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 @@ async def test_client_span_includes_session_id( ( 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 @@ async def test_client_span_includes_session_id( ) 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 @@ async def test_server_span_includes_session_id( ( 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 @@ async def test_server_span_includes_session_id( ) 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 @@ async def test_client_and_server_share_same_session_id( ( 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 @@ async def test_client_and_server_share_same_session_id( ( 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 @@ async def test_client_and_server_share_same_session_id( 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" diff --git a/tests/server/telemetry/test_provider_tracing.py b/tests/server/telemetry/test_provider_tracing.py index c0bf71e9b7..bf76a3496e 100644 --- a/tests/server/telemetry/test_provider_tracing.py +++ b/tests/server/telemetry/test_provider_tracing.py @@ -32,12 +32,12 @@ def child_tool() -> str: 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 @@ def greet() -> str: 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 diff --git a/tests/server/telemetry/test_server_tracing.py b/tests/server/telemetry/test_server_tracing.py index bfb2f387fc..ae5e57c07f 100644 --- a/tests/server/telemetry/test_server_tracing.py +++ b/tests/server/telemetry/test_server_tracing.py @@ -28,12 +28,16 @@ def greet(name: str) -> str: 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 @@ def failing_tool() -> str: 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 @@ async def test_call_nonexistent_tool_sets_error( 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 @@ def get_config() -> str: 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 @@ def get_user_profile(user_id: str) -> str: 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 @@ async def test_read_nonexistent_resource_sets_error( 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 @@ def greeting(name: str) -> str: 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 @@ async def test_render_nonexistent_prompt_sets_error( 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