using System.ComponentModel; using System.Text.Json; using System.Text.Json.Serialization; namespace ModelContextProtocol.Protocol; /// /// Represents any JSON-RPC message used in the Model Context Protocol (MCP). /// /// /// This interface serves as the foundation for all message types in the JSON-RPC 2.0 protocol /// used by MCP, including requests, responses, notifications, and errors. JSON-RPC is a stateless, /// lightweight remote procedure call (RPC) protocol that uses JSON as its data format. /// [JsonConverter(typeof(Converter))] public abstract class JsonRpcMessage { /// /// Gets the JSON-RPC protocol version used. /// /// [JsonPropertyName("jsonrpc")] public string JsonRpc { get; init; } = "2.0"; /// /// Gets or sets the transport the was received on or should be sent over. /// /// /// This is used to support the Streamable HTTP transport where the specification states that the server /// SHOULD include JSON-RPC responses in the HTTP response body for the POST request containing /// the corresponding JSON-RPC request. It may be for other transports. /// [JsonIgnore] public ITransport? RelatedTransport { get; set; } /// /// Provides a for messages, /// handling polymorphic deserialization of different message types. /// /// /// /// This converter is responsible for correctly deserializing JSON-RPC messages into their appropriate /// concrete types based on the message structure. It analyzes the JSON payload and determines if it /// represents a request, notification, successful response, or error response. /// /// /// The type determination rules follow the JSON-RPC 2.0 specification: /// /// Messages with "method" and "id" properties are deserialized as . /// Messages with "method" but no "id" property are deserialized as . /// Messages with "id" and "result" properties are deserialized as . /// Messages with "id" and "error" properties are deserialized as . /// /// /// [EditorBrowsable(EditorBrowsableState.Never)] public sealed class Converter : JsonConverter { /// public override JsonRpcMessage? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType != JsonTokenType.StartObject) { throw new JsonException("Expected StartObject token"); } using var doc = JsonDocument.ParseValue(ref reader); var root = doc.RootElement; // All JSON-RPC messages must have a jsonrpc property with value "2.0" if (!root.TryGetProperty("jsonrpc", out var versionProperty) || versionProperty.GetString() != "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) { // Messages with an error property are error responses if (hasError) { return JsonSerializer.Deserialize(rawText, options.GetTypeInfo()); } // Messages with a result property are success responses if (root.TryGetProperty("result", out _)) { return JsonSerializer.Deserialize(rawText, options.GetTypeInfo()); } throw new JsonException("Response must have either result or error"); } // Messages with a method but no id are notifications if (hasMethod && !hasId) { return JsonSerializer.Deserialize(rawText, options.GetTypeInfo()); } // Messages with both method and id are requests if (hasMethod && hasId) { return JsonSerializer.Deserialize(rawText, options.GetTypeInfo()); } throw new JsonException("Invalid JSON-RPC message format"); } /// public override void Write(Utf8JsonWriter writer, JsonRpcMessage value, JsonSerializerOptions options) { switch (value) { case JsonRpcRequest request: JsonSerializer.Serialize(writer, request, options.GetTypeInfo()); break; case JsonRpcNotification notification: JsonSerializer.Serialize(writer, notification, options.GetTypeInfo()); break; case JsonRpcResponse response: JsonSerializer.Serialize(writer, response, options.GetTypeInfo()); break; case JsonRpcError error: JsonSerializer.Serialize(writer, error, options.GetTypeInfo()); break; default: throw new JsonException($"Unknown JSON-RPC message type: {value.GetType()}"); } } } }