Telemetry reference (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.protocol.version |
string | 2026-07-28 |
Resolved protocol revision (_meta wins over the MCP-Protocol-Version header). Set only when the client declared a version. Constant: telemetry.AttrProtocolVersion. |
mcp.task.handle |
string | 786512e2-... |
Tasks-extension taskId (SEP-2663) — set when a response mints a task or a tasks/* request polls one. Correlates a logical task across RPCs. Constant: telemetry.AttrTaskHandle. |
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 | upstream_unreachable |
Error spans only — upstream/SSE failures. See enum below. |
mcp.license.grace |
bool | true |
Set only when the gateway is operating in the license grace window. |
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. |
mcp.guardrail.name |
string | pii-scanner |
Guardrail endpoint behind a matched guardrail rule. Set when a webhook callout was attempted, in either direction. On a dual-direction request the response-direction value overwrites the request-direction one (last write wins). Constant: telemetry.AttrGuardrailName. |
mcp.guardrail.verdict |
string | pass |
Webhook verdict. See enum below. Absent on a fail-closed callout failure, where mcp.error.kind = guardrail_unreachable carries the signal. Constant: telemetry.AttrGuardrailVerdict. |
mcp.guardrail.latency_ms |
int | 4 |
Wall time of the webhook Check call alone (never conflated with upstream latency). Constant: telemetry.AttrGuardrailLatency. |
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 |
guardrail |
A guardrail rule matched; the webhook verdict is in mcp.guardrail.verdict |
mcp.error.kind
The span attribute is set only on upstream, streaming, and guardrail-callout
failures. (Pre-parse, auth, policy, and rate-limit outcomes are recorded in
the audit error_kind field and the mcp.policy.decision attribute, not
here.)
| Value | Condition |
|---|---|
upstream_invalid |
Gateway could not build the upstream request |
upstream_unreachable |
Connect to upstream failed / no response |
upstream_read |
Error reading the upstream response |
upstream_too_large |
Upstream response exceeded the 16 MiB cap |
sse_truncated |
SSE response cut mid-stream (e.g. a frame exceeded max_sse_frame_bytes) |
guardrail_unreachable |
Guardrail webhook callout failed (non-200, timeout, malformed verdict) with fail_open: false; the request was denied |
mcp.guardrail.verdict
| Value | Condition |
|---|---|
pass |
The webhook returned pass; the envelope continues unchanged |
mask |
The webhook returned mask + body; the mutated body is forwarded (audit action redact) |
reject |
The webhook returned reject; the request is denied (-32001 policy_denied) |
error_failopen |
The callout failed and the endpoint has fail_open: true; the envelope continues |
The webhook’s reason string is never recorded on spans (operator-facing only, via gateway debug logs).
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 |
Enabled customer exporter, otherwise enabled operator exporter; blank falls back to mcpgw |
mcpgw |
service.version |
gateway version | 1.0.0 |
telemetry.sdk.name |
OTel | opentelemetry |
telemetry.sdk.language |
OTel | go |
| User-defined | Primary enabled exporter’s resource_attrs (customer wins when both are enabled) |
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.