OTEL: Fix attribute compliance with MCP semantic conventions (#3889)

* OTEL: Fix attribute compliance and improve telemetry helpers

Attribute compliance:
- Remove rpc.system/service/method (MCP is not traditional RPC)
- Add gen_ai.tool.name on tools/call spans
- Add gen_ai.prompt.name on prompts/get spans
- Fix session_id check (truthy -> is not None)

Telemetry helper improvements:
- Add is_recording() guards to skip work on non-recording spans
- Add error.type attribute with __qualname__ on error spans
- Use isinstance check for ToolError to set "tool_error" error type
- Include exception message in span status description
- Add tool_name/prompt_name params to server_span and client_span

Client call_tool enrichment:
- Reflect tool-level errors (result.isError) on client span status
  so callers see ERROR even though the MCP protocol call succeeded

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: remove resource URI from span names to avoid high-cardinality

Per MCP semantic conventions, resource URIs SHOULD NOT be included in
span names by default since they can be unbounded (especially with
templates like users://{id}/profile). The URI remains available via
the mcp.resource.uri attribute.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add missing gen_ai/mcp attributes to proxy and delegate spans

- Proxy tool spans: add gen_ai.tool.name
- Proxy prompt spans: add gen_ai.prompt.name
- All delegate spans: add mcp.method.name
- Docs: remove rpc.* references, update span names and attributes table

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Hoist ToolError imports to module level, add rpc.* migration note

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bill Easton 2026-04-14 15:28:47 -05:00 committed by GitHub
commit 7184a4ca21
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 216 additions and 96 deletions

View file

@ -33,10 +33,12 @@ class TestToolTracing:
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"
# gen_ai semantic conventions
assert span.attributes["gen_ai.tool.name"] == "greet"
# RPC attributes must NOT be present
assert "rpc.system" not in span.attributes
assert "rpc.service" not in span.attributes
assert "rpc.method" not in span.attributes
# FastMCP-specific attributes
assert span.attributes["fastmcp.server.name"] == "test-server"
assert span.attributes["fastmcp.component.type"] == "tool"
@ -60,6 +62,10 @@ class TestToolTracing:
span = spans[0]
assert span.name == "tools/call failing_tool"
assert span.status.status_code == StatusCode.ERROR
assert span.status.description is not None
assert "Something went wrong" in span.status.description
assert span.attributes is not None
assert span.attributes["error.type"] == "tool_error"
assert len(span.events) > 0 # Exception recorded
async def test_call_nonexistent_tool_sets_error(
@ -76,6 +82,9 @@ class TestToolTracing:
span = spans[0]
assert span.name == "tools/call nonexistent"
assert span.status.status_code == StatusCode.ERROR
assert span.attributes is not None
# NotFoundError is not a ToolError, so uses class name as fallback
assert span.attributes["error.type"] == "NotFoundError"
class TestResourceTracing:
@ -95,16 +104,16 @@ class TestResourceTracing:
assert len(spans) == 1
span = spans[0]
assert span.name == "resources/read config://app"
assert span.name == "resources/read"
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"
# RPC attributes must NOT be present
assert "rpc.system" not in span.attributes
assert "rpc.service" not in span.attributes
assert "rpc.method" not in span.attributes
# FastMCP-specific attributes
assert span.attributes["fastmcp.server.name"] == "test-server"
assert span.attributes["fastmcp.component.type"] == "resource"
@ -126,14 +135,15 @@ class TestResourceTracing:
assert len(spans) == 1
span = spans[0]
assert span.name == "resources/read users://123/profile"
assert span.name == "resources/read"
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"
# RPC attributes must NOT be present
assert "rpc.system" not in span.attributes
assert "rpc.method" not in span.attributes
# Template component type is set by get_span_attributes
assert span.attributes["fastmcp.component.type"] == "resource_template"
assert (
@ -153,7 +163,7 @@ class TestResourceTracing:
assert len(spans) == 1
span = spans[0]
assert span.name == "resources/read nonexistent://resource"
assert span.name == "resources/read"
assert span.status.status_code == StatusCode.ERROR
@ -179,10 +189,12 @@ class TestPromptTracing:
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"
# gen_ai semantic conventions
assert span.attributes["gen_ai.prompt.name"] == "greeting"
# RPC attributes must NOT be present
assert "rpc.system" not in span.attributes
assert "rpc.service" not in span.attributes
assert "rpc.method" not in span.attributes
# FastMCP-specific attributes
assert span.attributes["fastmcp.server.name"] == "test-server"
assert span.attributes["fastmcp.component.type"] == "prompt"