|
1 | 1 | using ModelContextProtocol.Server; |
2 | 2 | using System.ComponentModel; |
| 3 | +using System.Diagnostics; |
3 | 4 | using System.Text.Json; |
| 5 | +using System.Text.Json.Nodes; |
4 | 6 | using System.Text.Json.Serialization; |
5 | 7 |
|
6 | 8 | namespace ModelContextProtocol.Protocol; |
@@ -78,53 +80,127 @@ public sealed class Converter : JsonConverter<JsonRpcMessage> |
78 | 80 | throw new JsonException("Expected StartObject token"); |
79 | 81 | } |
80 | 82 |
|
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; |
83 | 91 |
|
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) |
87 | 93 | { |
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"); |
95 | 96 |
|
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) |
103 | 98 | { |
104 | | - return JsonSerializer.Deserialize(rawText, options.GetTypeInfo<JsonRpcError>()); |
| 99 | + break; |
105 | 100 | } |
106 | 101 |
|
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) |
109 | 109 | { |
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; |
111 | 144 | } |
| 145 | + } |
112 | 146 |
|
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"); |
114 | 151 | } |
115 | 152 |
|
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) |
118 | 155 | { |
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 | + } |
120 | 175 | } |
121 | 176 |
|
122 | | - // Messages with both method and id are requests |
123 | | - if (hasMethod && hasId) |
| 177 | + if (id.Id is not null) |
124 | 178 | { |
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"); |
126 | 201 | } |
127 | 202 |
|
| 203 | + // Error: Messages with neither id nor method are invalid |
128 | 204 | throw new JsonException("Invalid JSON-RPC message format"); |
129 | 205 | } |
130 | 206 |
|
|
0 commit comments