Skip to content

Commit 2bc105b

Browse files
CopilotScooletzeiriktsarpalisstephentoub
authored
Optimize JsonRpcMessage deserialization with single-pass parsing (#1138)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: scooletz <scooletz@gmail.com> Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com> Co-authored-by: Eirik Tsarpalis <eirik.tsarpalis@gmail.com> Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
1 parent 4a2c36c commit 2bc105b

2 files changed

Lines changed: 411 additions & 30 deletions

File tree

src/ModelContextProtocol.Core/Protocol/JsonRpcMessage.cs

Lines changed: 106 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
using ModelContextProtocol.Server;
22
using System.ComponentModel;
3+
using System.Diagnostics;
34
using System.Text.Json;
5+
using System.Text.Json.Nodes;
46
using System.Text.Json.Serialization;
57

68
namespace ModelContextProtocol.Protocol;
@@ -78,53 +80,127 @@ public sealed class Converter : JsonConverter<JsonRpcMessage>
7880
throw new JsonException("Expected StartObject token");
7981
}
8082

81-
using var doc = JsonDocument.ParseValue(ref reader);
82-
var root = doc.RootElement;
83+
// Local variables for parsed message data
84+
bool hasJsonRpc = false;
85+
RequestId id = default;
86+
string? method = null;
87+
JsonNode? parameters = null;
88+
JsonRpcErrorDetail? error = null;
89+
JsonNode? result = null;
90+
bool hasResult = false;
8391

84-
// All JSON-RPC messages must have a jsonrpc property with value "2.0"
85-
if (!root.TryGetProperty("jsonrpc", out var versionProperty) ||
86-
versionProperty.GetString() != "2.0")
92+
while (true)
8793
{
88-
throw new JsonException("Invalid or missing jsonrpc version");
89-
}
90-
91-
// Determine the message type based on the presence of id, method, and error properties
92-
bool hasId = root.TryGetProperty("id", out _);
93-
bool hasMethod = root.TryGetProperty("method", out _);
94-
bool hasError = root.TryGetProperty("error", out _);
94+
bool success = reader.Read();
95+
Debug.Assert(success, "custom converters are guaranteed to be passed fully buffered objects");
9596

96-
var rawText = root.GetRawText();
97-
98-
// Messages with an id but no method are responses
99-
if (hasId && !hasMethod)
100-
{
101-
// Messages with an error property are error responses
102-
if (hasError)
97+
if (reader.TokenType is JsonTokenType.EndObject)
10398
{
104-
return JsonSerializer.Deserialize(rawText, options.GetTypeInfo<JsonRpcError>());
99+
break;
105100
}
106101

107-
// Messages with a result property are success responses
108-
if (root.TryGetProperty("result", out _))
102+
Debug.Assert(reader.TokenType is JsonTokenType.PropertyName);
103+
string propertyName = reader.GetString()!;
104+
105+
success = reader.Read();
106+
Debug.Assert(success, "custom converters are guaranteed to be passed fully buffered objects");
107+
108+
switch (propertyName)
109109
{
110-
return JsonSerializer.Deserialize(rawText, options.GetTypeInfo<JsonRpcResponse>());
110+
case "jsonrpc":
111+
// Validate that the value is "2.0" without allocating a string
112+
if (!reader.ValueTextEquals("2.0"u8))
113+
{
114+
throw new JsonException("Invalid jsonrpc version");
115+
}
116+
hasJsonRpc = true;
117+
break;
118+
119+
case "id":
120+
id = JsonSerializer.Deserialize(ref reader, options.GetTypeInfo<RequestId>());
121+
break;
122+
123+
case "method":
124+
method = reader.GetString();
125+
break;
126+
127+
case "params":
128+
parameters = JsonSerializer.Deserialize(ref reader, options.GetTypeInfo<JsonNode>());
129+
break;
130+
131+
case "error":
132+
error = JsonSerializer.Deserialize(ref reader, options.GetTypeInfo<JsonRpcErrorDetail>());
133+
break;
134+
135+
case "result":
136+
result = JsonSerializer.Deserialize(ref reader, options.GetTypeInfo<JsonNode>());
137+
hasResult = true;
138+
break;
139+
140+
default:
141+
// Skip unknown properties
142+
reader.Skip();
143+
break;
111144
}
145+
}
112146

113-
throw new JsonException("Response must have either result or error");
147+
// All JSON-RPC messages must have a jsonrpc property with value "2.0"
148+
if (!hasJsonRpc)
149+
{
150+
throw new JsonException("Missing jsonrpc version");
114151
}
115152

116-
// Messages with a method but no id are notifications
117-
if (hasMethod && !hasId)
153+
// Determine message type based on presence of id and method properties
154+
if (method is not null)
118155
{
119-
return JsonSerializer.Deserialize(rawText, options.GetTypeInfo<JsonRpcNotification>());
156+
if (id.Id is not null)
157+
{
158+
// Messages with both method and id are requests
159+
return new JsonRpcRequest
160+
{
161+
Id = id,
162+
Method = method,
163+
Params = parameters
164+
};
165+
}
166+
else
167+
{
168+
// Messages with a method but no id are notifications
169+
return new JsonRpcNotification
170+
{
171+
Method = method,
172+
Params = parameters
173+
};
174+
}
120175
}
121176

122-
// Messages with both method and id are requests
123-
if (hasMethod && hasId)
177+
if (id.Id is not null)
124178
{
125-
return JsonSerializer.Deserialize(rawText, options.GetTypeInfo<JsonRpcRequest>());
179+
if (error is not null)
180+
{
181+
// Messages with an error and id are error responses
182+
return new JsonRpcError
183+
{
184+
Id = id,
185+
Error = error
186+
};
187+
}
188+
189+
if (hasResult)
190+
{
191+
// Messages with a result and id are success responses
192+
return new JsonRpcResponse
193+
{
194+
Id = id,
195+
Result = result
196+
};
197+
}
198+
199+
// Error: Messages with an id but no method, error, or result are invalid
200+
throw new JsonException("Response must have either result or error");
126201
}
127202

203+
// Error: Messages with neither id nor method are invalid
128204
throw new JsonException("Invalid JSON-RPC message format");
129205
}
130206

0 commit comments

Comments
 (0)