using ModelContextProtocol.Server;
using System.ComponentModel;
using System.Diagnostics;
using System.Text.Json;
using System.Text.Json.Nodes;
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
{
/// Prevent external derivations.
private protected JsonRpcMessage()
{
}
///
/// Gets or sets the JSON-RPC protocol version used.
///
///
[JsonPropertyName("jsonrpc")]
public string JsonRpc { get; set; } = "2.0";
///
/// Gets or sets the contextual information for this JSON-RPC message.
///
///
///
/// This property contains transport-specific and runtime context information that accompanies
/// JSON-RPC messages but is not serialized as part of the JSON-RPC payload. This includes
/// transport references, execution context, and authenticated user information.
///
///
/// This property should only be set when implementing a custom
/// that needs to pass additional per-message context or to pass a
/// to
/// or .
///
///
[JsonIgnore]
public JsonRpcMessageContext? Context { 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");
}
// Local variables for parsed message data
bool hasJsonRpc = false;
RequestId id = default;
string? method = null;
JsonNode? parameters = null;
JsonRpcErrorDetail? error = null;
JsonNode? result = null;
bool hasResult = false;
while (true)
{
bool success = reader.Read();
Debug.Assert(success, "custom converters are guaranteed to be passed fully buffered objects");
if (reader.TokenType is JsonTokenType.EndObject)
{
break;
}
Debug.Assert(reader.TokenType is JsonTokenType.PropertyName);
string propertyName = reader.GetString()!;
success = reader.Read();
Debug.Assert(success, "custom converters are guaranteed to be passed fully buffered objects");
switch (propertyName)
{
case "jsonrpc":
// Validate that the value is "2.0" without allocating a string
if (!reader.ValueTextEquals("2.0"u8))
{
throw new JsonException("Invalid jsonrpc version");
}
hasJsonRpc = true;
break;
case "id":
id = JsonSerializer.Deserialize(ref reader, options.GetTypeInfo());
break;
case "method":
method = reader.GetString();
break;
case "params":
parameters = JsonSerializer.Deserialize(ref reader, options.GetTypeInfo());
break;
case "error":
error = JsonSerializer.Deserialize(ref reader, options.GetTypeInfo());
break;
case "result":
result = JsonSerializer.Deserialize(ref reader, options.GetTypeInfo());
hasResult = true;
break;
default:
// Skip unknown properties
reader.Skip();
break;
}
}
// All JSON-RPC messages must have a jsonrpc property with value "2.0"
if (!hasJsonRpc)
{
throw new JsonException("Missing jsonrpc version");
}
// Determine message type based on presence of id and method properties
if (method is not null)
{
if (id.Id is not null)
{
// Messages with both method and id are requests
return new JsonRpcRequest
{
Id = id,
Method = method,
Params = parameters
};
}
else
{
// Messages with a method but no id are notifications
return new JsonRpcNotification
{
Method = method,
Params = parameters
};
}
}
if (id.Id is not null)
{
if (error is not null)
{
// Messages with an error and id are error responses
return new JsonRpcError
{
Id = id,
Error = error
};
}
if (hasResult)
{
// Messages with a result and id are success responses
return new JsonRpcResponse
{
Id = id,
Result = result
};
}
// Error: Messages with an id but no method, error, or result are invalid
throw new JsonException("Response must have either result or error");
}
// Error: Messages with neither id nor method are invalid
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()}");
}
}
}
}