diff --git a/.gitignore b/.gitignore
index 171615f9..2184f142 100644
--- a/.gitignore
+++ b/.gitignore
@@ -80,4 +80,8 @@ docs/api
# Rider
.idea/
-.idea_modules/
\ No newline at end of file
+.idea_modules/
+
+# Benchmarkdotnet
+
+benchmarks/ModelContextProtocol.Benchmarks/BenchmarkDotNet.Artifacts/
\ No newline at end of file
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 6da9521f..9320d991 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -79,5 +79,6 @@
+
\ No newline at end of file
diff --git a/ModelContextProtocol.slnx b/ModelContextProtocol.slnx
index 5ed8ba0d..730ffd4a 100644
--- a/ModelContextProtocol.slnx
+++ b/ModelContextProtocol.slnx
@@ -40,4 +40,7 @@
+
+
+
diff --git a/benchmarks/ModelContextProtocol.Benchmarks/JsonRpcMessageSerializationBenchmarks.cs b/benchmarks/ModelContextProtocol.Benchmarks/JsonRpcMessageSerializationBenchmarks.cs
new file mode 100644
index 00000000..f9e99994
--- /dev/null
+++ b/benchmarks/ModelContextProtocol.Benchmarks/JsonRpcMessageSerializationBenchmarks.cs
@@ -0,0 +1,72 @@
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using BenchmarkDotNet.Attributes;
+using ModelContextProtocol.Protocol;
+
+namespace ModelContextProtocol.Benchmarks;
+
+[MemoryDiagnoser]
+public class JsonRpcMessageSerializationBenchmarks
+{
+ private byte[] _requestJson = null!;
+ private byte[] _notificationJson = null!;
+ private byte[] _responseJson = null!;
+ private byte[] _errorJson = null!;
+
+ private JsonSerializerOptions _options = null!;
+
+ [GlobalSetup]
+ public void Setup()
+ {
+ _options = McpJsonUtilities.DefaultOptions;
+
+ _requestJson = JsonSerializer.SerializeToUtf8Bytes(
+ new JsonRpcRequest
+ {
+ Id = new RequestId("1"),
+ Method = "test",
+ Params = JsonValue.Create(1)
+ },
+ _options);
+
+ _notificationJson = JsonSerializer.SerializeToUtf8Bytes(
+ new JsonRpcNotification
+ {
+ Method = "notify",
+ Params = JsonValue.Create(2)
+ },
+ _options);
+
+ _responseJson = JsonSerializer.SerializeToUtf8Bytes(
+ new JsonRpcResponse
+ {
+ Id = new RequestId("1"),
+ Result = JsonValue.Create(3)
+ },
+ _options);
+
+ _errorJson = JsonSerializer.SerializeToUtf8Bytes(
+ new JsonRpcError
+ {
+ Id = new RequestId("1"),
+ Error = new JsonRpcErrorDetail { Code = 42, Message = "oops" }
+ },
+ _options);
+ }
+
+ [Benchmark]
+ public JsonRpcMessage DeserializeRequest() =>
+ JsonSerializer.Deserialize(_requestJson, _options)!;
+
+ [Benchmark]
+ public JsonRpcMessage DeserializeNotification() =>
+ JsonSerializer.Deserialize(_notificationJson, _options)!;
+
+ [Benchmark]
+ public JsonRpcMessage DeserializeResponse() =>
+ JsonSerializer.Deserialize(_responseJson, _options)!;
+
+ [Benchmark]
+ public JsonRpcMessage DeserializeError() =>
+ JsonSerializer.Deserialize(_errorJson, _options)!;
+}
\ No newline at end of file
diff --git a/benchmarks/ModelContextProtocol.Benchmarks/ModelContextProtocol.Benchmarks.csproj b/benchmarks/ModelContextProtocol.Benchmarks/ModelContextProtocol.Benchmarks.csproj
new file mode 100644
index 00000000..c25c9dfe
--- /dev/null
+++ b/benchmarks/ModelContextProtocol.Benchmarks/ModelContextProtocol.Benchmarks.csproj
@@ -0,0 +1,19 @@
+
+
+
+ Exe
+ net9.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
+
diff --git a/benchmarks/ModelContextProtocol.Benchmarks/Program.cs b/benchmarks/ModelContextProtocol.Benchmarks/Program.cs
new file mode 100644
index 00000000..c9a04672
--- /dev/null
+++ b/benchmarks/ModelContextProtocol.Benchmarks/Program.cs
@@ -0,0 +1,3 @@
+using BenchmarkDotNet.Running;
+
+BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
diff --git a/src/ModelContextProtocol.Core/McpJsonUtilities.cs b/src/ModelContextProtocol.Core/McpJsonUtilities.cs
index 21e2468d..08281ee7 100644
--- a/src/ModelContextProtocol.Core/McpJsonUtilities.cs
+++ b/src/ModelContextProtocol.Core/McpJsonUtilities.cs
@@ -96,6 +96,9 @@ internal static bool IsValidMcpToolSchema(JsonElement element)
[JsonSerializable(typeof(JsonRpcNotification))]
[JsonSerializable(typeof(JsonRpcResponse))]
[JsonSerializable(typeof(JsonRpcError))]
+
+ // JSON-RPC union to make it faster to deserialize messages
+ [JsonSerializable(typeof(JsonRpcMessage.Converter.Union))]
// MCP Notification Params
[JsonSerializable(typeof(CancelledNotificationParams))]
diff --git a/src/ModelContextProtocol.Core/Protocol/JsonRpcError.cs b/src/ModelContextProtocol.Core/Protocol/JsonRpcError.cs
index 5de344db..0e0bdfcd 100644
--- a/src/ModelContextProtocol.Core/Protocol/JsonRpcError.cs
+++ b/src/ModelContextProtocol.Core/Protocol/JsonRpcError.cs
@@ -18,10 +18,12 @@ namespace ModelContextProtocol.Protocol;
///
public sealed class JsonRpcError : JsonRpcMessageWithId
{
+ internal const string ErrorPropertyName = "error";
+
///
/// Gets detailed error information for the failed request, containing an error code,
/// message, and optional additional data
///
- [JsonPropertyName("error")]
+ [JsonPropertyName(ErrorPropertyName)]
public required JsonRpcErrorDetail Error { get; init; }
}
diff --git a/src/ModelContextProtocol.Core/Protocol/JsonRpcMessage.cs b/src/ModelContextProtocol.Core/Protocol/JsonRpcMessage.cs
index b3176937..bd9fae31 100644
--- a/src/ModelContextProtocol.Core/Protocol/JsonRpcMessage.cs
+++ b/src/ModelContextProtocol.Core/Protocol/JsonRpcMessage.cs
@@ -1,6 +1,7 @@
using ModelContextProtocol.Server;
using System.ComponentModel;
using System.Text.Json;
+using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
namespace ModelContextProtocol.Protocol;
@@ -16,6 +17,8 @@ namespace ModelContextProtocol.Protocol;
[JsonConverter(typeof(Converter))]
public abstract class JsonRpcMessage
{
+ private const string JsonRpcPropertyName = "jsonrpc";
+
/// Prevent external derivations.
private protected JsonRpcMessage()
{
@@ -25,7 +28,7 @@ private protected JsonRpcMessage()
/// Gets the JSON-RPC protocol version used.
///
///
- [JsonPropertyName("jsonrpc")]
+ [JsonPropertyName(JsonRpcPropertyName)]
public string JsonRpc { get; init; } = "2.0";
///
@@ -75,6 +78,48 @@ private protected JsonRpcMessage()
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed class Converter : JsonConverter
{
+ ///
+ /// The union to deserialize.
+ ///
+ internal struct Union
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName(JsonRpcPropertyName)]
+ public string JsonRpc { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName(JsonRpcMessageWithId.IdPropertyName)]
+ public RequestId Id { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName(JsonRpcRequest.MethodPropertyName)]
+ public string? Method { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName(JsonRpcRequest.ParamsPropertyName)]
+ public JsonNode? Params { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName(JsonRpcError.ErrorPropertyName)]
+ public JsonRpcErrorDetail? Error { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName(JsonRpcResponse.ResultPropertyName)]
+ public JsonNode? Result { get; set; }
+ }
+
///
public override JsonRpcMessage? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
@@ -83,51 +128,63 @@ public sealed class Converter : JsonConverter
throw new JsonException("Expected StartObject token");
}
- using var doc = JsonDocument.ParseValue(ref reader);
- var root = doc.RootElement;
+ var union = JsonSerializer.Deserialize(ref reader, options.GetTypeInfo());
// All JSON-RPC messages must have a jsonrpc property with value "2.0"
- if (!root.TryGetProperty("jsonrpc", out var versionProperty) ||
- versionProperty.GetString() != "2.0")
+ if (union.JsonRpc != "2.0")
{
throw new JsonException("Invalid or missing jsonrpc version");
}
- // Determine the message type based on the presence of id, method, and error properties
- bool hasId = root.TryGetProperty("id", out _);
- bool hasMethod = root.TryGetProperty("method", out _);
- bool hasError = root.TryGetProperty("error", out _);
-
- var rawText = root.GetRawText();
-
// Messages with an id but no method are responses
- if (hasId && !hasMethod)
+ if (union.Id.HasValue && union.Method is null)
{
// Messages with an error property are error responses
- if (hasError)
+ if (union.Error != null)
{
- return JsonSerializer.Deserialize(rawText, options.GetTypeInfo());
+ return new JsonRpcError
+ {
+ Id = union.Id,
+ Error = union.Error,
+ JsonRpc = union.JsonRpc,
+ };
}
// Messages with a result property are success responses
- if (root.TryGetProperty("result", out _))
+ if (union.Result != null)
{
- return JsonSerializer.Deserialize(rawText, options.GetTypeInfo());
+ return new JsonRpcResponse
+ {
+ Id = union.Id,
+ Result = union.Result,
+ JsonRpc = union.JsonRpc,
+ };
}
throw new JsonException("Response must have either result or error");
}
// Messages with a method but no id are notifications
- if (hasMethod && !hasId)
+ if (union.Method != null && !union.Id.HasValue)
{
- return JsonSerializer.Deserialize(rawText, options.GetTypeInfo());
+ return new JsonRpcNotification
+ {
+ Method = union.Method,
+ JsonRpc = union.JsonRpc,
+ Params = union.Params,
+ };
}
// Messages with both method and id are requests
- if (hasMethod && hasId)
+ if (union.Method != null && union.Id.HasValue)
{
- return JsonSerializer.Deserialize(rawText, options.GetTypeInfo());
+ return new JsonRpcRequest
+ {
+ Id = union.Id,
+ Method = union.Method,
+ JsonRpc = union.JsonRpc,
+ Params = union.Params,
+ };
}
throw new JsonException("Invalid JSON-RPC message format");
diff --git a/src/ModelContextProtocol.Core/Protocol/JsonRpcMessageWithId.cs b/src/ModelContextProtocol.Core/Protocol/JsonRpcMessageWithId.cs
index 8233df48..a32d72d8 100644
--- a/src/ModelContextProtocol.Core/Protocol/JsonRpcMessageWithId.cs
+++ b/src/ModelContextProtocol.Core/Protocol/JsonRpcMessageWithId.cs
@@ -14,6 +14,8 @@ namespace ModelContextProtocol.Protocol;
///
public abstract class JsonRpcMessageWithId : JsonRpcMessage
{
+ internal const string IdPropertyName = "id";
+
/// Prevent external derivations.
private protected JsonRpcMessageWithId()
{
@@ -25,6 +27,6 @@ private protected JsonRpcMessageWithId()
///
/// Each ID is expected to be unique within the context of a given session.
///
- [JsonPropertyName("id")]
+ [JsonPropertyName(IdPropertyName)]
public RequestId Id { get; init; }
}
diff --git a/src/ModelContextProtocol.Core/Protocol/JsonRpcRequest.cs b/src/ModelContextProtocol.Core/Protocol/JsonRpcRequest.cs
index ed6c8982..037f7b04 100644
--- a/src/ModelContextProtocol.Core/Protocol/JsonRpcRequest.cs
+++ b/src/ModelContextProtocol.Core/Protocol/JsonRpcRequest.cs
@@ -16,16 +16,19 @@ namespace ModelContextProtocol.Protocol;
///
public sealed class JsonRpcRequest : JsonRpcMessageWithId
{
+ internal const string MethodPropertyName = "method";
+ internal const string ParamsPropertyName = "params";
+
///
/// Name of the method to invoke.
///
- [JsonPropertyName("method")]
+ [JsonPropertyName(MethodPropertyName)]
public required string Method { get; init; }
///
/// Optional parameters for the method.
///
- [JsonPropertyName("params")]
+ [JsonPropertyName(ParamsPropertyName)]
public JsonNode? Params { get; init; }
internal JsonRpcRequest WithId(RequestId id)
diff --git a/src/ModelContextProtocol.Core/Protocol/JsonRpcResponse.cs b/src/ModelContextProtocol.Core/Protocol/JsonRpcResponse.cs
index c7d824b7..86889d2d 100644
--- a/src/ModelContextProtocol.Core/Protocol/JsonRpcResponse.cs
+++ b/src/ModelContextProtocol.Core/Protocol/JsonRpcResponse.cs
@@ -18,12 +18,14 @@ namespace ModelContextProtocol.Protocol;
///
public sealed class JsonRpcResponse : JsonRpcMessageWithId
{
+ internal const string ResultPropertyName = "result";
+
///
/// Gets the result of the method invocation.
///
///
/// This property contains the result data returned by the server in response to the JSON-RPC method request.
///
- [JsonPropertyName("result")]
+ [JsonPropertyName(ResultPropertyName)]
public required JsonNode? Result { get; init; }
}
diff --git a/src/ModelContextProtocol.Core/Protocol/RequestId.cs b/src/ModelContextProtocol.Core/Protocol/RequestId.cs
index 8d445deb..b302e26c 100644
--- a/src/ModelContextProtocol.Core/Protocol/RequestId.cs
+++ b/src/ModelContextProtocol.Core/Protocol/RequestId.cs
@@ -34,6 +34,11 @@ public RequestId(long value)
/// This will either be a , a boxed , or .
public object? Id => _id;
+ ///
+ /// Returns true if the underlying id is set.
+ ///
+ public bool HasValue => _id != null;
+
///
public override string ToString() =>
_id is string stringValue ? stringValue :