OpenTelemetry semantic conventions
mcpgw v1.x emits OpenTelemetry SemConv schema URL https://opentelemetry.io/schemas/1.26.0 and the experimental mcp.* namespace defined here. Mappings to OTel GenAI conventions track upstream stabilization; expect breaking attribute name changes only across major versions.
This page is the reference. For setup, see How-to: enable Datadog tracing. For the wire transport, see Configuration: telemetry.
Span names
Spans are named mcp.<method> with slashes replaced by dots.
| JSON-RPC method | Span name |
|---|---|
initialize | mcp.initialize |
tools/list | mcp.tools.list |
tools/call | mcp.tools.call |
resources/list | mcp.resources.list |
resources/read | mcp.resources.read |
prompts/list | mcp.prompts.list |
ping | mcp.ping |
The pattern is mechanical: replace / with . and prepend mcp..
Span kind
| Direction | Kind |
|---|---|
| Inbound request from MCP client | SERVER |
| Outbound forward to upstream MCP server | CLIENT |
Each request produces one SERVER span (the gateway-side) and, if the request reached the upstream, one CLIENT child span.
Attributes
Set on every span unless noted otherwise.
| Attribute | Type | Example | Notes |
|---|---|---|---|
mcp.method | string | tools/call | Original JSON-RPC method, with the slash. |
mcp.tool.name | string | fs_read | Empty string for non-tools/call methods. |
mcp.session.id | string | 01HXYZ... | From the Mcp-Session-Id header; empty if absent. |
mcp.transport | string | http+sse | See enum below. |
mcp.upstream | string | filesystem | Logical name from config. Never the URL. |
mcp.policy.decision | string | allow | See enum below. |
mcp.policy.rule_id | string | deny-shell | Set only when a rule fired. |
mcp.auth.key_id | string | claude-desktop-prod | Set when auth.enabled and authentication succeeds via the API-key path. |
mcp.auth.result | string | ok | Set when auth.enabled. See mcp.auth.result enum below. |
mcp.auth.oauth_client_id | string | my-agent | OAuth client_id (or azp fallback) of the verified Bearer token. Set on OAuth path only. Constant: telemetry.AttrOAuthClientID. |
mcp.auth.scopes | string | mcp:read mcp:write | Space-separated scopes from the verified token. Set on OAuth path only. See audit-log.md for the same field on the audit envelope. Constant: telemetry.AttrAuthScopes. |
mcp.auth.oauth_client_name | string | Claude Desktop | Human-readable client name resolved from CIMD document. Set on OAuth path when CIMD is enabled and the fetch succeeded. Constant: telemetry.AttrOAuthClientName. |
mcp.payload.bytes_in | int | 312 | Length of the incoming JSON-RPC request body. |
mcp.payload.bytes_out | int | 1024 | Length of the response body. Success spans only. |
mcp.error.code | int | -32001 | Error spans only. JSON-RPC error code. |
mcp.error.kind | string | policy_deny | Error spans only. See enum below. |
mcp.license.grace | bool | true | Set only when the gateway is operating in the license grace window. |
mcp.request.id | string | req_01HXYZ... | Stable correlation id; matches the X-MCPGW-Request-ID response header and the audit log request_id. |
mcp.synthesized | string | tools_list, tools_call_search | Set on synthesized responses to indicate which path produced them. tools_list for intercepted tools/list responses; tools_call_search for intercepted tools/call mcp_search responses. Only present when tool_search.mode: synthesize is active. Constant: telemetry.AttrSynthesized. |
mcp.toolsearch.hits | int | 5 | Number of tools returned by a mcp_search call. Set on tools_call_search synthesized spans. Constant: telemetry.AttrToolSearchHits. |
mcp.frames.total | int | 5 | Total SSE frames seen by the inspector (envelopes + opaque). Use mcp.frames.denied and mcp.frames.redacted to compute the policy-evaluated subset. Set on the proxy span for SSE responses. Constant: telemetry.AttrFramesTotal. |
mcp.frames.denied | int | 1 | Number of SSE frames dropped by deny or rate_limit rules. Subset of mcp.frames.total. Constant: telemetry.AttrFramesDenied. |
mcp.frames.redacted | int | 2 | Number of SSE frames whose data: bytes were rewritten by redact rules. Subset of mcp.frames.total. Constant: telemetry.AttrFramesRedacted. |
mcp.direction | string | server_to_client | Direction of the policy evaluation path. Set to "server_to_client" on the proxy span when the upstream response was an SSE stream. Also present on per-frame SSE audit lines. See enum below. Constant: telemetry.AttrDirection. |
mcp.app.served | int | 3 | Number of UI blocks forwarded unmodified to the client. Set on tools/call spans when the upstream response contained at least one UI block and no strip_app rule removed it. 0 or absent means no UI blocks were present or all were stripped. Constant: telemetry.AttrAppServed. |
mcp.app.stripped | int | 2 | Number of UI blocks removed by a strip_app action. Set on tools/call spans when a strip_app rule fired. 0 means the rule matched but found no UI blocks to remove. Absent when no strip_app rule fired. Constant: telemetry.AttrAppStripped. |
Enum values
mcp.transport
| Value | Description |
|---|---|
http+sse | HTTP request with SSE streaming response |
sse | Server-Sent Events transport |
stdio-bridge | Request arrived via the mcpgw stdio bridge |
http | Plain HTTP request, no SSE |
mcp.policy.decision
| Value | Condition |
|---|---|
allow | No rule fired, or a matching rule had action: allow |
deny | A rule with action: deny matched |
redact | A rule with action: redact matched and the payload was modified |
rate_limit_blocked | A rate-limit rule matched and the bucket was empty |
strip_app | A strip_app rule matched; UI blocks (if any) were removed from the response |
mcp.error.kind
| Value | Condition |
|---|---|
parse | JSON-RPC request could not be parsed |
policy_deny | A policy rule with action: deny matched |
rate_limited | A rate_limit rule blocked the call |
body_too_large | Pre-parse rejection at 16 MiB cap |
license_invalid | License missing, expired beyond grace, or signature invalid |
upstream_timeout | Upstream did not respond within the deadline |
upstream_unreachable | TCP connection to upstream failed |
upstream_protocol | Upstream returned non-MCP content |
internal | Unhandled gateway error |
mcp.direction
| Value | Description |
|---|---|
client_to_server | Policy evaluated on an inbound request from a client. |
server_to_client | Policy evaluated on an SSE frame from an upstream server. |
mcp.auth.result
| Value | Condition |
|---|---|
ok | Request authenticated successfully (API-key or OAuth path) |
missing | No configured auth header was present |
invalid | Header was present but did not match a configured key (API-key path), or JWT signature/format was rejected |
expired | Header matched a configured key whose expires_at is in the past (API-key path) |
bad_audience | JWT aud claim did not match auth.oauth.audience |
bad_issuer | JWT iss claim did not match auth.oauth.issuer |
insufficient_scope | JWT was valid but lacked a scope listed in auth.oauth.required_scopes |
Resource attributes
Set on the OTel Resource (every span carries them).
| Attribute | Source | Example |
|---|---|---|
service.name | telemetry.customer.service_name | mcpgw |
service.version | gateway version | 1.0.0 |
telemetry.sdk.name | OTel | opentelemetry |
telemetry.sdk.language | OTel | go |
| User-defined | telemetry.customer.resource_attrs | env=production, team=platform |
Recommended dashboards
See How-to: enable Datadog tracing for ready-to-build dashboard templates.
Notes on stability
mcp.upstreamis always the logical name fromupstreams[].nameinmcpgw.yaml, not the URL. This keeps spans clean and avoids leaking internal addresses into telemetry.mcp.tool.nameis set on every span regardless of method; it is an empty string for methods other thantools/call. This keeps Datadog facet cardinality predictable.mcp.policy.rule_idis only present when a rule fires. Absence means no rule matched (decision isallow).- The
mcp.*namespace is experimental and may evolve. Breaking changes are reserved for major versions only.