Default draft protocol support: sessionless + handshake-less (SEP-2575 + SEP-2567) (#1610)

This commit is contained in:
Stephen Halter 2026-06-23 21:24:22 -07:00 • committed by GitHub
commit 6c323e783b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -760,4 +760,71 @@ public static class JsonRpcMessageConverterTests
var error = (JsonRpcError)message;
Assert.NotNull(error.Error.Data);
}
[Fact]
public static void Deserialize_ErrorWithNullId_IsValid()
{
// Per JSON-RPC 2.0 §5.1, when an error occurs before the request id can be determined
// (parse error or invalid request), the server MUST respond with id=null. This shape is
// produced by some peers (e.g. Python's simple-streamablehttp-stateless on a draft probe)
// and must be accepted so the HTTP-fallback path can recognize the structured signal.
string json = """{"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"Bad Request"}}""";
var message = JsonSerializer.Deserialize<JsonRpcMessage>(json, McpJsonUtilities.DefaultOptions);
Assert.NotNull(message);
var error = Assert.IsType<JsonRpcError>(message);
Assert.Equal(default(RequestId), error.Id);
Assert.Equal(-32600, error.Error.Code);
Assert.Equal("Bad Request", error.Error.Message);
}
[Fact]
public static void Deserialize_ErrorWithMissingId_IsValid()
{
// Some peers omit `id` entirely on pre-routing errors; treat as null per JSON-RPC 2.0 §5.1.
string json = """{"jsonrpc":"2.0","error":{"code":-32700,"message":"Parse error"}}""";
var message = JsonSerializer.Deserialize<JsonRpcMessage>(json, McpJsonUtilities.DefaultOptions);
Assert.NotNull(message);
var error = Assert.IsType<JsonRpcError>(message);
Assert.Equal(default(RequestId), error.Id);
Assert.Equal(-32700, error.Error.Code);
}
[Fact]
public static void Deserialize_RequestWithExplicitNullId_Throws()
{
// A message carrying a `method` and an explicit `id:null` is a malformed request. Per the MCP
// base protocol the request id "MUST NOT be null", and a null id does NOT denote a notification
// (JSON-RPC 2.0 notifications omit the id member entirely). The converter must reject it rather
// than silently downgrading to a notification (which would swallow the id and skip the response).
string json = """{"jsonrpc":"2.0","id":null,"method":"tools/list"}""";
Assert.Throws<JsonException>(() =>
JsonSerializer.Deserialize<JsonRpcMessage>(json, McpJsonUtilities.DefaultOptions));
}
[Fact]
public static void Deserialize_RequestWithExplicitNullIdAndParams_Throws()
{
string json = """{"jsonrpc":"2.0","id":null,"method":"tools/call","params":{"name":"echo"}}""";
Assert.Throws<JsonException>(() =>
JsonSerializer.Deserialize<JsonRpcMessage>(json, McpJsonUtilities.DefaultOptions));
}
[Fact]
public static void Deserialize_NotificationWithoutIdMember_IsNotConfusedWithNullIdRequest()
{
// Contrast with the explicit-null-id case above: omitting the id member entirely is a valid
// notification and must continue to deserialize as one.
string json = """{"jsonrpc":"2.0","method":"notifications/cancelled","params":{"requestId":1}}""";
var message = JsonSerializer.Deserialize<JsonRpcMessage>(json, McpJsonUtilities.DefaultOptions);
var notification = Assert.IsType<JsonRpcNotification>(message);
Assert.Equal("notifications/cancelled", notification.Method);
}
}