Describe the bug
When a client negotiates "protocolVersion": "2025-11-25" via the initialize handshake, the server (v2.0.0-preview.3) still includes resultType, ttlMs, and cacheScope in every JSON-RPC result object. These fields are exclusive to the 2026-07-28 draft spec and must be absent from responses on a 2025-11-25 session. Clients that strictly validate against the 2025-11-25 schema — such as MCP Inspector 1.0.0 — treat them as unrecognized keys and fail the handshake.
To Reproduce
- Start a C# MCP server built with v2.0.0-preview.3 (stateless or stateful).
- Send an
initialize request with "protocolVersion": "2025-11-25".
- Observe that the
InitializeResult contains "resultType": "complete" despite the server echoing back "protocolVersion": "2025-11-25".
- Send a
tools/list request.
- Observe that the result contains
"resultType": "complete", "ttlMs": 0, and "cacheScope": "private".
- Point MCP Inspector 1.0.0 at the server — it rejects the handshake on these unrecognized fields.
Expected behavior
For sessions where the negotiated protocol version is 2025-11-25, resultType, ttlMs, and cacheScope must be absent from all result objects. The serializer already uses WhenWritingNull — if these properties are left null they are correctly omitted. The server should only stamp them when the negotiated (or per-request) version is 2026-07-28 or later.
Logs
InitializeResult wire shape (negotiated version 2025-11-25):
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2025-11-25",
"resultType": "complete",
"serverInfo": { "..." : "..." },
"capabilities": {}
}
}
tools/list wire shape (same session):
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"tools": [],
"resultType": "complete",
"ttlMs": 0,
"cacheScope": "private"
}
}
Additional context
This appears to have been introduced by two PRs without protocol-version gating:
The fix should gate both stampings on IsJuly2026OrLaterProtocolRequest(jsonRpcRequest), which requires moving the logic from the inner McpRequestHandler closures into the outer _requestHandlers.Set(...) lambda where jsonRpcRequest is in scope. SetWithAlternateHandler and SetTaskAugmentedHandler have the same pattern and need the same fix. The hardcoded ResultType = "complete" on InitializeResult at line 613 also needs a version gate — negotiatedProtocolVersion is already in scope at that point.
Describe the bug
When a client negotiates
"protocolVersion": "2025-11-25"via theinitializehandshake, the server (v2.0.0-preview.3) still includesresultType,ttlMs, andcacheScopein every JSON-RPC result object. These fields are exclusive to the 2026-07-28 draft spec and must be absent from responses on a 2025-11-25 session. Clients that strictly validate against the 2025-11-25 schema — such as MCP Inspector 1.0.0 — treat them as unrecognized keys and fail the handshake.To Reproduce
initializerequest with"protocolVersion": "2025-11-25".InitializeResultcontains"resultType": "complete"despite the server echoing back"protocolVersion": "2025-11-25".tools/listrequest."resultType": "complete","ttlMs": 0, and"cacheScope": "private".Expected behavior
For sessions where the negotiated protocol version is
2025-11-25,resultType,ttlMs, andcacheScopemust be absent from all result objects. The serializer already usesWhenWritingNull— if these properties are leftnullthey are correctly omitted. The server should only stamp them when the negotiated (or per-request) version is 2026-07-28 or later.Logs
InitializeResultwire shape (negotiated version2025-11-25):{ "jsonrpc": "2.0", "id": 1, "result": { "protocolVersion": "2025-11-25", "resultType": "complete", "serverInfo": { "..." : "..." }, "capabilities": {} } }tools/listwire shape (same session):{ "jsonrpc": "2.0", "id": 2, "result": { "tools": [], "resultType": "complete", "ttlMs": 0, "cacheScope": "private" } }Additional context
This appears to have been introduced by two PRs without protocol-version gating:
resultType: "complete"— added by Fix missing resultType on complete result responses #1684 (fixing Draft protocol: complete result responses omit requiredresultType#1676). The fix wraps everySetHandlerresult callback and stampsResultType = "complete"unconditionally, and also hardcodes it directly onInitializeResultinConfigureInitialize. The closures receive onlyMcpRequest, notJsonRpcRequest, so the existingIsJuly2026OrLaterProtocolRequest(jsonRpcRequest)helper is never consulted. Relevant code:McpServerImpl.cslines 613 and 1665–1678.ttlMs/cacheScopedefaults — added by the SEP-2549 implementation (Add SEP-2549 caching hints (ttlMs and cacheScope) to cacheable results #1623, related to Warn (don't throw) on the client when a draft-protocol server omits required SEP-2549 ttlMs/cacheScope #1650). Same pattern: unconditional defaulting toTimeSpan.Zero/CacheScope.PrivateinSetHandlerfor anyICacheableResult, with no version check. Relevant code:McpServerImpl.cslines 1649–1663.The fix should gate both stampings on
IsJuly2026OrLaterProtocolRequest(jsonRpcRequest), which requires moving the logic from the innerMcpRequestHandlerclosures into the outer_requestHandlers.Set(...)lambda wherejsonRpcRequestis in scope.SetWithAlternateHandlerandSetTaskAugmentedHandlerhave the same pattern and need the same fix. The hardcodedResultType = "complete"onInitializeResultat line 613 also needs a version gate —negotiatedProtocolVersionis already in scope at that point.