Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 0 additions & 67 deletions src/ModelContextProtocol.Core/McpSessionHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -401,14 +401,6 @@ private static async Task<ClientCompletionDetails> GetCompletionDetailsAsync(Tas

private async Task HandleMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken)
{
// Project the 2026-07-28 protocol's per-request _meta fields onto the message context before any
// filters run so they (and downstream handlers) can read client info / capabilities /
// protocol version / log level without re-parsing.
if (_isServer && message is JsonRpcRequest incomingRequest)
{
PopulateContextFromMeta(incomingRequest);
}

Histogram<double> durationMetric = _isServer ? s_serverOperationDuration : s_clientOperationDuration;
string method = GetMethodName(message);

Expand Down Expand Up @@ -544,65 +536,6 @@ await SendMessageAsync(new JsonRpcResponse
return result;
}

/// <summary>
/// Reads the 2026-07-28 protocol's per-request <c>_meta</c> fields off the request and projects them onto
/// <see cref="JsonRpcMessage.Context"/> so they're available without re-parsing throughout the pipeline.
/// </summary>
/// <remarks>
/// Per SEP-2575 the keys are <c>io.modelcontextprotocol/protocolVersion</c>,
/// <c>/clientInfo</c>, <c>/clientCapabilities</c>, and (optional) <c>/logLevel</c>. Any field
/// that's already set on the context (e.g., <see cref="JsonRpcMessageContext.ProtocolVersion"/>
/// populated by the HTTP transport from the <c>MCP-Protocol-Version</c> header) is left alone
/// unless explicitly overwritten by a non-null value parsed here.
/// </remarks>
internal static void PopulateContextFromMeta(JsonRpcRequest request)
{
if (request.Params is not JsonObject paramsObj)
{
return;
}

if (paramsObj["_meta"] is not JsonObject metaObj)
{
return;
}

var context = request.Context ??= new JsonRpcMessageContext();

if (metaObj[MetaKeys.ProtocolVersion] is JsonValue protocolVersion &&
protocolVersion.TryGetValue(out string? protocolVersionValue))
{
// If a transport-level header (e.g., the Streamable HTTP MCP-Protocol-Version header) already
// populated this, validate the body _meta matches per SEP-2575. A disagreement is reported with
// -32020 HeaderMismatch (the same code used for the Mcp-Method/Mcp-Name header-vs-body checks),
// which conformant 2026-07-28 clients recognize as a SEP-2575 signal and surface as-is rather
// than mistaking it for an initialize-handshake server and falling back to initialize.
if (context.ProtocolVersion is { } existing && !string.Equals(existing, protocolVersionValue, StringComparison.Ordinal))
{
throw new McpProtocolException(
$"Header mismatch: the per-request _meta protocol version '{protocolVersionValue}' does not match the MCP-Protocol-Version header value '{existing}'.",
McpErrorCode.HeaderMismatch);
}

context.ProtocolVersion = protocolVersionValue;
}

if (metaObj[MetaKeys.ClientInfo] is JsonNode clientInfoNode)
{
context.ClientInfo = JsonSerializer.Deserialize(clientInfoNode, McpJsonUtilities.JsonContext.Default.Implementation);
}

if (metaObj[MetaKeys.ClientCapabilities] is JsonNode clientCapabilitiesNode)
{
context.ClientCapabilities = JsonSerializer.Deserialize(clientCapabilitiesNode, McpJsonUtilities.JsonContext.Default.ClientCapabilities);
}

if (metaObj[MetaKeys.LogLevel] is JsonNode logLevelNode)
{
context.LogLevel = JsonSerializer.Deserialize(logLevelNode, McpJsonUtilities.JsonContext.Default.LoggingLevel);
}
}

/// <summary>
/// Injects the 2026-07-28 protocol's per-request <c>_meta</c> fields into an outgoing request.
/// Protocol version and client info overwrite any existing values; client capabilities are merged
Expand Down
20 changes: 11 additions & 9 deletions src/ModelContextProtocol.Core/Protocol/JsonRpcMessageContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,12 @@ public sealed class JsonRpcMessageContext
public string? RoutingName { get; set; }

/// <summary>
/// Gets or sets the protocol version from the transport-level header (e.g. <c>Mcp-Protocol-Version</c>)
/// that accompanied this JSON-RPC message.
/// Gets or sets the authoritative protocol version for this JSON-RPC message.
/// </summary>
/// <remarks>
/// In stateless Streamable HTTP mode, the protocol version cannot be negotiated via the <c>initialize</c>
/// handshake because each request creates a new server instance. This property allows the transport layer
/// to flow the protocol version header so the server can determine client capabilities.
/// The transport may populate this from a header such as <c>Mcp-Protocol-Version</c>. For modern revisions,
/// the server validates and projects the matching per-request <c>_meta</c> value. A known legacy version in
/// <c>_meta</c> is advisory and does not establish or change the negotiated session version.
/// </remarks>
public string? ProtocolVersion { get; set; }

Expand All @@ -104,7 +103,8 @@ public sealed class JsonRpcMessageContext
/// </summary>
/// <remarks>
/// Introduced by the 2026-07-28 protocol revision (SEP-2575). When the request was made under the 2026-07-28 or later revision,
/// the server uses this in lieu of the value previously captured during the <c>initialize</c> handshake.
/// the server uses this in lieu of the value previously captured during the <c>initialize</c> handshake. A legacy request
/// may also carry this field for forward compatibility; stateful legacy sessions continue to use their initialized identity.
/// </remarks>
public Implementation? ClientInfo { get; set; }

Expand All @@ -114,7 +114,9 @@ public sealed class JsonRpcMessageContext
/// </summary>
/// <remarks>
/// Introduced by the 2026-07-28 protocol revision (SEP-2575). Per the spec, the server MUST NOT infer client
/// capabilities from previous requests; the authoritative value is the one declared on each request.
/// capabilities from previous modern requests; the authoritative value is the one declared on each request.
/// A legacy request may also carry this field for forward compatibility, but stateful legacy sessions continue
/// to use the capabilities negotiated during initialization.
/// </remarks>
public ClientCapabilities? ClientCapabilities { get; set; }

Expand All @@ -124,8 +126,8 @@ public sealed class JsonRpcMessageContext
/// </summary>
/// <remarks>
/// Introduced by the 2026-07-28 protocol revision (SEP-2575). Replaces the legacy
/// <see cref="RequestMethods.LoggingSetLevel"/> RPC. When absent, the server MUST NOT emit log notifications
/// for the request.
/// <see cref="RequestMethods.LoggingSetLevel"/> RPC. When absent from a modern request, the server MUST NOT emit
/// log notifications for the request. Legacy requests continue to use their negotiated logging behavior.
/// </remarks>
public LoggingLevel? LogLevel { get; set; }
}
Loading