mcp.protocol.version activity tag not emitted on server spans in stateless HTTP mode
Description
McpSessionHandler.AddTags tags the request Activity with mcp.protocol.version from NegotiatedProtocolVersion:
if (NegotiatedProtocolVersion is not null)
{
tags.Add("mcp.protocol.version", NegotiatedProtocolVersion);
}
NegotiatedProtocolVersion is a session-level property, set once during the initialize handshake (or from McpServer.NegotiatedProtocolVersion after negotiation completes). In stateless HTTP mode (HttpServerTransportOptions.Stateless = true, the default) and especially under the 2026-07-28 protocol revision (no initialize handshake, no session), each HTTP request gets its own short-lived McpServer/session, and per-request protocol negotiation happens via the MCP-Protocol-Version header / _meta.io.modelcontextprotocol/protocolVersion field rather than a one-time handshake.
In practice this means NegotiatedProtocolVersion is null (or not populated in time) when AddTags runs for a given request, so the SDK's own mcp.protocol.version tag never materializes on the span — even though the request does carry a negotiated version via the header/_meta field.
Impact
Consumers building custom telemetry filters/enrichers around MCP spans (e.g. IEndpointFilter, ConfigureSessionOptions, or a tools/call request filter) can't rely on the SDK to tag mcp.protocol.version in stateless mode and end up re-deriving it themselves from the per-request context, duplicating what the SDK is supposed to already provide per the OTel GenAI MCP semantic conventions (introduced in #1139).
Verified in source
Checked src/ModelContextProtocol.Core/McpSessionHandler.cs:
- Line 409 —
HandleMessageAsync calls PopulateContextFromMeta(incomingRequest) for every incoming server request. This populates message.Context.ProtocolVersion from the per-request MCP-Protocol-Version header / _meta field, before any tagging happens.
- Line 435 —
AddTags runs right after, in the same method, so message.Context?.ProtocolVersion is already available at that point.
- Line 967 —
AddTags still only reads NegotiatedProtocolVersion, never touches message.Context?.ProtocolVersion — so in stateless mode, where NegotiatedProtocolVersion is null, the per-request value that's already sitting on the message gets ignored.
- Line 156 (a different method, ping-availability check) already uses the fallback pattern:
jsonRpcRequest?.Context?.ProtocolVersion ?? NegotiatedProtocolVersion — proving the pattern is already established elsewhere in the same file, just not applied to the tag.
Suggested fix
AddTags should fall back to the per-request Context.ProtocolVersion when NegotiatedProtocolVersion is null — the same pattern already used at line 156:
string? protocolVersion = (message as JsonRpcRequest)?.Context?.ProtocolVersion ?? NegotiatedProtocolVersion;
if (protocolVersion is not null)
{
tags.Add("mcp.protocol.version", protocolVersion);
}
No protocol/wire-format change — this is purely wiring the tag to a value the SDK already computes for every request.
Repro sketch
- Configure
HttpServerTransportOptions.Stateless = true (default).
- Send a
tools/call request with 2026-07-28 MCP-Protocol-Version header / per-request _meta.
- Attach a listener to the
Experimental.ModelContextProtocol ActivitySource and inspect the finished span's TagObjects.
mcp.protocol.version is absent (or reflects a stale/null session-level value).
SDK version
ModelContextProtocol.AspNetCore 2.0.0
mcp.protocol.version activity tag not emitted on server spans in stateless HTTP mode
Description
McpSessionHandler.AddTagstags the requestActivitywithmcp.protocol.versionfromNegotiatedProtocolVersion:NegotiatedProtocolVersionis a session-level property, set once during theinitializehandshake (or fromMcpServer.NegotiatedProtocolVersionafter negotiation completes). In stateless HTTP mode (HttpServerTransportOptions.Stateless = true, the default) and especially under the2026-07-28protocol revision (noinitializehandshake, no session), each HTTP request gets its own short-livedMcpServer/session, and per-request protocol negotiation happens via theMCP-Protocol-Versionheader /_meta.io.modelcontextprotocol/protocolVersionfield rather than a one-time handshake.In practice this means
NegotiatedProtocolVersionisnull(or not populated in time) whenAddTagsruns for a given request, so the SDK's ownmcp.protocol.versiontag never materializes on the span — even though the request does carry a negotiated version via the header/_metafield.Impact
Consumers building custom telemetry filters/enrichers around MCP spans (e.g.
IEndpointFilter,ConfigureSessionOptions, or atools/callrequest filter) can't rely on the SDK to tagmcp.protocol.versionin stateless mode and end up re-deriving it themselves from the per-request context, duplicating what the SDK is supposed to already provide per the OTel GenAI MCP semantic conventions (introduced in #1139).Verified in source
Checked
src/ModelContextProtocol.Core/McpSessionHandler.cs:HandleMessageAsynccallsPopulateContextFromMeta(incomingRequest)for every incoming server request. This populatesmessage.Context.ProtocolVersionfrom the per-requestMCP-Protocol-Versionheader /_metafield, before any tagging happens.AddTagsruns right after, in the same method, somessage.Context?.ProtocolVersionis already available at that point.AddTagsstill only readsNegotiatedProtocolVersion, never touchesmessage.Context?.ProtocolVersion— so in stateless mode, whereNegotiatedProtocolVersionis null, the per-request value that's already sitting on the message gets ignored.jsonRpcRequest?.Context?.ProtocolVersion ?? NegotiatedProtocolVersion— proving the pattern is already established elsewhere in the same file, just not applied to the tag.Suggested fix
AddTagsshould fall back to the per-requestContext.ProtocolVersionwhenNegotiatedProtocolVersionis null — the same pattern already used at line 156:No protocol/wire-format change — this is purely wiring the tag to a value the SDK already computes for every request.
Repro sketch
HttpServerTransportOptions.Stateless = true(default).tools/callrequest with2026-07-28MCP-Protocol-Versionheader / per-request_meta.Experimental.ModelContextProtocolActivitySourceand inspect the finished span'sTagObjects.mcp.protocol.versionis absent (or reflects a stale/null session-level value).SDK version
ModelContextProtocol.AspNetCore2.0.0